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
+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,