type: 收敛测试 schemas 与协议层类型
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user