diff --git a/backend/task_scheduler/app/main.py b/backend/task_scheduler/app/main.py index 39f2746..720115f 100644 --- a/backend/task_scheduler/app/main.py +++ b/backend/task_scheduler/app/main.py @@ -282,7 +282,7 @@ def _push_signin_result(account_id: str, result: dict, elapsed: float): elif status == "completed" and result.get("signed", 0) == 0 and result.get("total", 0) == 0: msg = result.get("message", "") if "no topics" in msg: - lines = [f"⚠️ {remark} Cookie 已失效,请重新扫码添加"] + lines = [f"⚠️ {remark} 获取超话失败,可能被风控或 Cookie 失效,请检查"] elif "no selected" in msg: lines = [f"⚠️ {remark} 签到跳过: 没有选中的超话"] else: @@ -388,14 +388,15 @@ async def _async_do_signin(account_id: str, cron_expr: str = ""): try: topics = await _fetch_topics(cookies) if not topics: + # 获取超话失败,可能是风控或 Cookie 失效 + # 不立即标记 invalid_cookie,等用户手动验证确认 async with SessionFactory() as session: result = await session.execute(select(Account).where(Account.id == acc_id)) acc = result.scalar_one_or_none() if acc: - acc.status = "invalid_cookie" acc.last_checked_at = datetime.now() await session.commit() - return {"status": "completed", "signed": 0, "message": "no topics - cookie可能已失效"} + return {"status": "completed", "signed": 0, "message": "no topics - 获取超话失败"} # 如果用户选择了特定超话,只签选中的 if acc_selected_topics and isinstance(acc_selected_topics, list): @@ -477,7 +478,7 @@ async def _async_do_signin(account_id: str, cron_expr: str = ""): async def _fetch_topics(cookies: dict) -> list: """ 获取关注的超话列表。 - 带重试机制:微博凌晨可能触发风控(302 到通行证验证页),重试 2 次。 + 带重试机制:微博可能触发风控,重试 3 次。 返回空列表表示获取失败(Cookie 失效或风控)。 """ import httpx @@ -486,32 +487,34 @@ async def _fetch_topics(cookies: dict) -> list: for attempt in range(max_retries): topics = [] try: - # 不自动跟随重定向,手动检测 302 - async with httpx.AsyncClient(timeout=15, follow_redirects=False) as client: + async with httpx.AsyncClient(timeout=20, follow_redirects=True) as client: resp = await client.get("https://weibo.com/", headers=WEIBO_HEADERS, cookies=cookies) + final_url = str(resp.url) - # 302 重定向到登录页 = Cookie 失效或风控 - if resp.status_code in (301, 302): - location = resp.headers.get("location", "") - if "login.sina.com.cn" in location or "passport" in location: - if attempt < max_retries - 1: - wait = (attempt + 1) * 3 - logger.warning(f"被重定向到登录页(尝试 {attempt+1}/{max_retries}),{wait}秒后重试...") - await asyncio.sleep(wait) - continue - else: - logger.warning(f"多次重试仍被重定向,Cookie 可能已失效") - return [] + # 最终落地在登录页 = Cookie 失效或风控 + if "login.sina.com.cn" in final_url or "passport.weibo.com" in final_url: + if attempt < max_retries - 1: + wait = (attempt + 1) * 3 + logger.warning(f"被重定向到登录页(尝试 {attempt+1}/{max_retries}),{wait}秒后重试... url={final_url[:120]}") + await asyncio.sleep(wait) + continue + else: + logger.warning(f"多次重试仍被重定向到登录页,Cookie 已失效: {final_url[:120]}") + return [] - # 其他 302(如 weibo.com → www.weibo.com),跟随一次 - resp = await client.get(location, headers=WEIBO_HEADERS, cookies=cookies) - - xsrf = "" - for c in resp.cookies.jar: - if c.name == "XSRF-TOKEN": - xsrf = c.value - break + # 检查响应内容是否是通行证验证页(HTML 而非正常页面) + content_type = resp.headers.get("content-type", "") + if "text/html" in content_type and "通行证" in resp.text[:500]: + if attempt < max_retries - 1: + wait = (attempt + 1) * 3 + logger.warning(f"触发通行证验证(尝试 {attempt+1}/{max_retries}),{wait}秒后重试...") + await asyncio.sleep(wait) + continue + else: + logger.warning("多次重试仍触发通行证验证") + return [] + xsrf = client.cookies.get("XSRF-TOKEN", "") headers = {**WEIBO_HEADERS, "X-Requested-With": "XMLHttpRequest"} if xsrf: headers["X-XSRF-TOKEN"] = xsrf @@ -524,22 +527,25 @@ async def _fetch_topics(cookies: dict) -> list: headers=headers, cookies=cookies, ) - # 超话接口也可能被 302 - if resp.status_code in (301, 302): + # 超话接口被重定向到登录页 + final_url = str(resp.url) + if "login.sina.com.cn" in final_url or "passport" in final_url: if attempt < max_retries - 1: - wait = (attempt + 1) * 3 - logger.warning(f"超话接口被重定向(尝试 {attempt+1}/{max_retries}),{wait}秒后重试...") - await asyncio.sleep(wait) - break # break inner loop, retry outer + logger.warning(f"超话接口被重定向(尝试 {attempt+1}/{max_retries}),重试...") + await asyncio.sleep((attempt + 1) * 3) + break else: - logger.warning("超话接口多次重定向,放弃") return [] try: data = resp.json() except Exception: - logger.warning(f"超话列表响应非 JSON: {resp.text[:200]}") - break + logger.warning(f"超话列表响应非 JSON(尝试 {attempt+1}): {resp.text[:200]}") + if attempt < max_retries - 1: + await asyncio.sleep((attempt + 1) * 3) + break + return [] + if data.get("ok") != 1: break tlist = data.get("data", {}).get("list", []) @@ -559,11 +565,9 @@ async def _fetch_topics(cookies: dict) -> list: break page += 1 else: - # while 正常结束(没有 break),说明成功 if topics: return topics - # 如果有 topics 说明成功了 if topics: return topics