feat: 登录流程整体重试机制 + 代理池耗尽等待恢复
- DouyuLogin.login() 添加整体重试循环,失败时换新代理从头重跑 - 新增 max_login_retries(默认3次)和 max_total_time(默认300秒) - 重试前重置 session cookies + 换新代理,避免残留状态 - 成功后归还代理到池;失败时标记代理坏 - 代理池耗尽时不再立即放弃账号,等待最多60秒恢复后再试 - API代理模式改为 DouyuLogin 内部通过 proxy_manager 自治管理 登录过程中的代理切换(极验、整体重试)不再由服务层预取 - Schema/LoginBatchRequest 添加 max_login_retries 和 max_total_time - 前端 API 调用改为对象参数形式,透传新参数
This commit is contained in:
+72
-33
@@ -73,6 +73,8 @@ class DouyuLogin:
|
||||
timeout: tuple[float, float] = REQUEST_TIMEOUT,
|
||||
max_geetest_retries: int = 5,
|
||||
max_proxy_retries: int = 10,
|
||||
max_login_retries: int = 3,
|
||||
max_total_time: float = 300,
|
||||
whitelist_uid: str = "",
|
||||
whitelist_ukey: str = "",
|
||||
proxy_manager: Optional[ProxyManager] = None,
|
||||
@@ -82,6 +84,8 @@ class DouyuLogin:
|
||||
self.timeout = timeout
|
||||
self.max_geetest_retries = max_geetest_retries
|
||||
self.max_proxy_retries = max_proxy_retries # 0=无限切换直到成功
|
||||
self.max_login_retries = max_login_retries # 登录整体重试次数(换代理从头重跑)
|
||||
self.max_total_time = max_total_time # 单账号登录总时长上限(秒),超时则放弃
|
||||
self.session = requests.Session()
|
||||
|
||||
# 初始化代理管理器(优先使用外部传入的共享实例,避免并发刷新冲突)
|
||||
@@ -314,53 +318,88 @@ class DouyuLogin:
|
||||
|
||||
def login(self) -> LoginResult:
|
||||
"""
|
||||
完整登录流程
|
||||
完整登录流程(带整体重试)。
|
||||
|
||||
任何步骤失败时,换新代理从头重跑,最多重试 max_login_retries 次。
|
||||
整体超时 max_total_time 秒后放弃。
|
||||
|
||||
Returns:
|
||||
LoginResult: 登录结果,包含cookie
|
||||
"""
|
||||
logger.info(f"开始登录账号: {self.account.username}")
|
||||
start_time = time.monotonic()
|
||||
|
||||
try:
|
||||
# 1️⃣ 第一次登录(获取极验参数)
|
||||
logger.info("步骤1: 第一次登录,获取极验参数...")
|
||||
gt, challenge, code_token, initial_cookies = self._first_login()
|
||||
for attempt in range(1, self.max_login_retries + 1):
|
||||
elapsed = time.monotonic() - start_time
|
||||
if elapsed > self.max_total_time:
|
||||
logger.warning(f"登录总耗时 {elapsed:.0f}s 超过上限 {self.max_total_time}s,放弃")
|
||||
return LoginResult(success=False, message=f"登录超时({elapsed:.0f}s > {self.max_total_time}s)")
|
||||
|
||||
# 2️⃣ 极验 fullpage 验证
|
||||
logger.info("步骤2: 极验 fullpage 验证...")
|
||||
validate, seccode = self._solve_geetest(gt, challenge)
|
||||
if attempt > 1:
|
||||
logger.info(f"登录整体重试 {attempt}/{self.max_login_retries},换代理重新开始")
|
||||
# 重试前:换新代理 + 重置 session(清 cookies)
|
||||
self._prepare_retry()
|
||||
|
||||
# 3️⃣ 第二次登录(带极验)
|
||||
logger.info("步骤3: 第二次登录(带极验验证)...")
|
||||
remote_code = self._second_login(gt, challenge, validate, seccode, code_token)
|
||||
try:
|
||||
# 1️⃣ 第一次登录(获取极验参数)
|
||||
logger.info("步骤1: 第一次登录,获取极验参数...")
|
||||
gt, challenge, code_token, initial_cookies = self._first_login()
|
||||
|
||||
# 4️⃣ 发送邮箱验证
|
||||
logger.info("步骤4: 发送邮箱验证...")
|
||||
email_sent_at = time.time()
|
||||
self._send_email_verify(remote_code)
|
||||
# 2️⃣ 极验 fullpage 验证
|
||||
logger.info("步骤2: 极验 fullpage 验证...")
|
||||
validate, seccode = self._solve_geetest(gt, challenge)
|
||||
|
||||
# 5️⃣ IMAP获取验证码
|
||||
logger.info("步骤5: 获取邮箱验证码...")
|
||||
verify_code = self._get_email_code(after_timestamp=email_sent_at)
|
||||
# 3️⃣ 第二次登录(带极验)
|
||||
logger.info("步骤3: 第二次登录(带极验验证)...")
|
||||
remote_code = self._second_login(gt, challenge, validate, seccode, code_token)
|
||||
|
||||
# 6️⃣ 提交验证码
|
||||
logger.info("步骤6: 提交验证码...")
|
||||
login_url = self._submit_verify_code(remote_code, verify_code)
|
||||
# 4️⃣ 发送邮箱验证
|
||||
logger.info("步骤4: 发送邮箱验证...")
|
||||
email_sent_at = time.time()
|
||||
self._send_email_verify(remote_code)
|
||||
|
||||
# 7️⃣ 完成登录获取Cookie
|
||||
logger.info("步骤7: 完成登录,获取Cookie...")
|
||||
cookie = self._complete_login(login_url)
|
||||
# 5️⃣ IMAP获取验证码
|
||||
logger.info("步骤5: 获取邮箱验证码...")
|
||||
verify_code = self._get_email_code(after_timestamp=email_sent_at)
|
||||
|
||||
logger.success(f"登录成功! Cookie长度: {len(cookie)}")
|
||||
return LoginResult(success=True, cookie=cookie, message="登录成功")
|
||||
# 6️⃣ 提交验证码
|
||||
logger.info("步骤6: 提交验证码...")
|
||||
login_url = self._submit_verify_code(remote_code, verify_code)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"登录失败: {e}")
|
||||
return LoginResult(success=False, message=str(e))
|
||||
finally:
|
||||
# 归还代理到池,让其他账号可以复用
|
||||
if self.proxy_manager and self._current_proxy_url:
|
||||
self.proxy_manager.release_proxy(self._current_proxy_url)
|
||||
# 7️⃣ 完成登录获取Cookie
|
||||
logger.info("步骤7: 完成登录,获取Cookie...")
|
||||
cookie = self._complete_login(login_url)
|
||||
|
||||
logger.success(f"登录成功! Cookie长度: {len(cookie)}")
|
||||
# 成功后归还代理到池,让其他账号复用
|
||||
if self.proxy_manager and self._current_proxy_url:
|
||||
self.proxy_manager.release_proxy(self._current_proxy_url)
|
||||
return LoginResult(success=True, cookie=cookie, message="登录成功")
|
||||
|
||||
except Exception as e:
|
||||
elapsed = time.monotonic() - start_time
|
||||
logger.error(f"登录失败(尝试 {attempt}/{self.max_login_retries},已耗时 {elapsed:.0f}s): {e}")
|
||||
if attempt < self.max_login_retries and elapsed < self.max_total_time:
|
||||
# 还有重试机会且未超时:标记当前代理坏,下一轮自动换新代理
|
||||
if self.proxy_manager and self._current_proxy_url:
|
||||
self.proxy_manager.mark_bad(self._current_proxy_url)
|
||||
time.sleep(2)
|
||||
continue
|
||||
# 所有重试耗尽或超时
|
||||
return LoginResult(success=False, message=str(e))
|
||||
|
||||
return LoginResult(success=False, message=f"登录失败,已重试 {self.max_login_retries} 次")
|
||||
|
||||
def _prepare_retry(self) -> None:
|
||||
"""重试前准备:换新代理、重置 session cookies。"""
|
||||
# 重置 session(清掉旧 cookies,避免残留状态干扰)
|
||||
self.session.cookies.clear()
|
||||
# 刷新代理
|
||||
if self.proxy_manager:
|
||||
new_proxy = self.proxy_manager.get_proxy()
|
||||
if new_proxy:
|
||||
self._apply_proxy(new_proxy)
|
||||
# 如果获取新代理失败,保留旧代理继续尝试
|
||||
|
||||
def _first_login(self) -> Tuple[str, str, str, dict]:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user