type: 收敛测试 schemas 与协议层类型

This commit is contained in:
yml2213
2026-08-30 20:35:08 +08:00
parent 92dc461e52
commit c891ac982e
26 changed files with 1846 additions and 882 deletions
+77 -30
View File
@@ -10,9 +10,10 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Optional
from typing import Optional, cast
from core.douyu import DouyuLogin, WgapiLoginAPI
from core.douyu.login import AccountLike
from core.douyu.proxy_fetcher import ProxyFetcher
from ..models import ProxyConfig as ProxyConfigModel
@@ -118,13 +119,15 @@ def parse_account_check_lines(text: str) -> list[AccountCheckInput]:
f"或 账号|密码|邮箱|邮箱密码"
)
accounts.append(AccountCheckInput(
line=line_no,
username=parts[0],
password=parts[1],
email=parts[2],
email_password=parts[3],
))
accounts.append(
AccountCheckInput(
line=line_no,
username=parts[0],
password=parts[1],
email=parts[2],
email_password=parts[3],
)
)
return accounts
@@ -158,9 +161,15 @@ class AccountCheckRunner:
wl_platform = "xiequ"
wl_credentials = None
if self.proxy_config.whitelist_enabled:
wl_platform = getattr(self.proxy_config, "whitelist_platform", None) or "xiequ"
wl_platform = (
getattr(self.proxy_config, "whitelist_platform", None) or "xiequ"
)
wl_credentials = getattr(self.proxy_config, "whitelist_credentials", None)
if not wl_credentials and self.proxy_config.whitelist_uid and self.proxy_config.whitelist_ukey:
if (
not wl_credentials
and self.proxy_config.whitelist_uid
and self.proxy_config.whitelist_ukey
):
wl_credentials = {
"uid": self.proxy_config.whitelist_uid,
"ukey": self.proxy_config.whitelist_ukey,
@@ -209,25 +218,38 @@ class AccountCheckRunner:
def _run_one(self, index: int, account: AccountCheckInput):
if self._stop.is_set():
self._set_item(index, status="stopped", message="已停止", finished_at=_now())
self._set_item(
index, status="stopped", message="已停止", finished_at=_now()
)
return
proxy_dict, proxy_error = self._resolve_static_proxy()
if proxy_error:
self._set_item(index, status="error", message=proxy_error, finished_at=_now())
self._set_item(
index, status="error", message=proxy_error, finished_at=_now()
)
return
self._set_item(index, status="running", message="检测中", started_at=_now(), finished_at=None)
self._set_item(
index,
status="running",
message="检测中",
started_at=_now(),
finished_at=None,
)
try:
result = DouyuLogin(
SimpleNamespace(
username=account.username,
password=account.password,
email=account.email,
email_password=account.email_password,
email_imap_server="",
email_imap_port=993,
email_imap_ssl=True,
cast(
AccountLike,
SimpleNamespace(
username=account.username,
password=account.password,
email=account.email,
email_password=account.email_password,
email_imap_server="",
email_imap_port=993,
email_imap_ssl=True,
),
),
proxy=proxy_dict,
max_login_retries=self.batch.max_login_retries,
@@ -237,15 +259,24 @@ class AccountCheckRunner:
api_strategy=WgapiLoginAPI(),
).check_account()
except Exception as exc:
self._set_item(index, status="error", message=f"检测异常: {exc}", finished_at=_now())
self._set_item(
index, status="error", message=f"检测异常: {exc}", finished_at=_now()
)
return
if self._stop.is_set() and not result.success:
self._set_item(index, status="stopped", message=result.message or "已停止", finished_at=_now())
self._set_item(
index,
status="stopped",
message=result.message or "已停止",
finished_at=_now(),
)
return
if result.success:
status = result.code if result.code in STATUS_LABELS else "account_auth_unknown"
status = (
result.code if result.code in STATUS_LABELS else "account_auth_unknown"
)
message = result.message or STATUS_LABELS.get(status, "认证状态未知")
else:
status = "error"
@@ -260,7 +291,9 @@ class AccountCheckRunner:
status: sum(1 for item in self.batch.items if item.status == status)
for status in STATUS_LABELS
}
running_count = sum(1 for item in self.batch.items if item.status in {"pending", "running"})
running_count = sum(
1 for item in self.batch.items if item.status in {"pending", "running"}
)
finished_count = len(self.batch.items) - running_count
return {
"batch_id": self.batch.batch_id,
@@ -296,7 +329,10 @@ class AccountCheckRunner:
if item.status != status:
continue
line = item.export_text
if status in {"error", "stopped", "account_auth_unknown"} and item.message:
if (
status in {"error", "stopped", "account_auth_unknown"}
and item.message
):
line = f"{line}----{item.message}"
lines.append(line)
content = "\n".join(lines)
@@ -309,7 +345,9 @@ class AccountCheckRunner:
def run(self):
"""线程入口。"""
self._set_batch(status="running", message="批次运行中", started_at=_now(), finished_at=None)
self._set_batch(
status="running", message="批次运行中", started_at=_now(), finished_at=None
)
try:
if self._shared_proxy_fetcher:
self._shared_proxy_fetcher.warmup_whitelist()
@@ -318,14 +356,21 @@ class AccountCheckRunner:
futures = []
for index, account in enumerate(self.accounts):
if self._stop.is_set():
self._set_item(index, status="stopped", message="已停止", finished_at=_now())
self._set_item(
index,
status="stopped",
message="已停止",
finished_at=_now(),
)
continue
futures.append(executor.submit(self._run_one, index, account))
for future in as_completed(futures):
future.result()
except Exception as exc:
self._set_batch(status="error", message=f"批次执行异常: {exc}", finished_at=_now())
self._set_batch(
status="error", message=f"批次执行异常: {exc}", finished_at=_now()
)
return
if self._stop.is_set():
@@ -370,7 +415,9 @@ class AccountCheckRegistry:
for account in accounts
],
)
runner = AccountCheckRunner(batch=batch, accounts=accounts, proxy_config=proxy_config)
runner = AccountCheckRunner(
batch=batch, accounts=accounts, proxy_config=proxy_config
)
with self._lock:
self._runners[batch_id] = runner
return runner
+23 -8
View File
@@ -42,9 +42,17 @@ def check_douyu_cookie(cookie: str) -> dict:
).json()
if isinstance(fish_data, dict) and fish_data.get("error") in (0, "0"):
fish_ok = True
fish_ball = (fish_data.get("data") or {}).get("count") if isinstance(fish_data.get("data"), dict) else None
fish_ball = (
(fish_data.get("data") or {}).get("count")
if isinstance(fish_data.get("data"), dict)
else None
)
else:
fish_msg = str(fish_data.get("msg") or fish_data.get("error") or "响应异常") if isinstance(fish_data, dict) else "响应异常"
fish_msg = (
str(fish_data.get("msg") or fish_data.get("error") or "响应异常")
if isinstance(fish_data, dict)
else "响应异常"
)
except Exception as exc:
fish_msg = f"请求失败: {exc}"
@@ -66,11 +74,16 @@ def check_douyu_cookie(cookie: str) -> dict:
).json()
if isinstance(level_data, dict) and level_data.get("error") in (0, "0"):
level_ok = True
info = level_data.get("data") if isinstance(level_data.get("data"), dict) else {}
info_raw = level_data.get("data")
info = info_raw if isinstance(info_raw, dict) else {}
nickname = str(info.get("nn") or "") or None
level = info.get("lv")
else:
level_msg = str(level_data.get("msg") or level_data.get("error") or "响应异常") if isinstance(level_data, dict) else "响应异常"
level_msg = (
str(level_data.get("msg") or level_data.get("error") or "响应异常")
if isinstance(level_data, dict)
else "响应异常"
)
except Exception as exc:
level_msg = f"请求失败: {exc}"
@@ -78,10 +91,12 @@ def check_douyu_cookie(cookie: str) -> dict:
if valid:
message = "有效"
else:
message = "".join([
f"鱼丸接口: {'ok' if fish_ok else (fish_msg or '失败')}",
f"等级接口: {'ok' if level_ok else (level_msg or '失败')}",
])
message = "".join(
[
f"鱼丸接口: {'ok' if fish_ok else (fish_msg or '失败')}",
f"等级接口: {'ok' if level_ok else (level_msg or '失败')}",
]
)
return {
**base,
"valid": valid,
+26 -7
View File
@@ -21,7 +21,13 @@ from .douyu_runner_xpd import XpdMixin
class DouyuBatchRunner(
DouyuBatchRunnerCore, BindMixin, ManualMixin, GoldMixin, DonateMixin, GoodsMixin, XpdMixin,
DouyuBatchRunnerCore,
BindMixin,
ManualMixin,
GoldMixin,
DonateMixin,
GoodsMixin,
XpdMixin,
):
"""批量执行斗鱼活动任务(功能域 Mixin 聚合 + 批次调度)。"""
@@ -43,13 +49,18 @@ class DouyuBatchRunner(
self._started += 1
current = self._started
self._push_log("info", f"[{current}/{total}] 开始: {self._account_name(account)}")
self._push_log(
"info", f"[{current}/{total}] 开始: {self._account_name(account)}"
)
login_task = latest_success_login_task(worker_db, account.id)
cookie = login_task.cookie if login_task else ""
if not cookie:
self._mark_task(worker_db, task, "failed", "账号没有成功登录 Cookie")
self._push_log("warning", f"[{current}] {self._account_name(account)} 无 Cookie")
self._push_log(
"warning", f"[{current}] {self._account_name(account)} 无 Cookie"
)
return
assert login_task is not None
cookie_check = check_douyu_cookie(cookie)
login_task.ck_check_status = "valid" if cookie_check["valid"] else "invalid"
@@ -62,7 +73,9 @@ class DouyuBatchRunner(
if not cookie_check["valid"]:
message = f"Cookie 已失效,请重新登录:{cookie_check['message']}"
self._mark_task(worker_db, task, "failed", message)
self._push_log("warning", f"[{current}] {self._account_name(account)} {message}")
self._push_log(
"warning", f"[{current}] {self._account_name(account)} {message}"
)
return
update_account_profile_from_cookie(account, cookie)
@@ -109,7 +122,9 @@ class DouyuBatchRunner(
return
handler(worker_db, task, account, cookie, config)
self._push_log("success", f"[{current}] {self._account_name(account)} {task.message}")
self._push_log(
"success", f"[{current}] {self._account_name(account)} {task.message}"
)
except DouyuActivityError as exc:
if "task" in locals() and task:
self._mark_task(worker_db, task, "failed", str(exc))
@@ -128,7 +143,9 @@ class DouyuBatchRunner(
config = self._config_info(self.db)
tasks = (
self.db.query(DouyuTask)
.filter(DouyuTask.batch_id == self.batch_id, DouyuTask.status == "planned")
.filter(
DouyuTask.batch_id == self.batch_id, DouyuTask.status == "planned"
)
.order_by(DouyuTask.id.asc())
.all()
)
@@ -150,7 +167,9 @@ class DouyuBatchRunner(
for task in tasks:
if self._stop.is_set():
break
futures.append(executor.submit(self._execute_one, task.id, config, total))
futures.append(
executor.submit(self._execute_one, task.id, config, total)
)
for future in as_completed(futures):
try:
future.result()
+28 -10
View File
@@ -21,7 +21,12 @@ from ..models import (
DouyuXpdGoodsSnapshot,
ProxyConfig as ProxyConfigModel,
)
from .douyu_service import DOUYU_CONFIG_FIELDS, douyu_config_value, ensure_douyu_config, douyu_task_payload
from .douyu_service import (
DOUYU_CONFIG_FIELDS,
douyu_config_value,
ensure_douyu_config,
douyu_task_payload,
)
if TYPE_CHECKING:
from .douyu_runner import DouyuBatchRunner
@@ -78,7 +83,9 @@ class DouyuBatchRunnerCore:
self._proxy_fetcher = self._create_proxy_fetcher()
self._static_proxies = self._resolve_static_proxies()
if self._static_proxies:
logger.info(f"[douyu] 写操作任务将走静态代理: {self._static_proxies.get('https', '')}")
logger.info(
f"[douyu] 写操作任务将走静态代理: {self._static_proxies.get('https', '')}"
)
elif self._proxy_fetcher:
logger.info("[douyu] 写操作任务将按任务从代理 API 取新代理")
@@ -89,7 +96,11 @@ class DouyuBatchRunnerCore:
return None
wl_platform = getattr(cfg, "whitelist_platform", None) or "xiequ"
wl_credentials = getattr(cfg, "whitelist_credentials", None)
if not wl_credentials and getattr(cfg, "whitelist_uid", "") and getattr(cfg, "whitelist_ukey", ""):
if (
not wl_credentials
and getattr(cfg, "whitelist_uid", "")
and getattr(cfg, "whitelist_ukey", "")
):
wl_credentials = {"uid": cfg.whitelist_uid, "ukey": cfg.whitelist_ukey}
return ProxyFetcher(
api_url=cfg.api_url,
@@ -267,8 +278,7 @@ class DouyuBatchRunnerCore:
"""同步和平小店商品快照,移除上一次热门抢购等遗留商品。"""
now = datetime.now(timezone.utc)
commodity_ids = {
str(raw.get("commodity_id") or raw.get("iGoodsId") or "")
for raw in goods
str(raw.get("commodity_id") or raw.get("iGoodsId") or "") for raw in goods
}
commodity_ids.discard("")
query = db.query(DouyuXpdGoodsSnapshot)
@@ -304,11 +314,15 @@ class DouyuBatchRunnerCore:
def _config_info(self, db: Session) -> dict:
config = ensure_douyu_config(db)
return {field: douyu_config_value(field, getattr(config, field, None)) for field in DOUYU_CONFIG_FIELDS}
return {
field: douyu_config_value(field, getattr(config, field, None))
for field in DOUYU_CONFIG_FIELDS
}
def _task_payload(self, task: DouyuTask) -> dict:
result = task.result if isinstance(task.result, dict) else {}
payload = result.get("payload") if isinstance(result.get("payload"), dict) else {}
payload_raw = result.get("payload")
payload = payload_raw if isinstance(payload_raw, dict) else {}
return {**payload, **self.payload}
def _client(self, cookie: str) -> DouyuActivityClient:
@@ -338,8 +352,13 @@ class DouyuBatchRegistry:
def __init__(self):
self._batches: dict[str, dict] = {}
def register(self, batch_id: str, log_queue: asyncio.Queue,
loop: asyncio.AbstractEventLoop, runner: DouyuBatchRunner):
def register(
self,
batch_id: str,
log_queue: asyncio.Queue,
loop: asyncio.AbstractEventLoop,
runner: DouyuBatchRunner,
):
self._batches[batch_id] = {
"log_queue": log_queue,
"loop": loop,
@@ -368,4 +387,3 @@ class DouyuBatchRegistry:
douyu_batch_registry = DouyuBatchRegistry()
+99 -34
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import uuid
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from core.douyu.activity_client import DouyuActivityClient
@@ -13,7 +14,8 @@ from core.douyu.cookie_utils import cookie_value
from ..models import Account, DouyuConfig, DouyuTask, LoginTask
SUPPORTED_DOUYU_TASK_TYPES = { "get_bind_qr": "获取绑定二维码",
SUPPORTED_DOUYU_TASK_TYPES = {
"get_bind_qr": "获取绑定二维码",
"confirm_bind": "确认绑定",
"create_elite_qr": "开通精英宝典30",
"prepare_esports_bind": "绑定电竞手册角色",
@@ -53,26 +55,55 @@ SUPPORTED_DOUYU_TASK_TYPES = { "get_bind_qr": "获取绑定二维码",
DOUYU_HANDBOOK_SCOPES = {"elite", "esports", "peace"}
DOUYU_HANDBOOK_TASK_TYPES = {
"elite": {
"get_bind_qr", "confirm_bind", "create_elite_qr", "create_gold_qr", "donate_elite_gift",
"query_points", "lock_goods", "pay_locked_order", "exchange_goods", "query_game_name", "query_change_bind_time",
"query_limited_goods", "query_gold_balance", "refresh_goods", "query_exchange_records",
"get_bind_qr",
"confirm_bind",
"create_elite_qr",
"create_gold_qr",
"donate_elite_gift",
"query_points",
"lock_goods",
"pay_locked_order",
"exchange_goods",
"query_game_name",
"query_change_bind_time",
"query_limited_goods",
"query_gold_balance",
"refresh_goods",
"query_exchange_records",
"prefetch_csrf_token",
},
"esports": {
"prepare_esports_bind", "get_esports_bind_qr", "query_esports_game_name", "confirm_esports_bind",
"create_esports_qr", "query_esports_points", "query_gold_balance", "query_change_bind_time",
"query_limited_goods", "refresh_esports_goods", "exchange_esports_goods", "create_gold_qr",
"donate_esports_chicken_gift", "donate_esports_firework_gift",
"prepare_esports_bind",
"get_esports_bind_qr",
"query_esports_game_name",
"confirm_esports_bind",
"create_esports_qr",
"query_esports_points",
"query_gold_balance",
"query_change_bind_time",
"query_limited_goods",
"refresh_esports_goods",
"exchange_esports_goods",
"create_gold_qr",
"donate_esports_chicken_gift",
"donate_esports_firework_gift",
},
"peace": {
"get_xpd_bind_qr", "query_xpd_bind_info", "confirm_xpd_bind", "query_xpd_role",
"refresh_xpd_goods", "query_xpd_balance", "query_xpd_fragments",
"query_xpd_purchase_records", "exchange_xpd_goods",
"get_xpd_bind_qr",
"query_xpd_bind_info",
"confirm_xpd_bind",
"query_xpd_role",
"refresh_xpd_goods",
"query_xpd_balance",
"query_xpd_fragments",
"query_xpd_purchase_records",
"exchange_xpd_goods",
},
}
DOUYU_CONFIG_DEFAULTS = { "manual_id": "G4KA4Qnz4LDp7",
DOUYU_CONFIG_DEFAULTS = {
"manual_id": "G4KA4Qnz4LDp7",
"rid": "9263298",
"bind_act_alias": "20260120QYOOB",
"confirm_act_alias": "20260120QYOOB",
@@ -117,12 +148,18 @@ def apply_douyu_config_defaults(config: DouyuConfig) -> bool:
"""补齐斗鱼配置默认值,返回是否发生变更。"""
changed = False
for field in DOUYU_CONFIG_FIELDS:
if field == "bind_act_alias" and str(getattr(config, field, "") or "").strip() == "20250213NQCYX":
if (
field == "bind_act_alias"
and str(getattr(config, field, "") or "").strip() == "20250213NQCYX"
):
setattr(config, field, DOUYU_CONFIG_DEFAULTS[field])
changed = True
continue
normalized = douyu_config_value(field, getattr(config, field, None))
if field == "gold_recharge_channel" and normalized not in {"wechat_qr", "supplier_api"}:
if field == "gold_recharge_channel" and normalized not in {
"wechat_qr",
"supplier_api",
}:
normalized = DOUYU_CONFIG_DEFAULTS[field]
if getattr(config, field, None) != normalized:
setattr(config, field, normalized)
@@ -178,7 +215,11 @@ def visible_douyu_task_accounts(db: Session, account_ids: list[int]) -> list[Acc
"""只保留存在成功 Cookie 的斗鱼账号。"""
if not account_ids:
return []
cookie_ids = cookie_account_ids_query(db).subquery()
cookie_ids = select(LoginTask.account_id).where(
LoginTask.status == "success",
LoginTask.cookie != "",
LoginTask.cookie.isnot(None),
)
return (
db.query(Account)
.filter(Account.id.in_(account_ids), Account.id.in_(cookie_ids))
@@ -217,23 +258,28 @@ def create_douyu_planned_tasks(
raise ValueError("该任务不属于当前工作台")
accounts = visible_douyu_task_accounts(db, account_ids)
if task_type in {"refresh_goods", "refresh_esports_goods", "refresh_xpd_goods"} and accounts:
if (
task_type in {"refresh_goods", "refresh_esports_goods", "refresh_xpd_goods"}
and accounts
):
# 商品快照是全局数据,一个可用 CK 足够;没有 CK 时前端无法选账号创建任务。
accounts = accounts[:1]
batch_id = uuid.uuid4().hex[:12]
payload = payload or {}
for account in accounts:
db.add(DouyuTask(
batch_id=batch_id,
account_id=account.id,
task_type=task_type,
handbook_scope=handbook_scope,
status="planned",
message="任务已创建,等待执行",
result={"payload": payload} if payload else None,
created_by=created_by,
))
db.add(
DouyuTask(
batch_id=batch_id,
account_id=account.id,
task_type=task_type,
handbook_scope=handbook_scope,
status="planned",
message="任务已创建,等待执行",
result={"payload": payload} if payload else None,
created_by=created_by,
)
)
db.commit()
return batch_id, len(accounts)
@@ -272,7 +318,17 @@ def slim_douyu_goods(goods: object) -> object:
return {
key: value
for key, value in goods.items()
if key in {"commodityId", "commodity_id", "commodityName", "name", "webPic", "pic", "score", "status"}
if key
in {
"commodityId",
"commodity_id",
"commodityName",
"name",
"webPic",
"pic",
"score",
"status",
}
}
@@ -284,11 +340,13 @@ def slim_douyu_limited_goods(goods: object) -> list[dict[str, object]]:
for item in goods[:5]:
if not isinstance(item, dict):
continue
result.append({
key: value
for key, value in item.items()
if key in {"commodityId", "commodity_id", "commodityName", "name"}
})
result.append(
{
key: value
for key, value in item.items()
if key in {"commodityId", "commodity_id", "commodityName", "name"}
}
)
return result
@@ -305,7 +363,9 @@ def strip_douyu_raw_snapshots(value: object) -> object:
return value
def sanitize_douyu_task_result(result: dict | None, task_type: str, *, include_detail: bool = False) -> dict | None:
def sanitize_douyu_task_result(
result: dict | None, task_type: str, *, include_detail: bool = False
) -> dict | None:
"""列表/实时推送接口剥离原始快照与大数组;详情接口保留完整 result。"""
if not isinstance(result, dict):
return result
@@ -346,7 +406,12 @@ def sanitize_douyu_task_result(result: dict | None, task_type: str, *, include_d
if "limited_goods" in data:
data["limited_goods"] = slim_douyu_limited_goods(data.get("limited_goods"))
if task_type not in {"get_bind_qr", "prepare_esports_bind", "get_esports_bind_qr", "get_xpd_bind_qr"}:
if task_type not in {
"get_bind_qr",
"prepare_esports_bind",
"get_esports_bind_qr",
"get_xpd_bind_qr",
}:
data.pop("url", None)
if task_type not in {"create_elite_qr", "create_esports_qr", "create_gold_qr"}:
data.pop("pay_url", None)
+170 -66
View File
@@ -8,12 +8,13 @@ import uuid
from types import SimpleNamespace
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from typing import Optional
from typing import Optional, cast
from sqlalchemy.orm import Session
from loguru import logger
from core.douyu import DouyuLogin, WgapiLoginAPI, IframeLoginAPI
from core.douyu.login import AccountLike
from core.douyu.proxy_fetcher import ProxyFetcher
from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
from .cookie_check_service import check_douyu_cookie
@@ -60,21 +61,28 @@ def get_relogin_limits() -> tuple[int, int]:
)
def _snapshot_proxy_config(proxy_config: Optional[ProxyConfigModel]) -> Optional[SimpleNamespace]:
def _snapshot_proxy_config(
proxy_config: Optional[ProxyConfigModel],
) -> Optional[ProxyConfigModel]:
"""复制代理配置,避免后台线程访问已关闭会话中的 ORM 对象。"""
if proxy_config is None:
return None
credentials = getattr(proxy_config, "whitelist_credentials", None)
return SimpleNamespace(
enabled=bool(getattr(proxy_config, "enabled", False)),
http=getattr(proxy_config, "http", "") or "",
https=getattr(proxy_config, "https", "") or "",
api_url=getattr(proxy_config, "api_url", "") or "",
whitelist_enabled=bool(getattr(proxy_config, "whitelist_enabled", False)),
whitelist_platform=getattr(proxy_config, "whitelist_platform", None),
whitelist_credentials=dict(credentials) if isinstance(credentials, dict) else credentials,
whitelist_uid=getattr(proxy_config, "whitelist_uid", "") or "",
whitelist_ukey=getattr(proxy_config, "whitelist_ukey", "") or "",
return cast(
ProxyConfigModel,
SimpleNamespace(
enabled=bool(getattr(proxy_config, "enabled", False)),
http=getattr(proxy_config, "http", "") or "",
https=getattr(proxy_config, "https", "") or "",
api_url=getattr(proxy_config, "api_url", "") or "",
whitelist_enabled=bool(getattr(proxy_config, "whitelist_enabled", False)),
whitelist_platform=getattr(proxy_config, "whitelist_platform", None),
whitelist_credentials=dict(credentials)
if isinstance(credentials, dict)
else credentials,
whitelist_uid=getattr(proxy_config, "whitelist_uid", "") or "",
whitelist_ukey=getattr(proxy_config, "whitelist_ukey", "") or "",
),
)
@@ -124,12 +132,21 @@ class LoginBatchRunner:
wl_platform = "xiequ"
wl_credentials = None
if proxy_config.whitelist_enabled:
wl_platform = getattr(proxy_config, 'whitelist_platform', None) or "xiequ"
wl_credentials = getattr(proxy_config, 'whitelist_credentials', None)
wl_platform = (
getattr(proxy_config, "whitelist_platform", None) or "xiequ"
)
wl_credentials = getattr(proxy_config, "whitelist_credentials", None)
# 向后兼容
if not wl_credentials and proxy_config.whitelist_uid and proxy_config.whitelist_ukey:
if (
not wl_credentials
and proxy_config.whitelist_uid
and proxy_config.whitelist_ukey
):
wl_platform = "xiequ"
wl_credentials = {"uid": proxy_config.whitelist_uid, "ukey": proxy_config.whitelist_ukey}
wl_credentials = {
"uid": proxy_config.whitelist_uid,
"ukey": proxy_config.whitelist_ukey,
}
self._shared_proxy_fetcher = ProxyFetcher(
api_url=proxy_config.api_url,
@@ -170,7 +187,11 @@ class LoginBatchRunner:
def _push_log(self, level: str, message: str):
# 即使没有页面实时日志,也要保留批次进度到 app.log,便于排查卡点。
if message:
log_level = level if level in {"debug", "info", "warning", "error", "success"} else "debug"
log_level = (
level
if level in {"debug", "info", "warning", "error", "success"}
else "debug"
)
getattr(logger, log_level)(f"[登录批次 {self.batch_id}] {message}")
if self.log_queue and self.loop:
asyncio.run_coroutine_threadsafe(
@@ -181,15 +202,15 @@ class LoginBatchRunner:
def _resolve_static_proxy(self) -> tuple[Optional[dict], str]:
"""解析静态代理配置。"""
if not self.proxy_config or not self.proxy_config.enabled:
return None, ''
return None, ""
# 静态代理
if self.proxy_config.http or self.proxy_config.https:
proxy_url = self.proxy_config.http or self.proxy_config.https
return {'http': proxy_url, 'https': proxy_url}, f'使用静态代理: {proxy_url}'
return {"http": proxy_url, "https": proxy_url}, f"使用静态代理: {proxy_url}"
# API代理:由 DouyuLogin 通过 proxy_fetcher 内部管理
return None, ''
return None, ""
def _execute_one(self, task_id: int, acc_info: dict, total: int):
"""在独立线程中执行单个账号登录,使用独立的 DB 会话。"""
@@ -216,8 +237,16 @@ class LoginBatchRunner:
self._completed += 1
current = self._completed
action_name = "检测" if self.mode == "check" else "重新登录" if self.mode == "relogin" else "登录"
self._push_log("info", f"[{current}/{total}] 开始{action_name}: {acc_info['username']}")
action_name = (
"检测"
if self.mode == "check"
else "重新登录"
if self.mode == "relogin"
else "登录"
)
self._push_log(
"info", f"[{current}/{total}] 开始{action_name}: {acc_info['username']}"
)
try:
# 代理配置也可能异常,必须由当前任务的失败处理收敛状态。
@@ -226,26 +255,39 @@ class LoginBatchRunner:
self._push_log("info", f"[{current}] {proxy_msg}")
# 静态代理启用但配置为空 → 不可用
if self.proxy_config and self.proxy_config.enabled and not (self.proxy_config.http or self.proxy_config.https) and not self._shared_proxy_fetcher and not proxy_dict:
if (
self.proxy_config
and self.proxy_config.enabled
and not (self.proxy_config.http or self.proxy_config.https)
and not self._shared_proxy_fetcher
and not proxy_dict
):
if self.mode == "relogin":
task.status = "relogin_failed"
task.message = "重新登录失败: 代理不可用: 未配置代理(旧 Cookie 已保留)"
task.message = (
"重新登录失败: 代理不可用: 未配置代理(旧 Cookie 已保留)"
)
else:
task.status = "error"
task.message = "代理不可用: 未配置代理"
task.finished_at = datetime.now(timezone.utc)
worker_db.commit()
self._push_log("error", f"[{current}] {acc_info['username']} 代理不可用")
self._push_log(
"error", f"[{current}] {acc_info['username']} 代理不可用"
)
return
account = SimpleNamespace(
username=acc_info["username"],
password=acc_info["password"],
email=acc_info["email"],
email_password=acc_info["email_password"],
email_imap_server=acc_info["email_imap_server"] or "",
email_imap_port=acc_info["email_imap_port"] or 993,
email_imap_ssl=acc_info["email_imap_ssl"],
account = cast(
AccountLike,
SimpleNamespace(
username=acc_info["username"],
password=acc_info["password"],
email=acc_info["email"],
email_password=acc_info["email_password"],
email_imap_server=acc_info["email_imap_server"] or "",
email_imap_port=acc_info["email_imap_port"] or 993,
email_imap_ssl=acc_info["email_imap_ssl"],
),
)
loginer = DouyuLogin(
@@ -257,22 +299,33 @@ class LoginBatchRunner:
stop_event=self._stop,
api_strategy=self.api_strategy,
)
result = loginer.check_account() if self.mode == "check" else loginer.login()
result = (
loginer.check_account() if self.mode == "check" else loginer.login()
)
if self.mode == "check" and result.success:
status = result.code if result.code in CHECK_STATUS_MESSAGES else "account_auth_unknown"
status = (
result.code
if result.code in CHECK_STATUS_MESSAGES
else "account_auth_unknown"
)
task.status = status
task.cookie = ""
task.message = result.message or CHECK_STATUS_MESSAGES[status]
level = CHECK_STATUS_LOG_LEVELS.get(status, "info")
self._push_log(level, f"[{current}] {acc_info['username']} 检测结果: {task.message}")
self._push_log(
level,
f"[{current}] {acc_info['username']} 检测结果: {task.message}",
)
elif result.success:
task.status = "success"
task.cookie = result.cookie
task.message = result.message or "登录成功"
if self.mode == "relogin":
check_result = check_douyu_cookie(result.cookie)
task.ck_check_status = "valid" if check_result["valid"] else "invalid"
task.ck_check_status = (
"valid" if check_result["valid"] else "invalid"
)
task.ck_check_result = {
"fish_ball": check_result["fish_ball"],
"nickname": check_result["nickname"],
@@ -282,32 +335,54 @@ class LoginBatchRunner:
task.ck_checked_at = check_result["checked_at"]
if check_result["valid"]:
task.message = "重新登录成功,Cookie 有效"
self._push_log("success", f"[{current}] {acc_info['username']} 重新登录成功,Cookie 已替换并验证有效")
self._push_log(
"success",
f"[{current}] {acc_info['username']} 重新登录成功,Cookie 已替换并验证有效",
)
else:
task.message = f"重新登录成功,但 Cookie 有效性检测失败: {check_result['message']}"
self._push_log("warning", f"[{current}] {acc_info['username']} 重新登录成功,但 Cookie 有效性检测失败: {check_result['message']}")
self._push_log(
"warning",
f"[{current}] {acc_info['username']} 重新登录成功,但 Cookie 有效性检测失败: {check_result['message']}",
)
else:
self._push_log("success", f"[{current}] {acc_info['username']} {task.message}")
self._push_log(
"success",
f"[{current}] {acc_info['username']} {task.message}",
)
else:
if self.mode == "relogin":
# 重新登录失败时保留旧 Cookie 与成功状态,仅记录失败原因,行不消失
task.status = "relogin_failed"
task.message = f"重新登录失败: {result.message}(旧 Cookie 已保留)"
self._push_log("error", f"[{current}] {acc_info['username']} 重新登录失败: {result.message}")
task.message = (
f"重新登录失败: {result.message}(旧 Cookie 已保留)"
)
self._push_log(
"error",
f"[{current}] {acc_info['username']} 重新登录失败: {result.message}",
)
else:
task.status = "failed"
task.message = result.message
self._push_log("error", f"[{current}] {acc_info['username']} {action_name}失败: {result.message}")
self._push_log(
"error",
f"[{current}] {acc_info['username']} {action_name}失败: {result.message}",
)
except Exception as e:
if self.mode == "relogin":
task.status = "relogin_failed"
task.message = f"重新登录异常: {e}(旧 Cookie 已保留)"
self._push_log("error", f"[{current}] {acc_info['username']} 重新登录异常: {e}")
self._push_log(
"error", f"[{current}] {acc_info['username']} 重新登录异常: {e}"
)
else:
task.status = "error"
task.message = str(e)
self._push_log("error", f"[{current}] {acc_info['username']} {action_name}异常: {e}")
self._push_log(
"error",
f"[{current}] {acc_info['username']} {action_name}异常: {e}",
)
task.finished_at = datetime.now(timezone.utc)
worker_db.commit()
@@ -319,8 +394,17 @@ class LoginBatchRunner:
"""在线程中执行批量登录。"""
batch_id = self.batch_id
concurrency = self.concurrency
action_name = "账号检测" if self.mode == "check" else "重新登录" if self.mode == "relogin" else "登录"
self._push_log("info", f"批量{action_name}任务 {batch_id} 开始,共 {len(self.account_ids or self.relogin_task_ids)} 个账号,并发数: {concurrency}")
action_name = (
"账号检测"
if self.mode == "check"
else "重新登录"
if self.mode == "relogin"
else "登录"
)
self._push_log(
"info",
f"批量{action_name}任务 {batch_id} 开始,共 {len(self.account_ids or self.relogin_task_ids)} 个账号,并发数: {concurrency}",
)
# 批次开始前同步一次出口 IP 到白名单,后续 fetch_new_proxy 不再主动同步
if self._shared_proxy_fetcher:
@@ -339,18 +423,22 @@ class LoginBatchRunner:
task.message = ""
task.finished_at = None
self.db.flush()
task_infos.append({
"task_id": task.id,
"acc_info": {
"username": acc.username,
"password": acc.password,
"email": acc.email,
"email_password": acc.email_password,
"email_imap_server": acc.email_imap_server or "",
"email_imap_port": acc.email_imap_port or 993,
"email_imap_ssl": acc.email_imap_ssl if acc.email_imap_ssl is not None else True,
},
})
task_infos.append(
{
"task_id": task.id,
"acc_info": {
"username": acc.username,
"password": acc.password,
"email": acc.email,
"email_password": acc.email_password,
"email_imap_server": acc.email_imap_server or "",
"email_imap_port": acc.email_imap_port or 993,
"email_imap_ssl": acc.email_imap_ssl
if acc.email_imap_ssl is not None
else True,
},
}
)
try:
# 创建或复用任务记录(顺序执行,线程安全)
@@ -358,10 +446,16 @@ class LoginBatchRunner:
if self.relogin_task_ids:
# 重新登录模式:复用指定 Cookie 记录,登录成功后原地替换 Cookie
for task_id in self.relogin_task_ids:
task = self.db.query(LoginTask).filter(LoginTask.id == task_id).first()
task = (
self.db.query(LoginTask).filter(LoginTask.id == task_id).first()
)
if not task:
continue
acc = self.db.query(AccountModel).filter(AccountModel.id == task.account_id).first()
acc = (
self.db.query(AccountModel)
.filter(AccountModel.id == task.account_id)
.first()
)
if not acc:
self._push_log("warning", f"跳过无账号的任务 #{task_id}")
continue
@@ -393,7 +487,9 @@ class LoginBatchRunner:
# 一个斗鱼账号只保留一条成功 CK:再次普通登录时更新最新成功记录。
latest_success_task = (
self.db.query(LoginTask)
.filter(LoginTask.account_id == aid, LoginTask.status == "success")
.filter(
LoginTask.account_id == aid, LoginTask.status == "success"
)
.order_by(LoginTask.finished_at.desc(), LoginTask.id.desc())
.first()
)
@@ -414,7 +510,10 @@ class LoginBatchRunner:
# 复用该账号最近一条失败任务记录,避免重复产生多条失败历史。
existing_task = (
self.db.query(LoginTask)
.filter(LoginTask.account_id == aid, LoginTask.status.in_(["failed", "error"]))
.filter(
LoginTask.account_id == aid,
LoginTask.status.in_(["failed", "error"]),
)
.order_by(LoginTask.id.desc())
.first()
)
@@ -479,9 +578,14 @@ class BatchRegistry:
def __init__(self):
self._batches: dict[str, dict] = {}
def register(self, batch_id: str, log_queue: Optional[asyncio.Queue],
loop: Optional[asyncio.AbstractEventLoop], runner: LoginBatchRunner,
owner_id: Optional[int] = None):
def register(
self,
batch_id: str,
log_queue: Optional[asyncio.Queue],
loop: Optional[asyncio.AbstractEventLoop],
runner: LoginBatchRunner,
owner_id: Optional[int] = None,
):
self._batches[batch_id] = {
"log_queue": log_queue,
"loop": loop,