diff --git a/backend/task_scheduler/app/main.py b/backend/task_scheduler/app/main.py index 2877534..b8d4dff 100644 --- a/backend/task_scheduler/app/main.py +++ b/backend/task_scheduler/app/main.py @@ -466,55 +466,103 @@ async def _async_do_signin(account_id: str, cron_expr: str = ""): async def _fetch_topics(cookies: dict) -> list: - """获取关注的超话列表。Cookie 失效时返回空列表。""" + """ + 获取关注的超话列表。 + 带重试机制:微博凌晨可能触发风控(302 到通行证验证页),重试 2 次。 + 返回空列表表示获取失败(Cookie 失效或风控)。 + """ import httpx - topics = [] - try: - async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client: - resp = await client.get("https://weibo.com/", headers=WEIBO_HEADERS, cookies=cookies) - # 检测是否被重定向到访客页(Cookie 失效) - if "passport.weibo.com" in str(resp.url) or "visitor" in str(resp.url).lower(): - logger.warning("Cookie 已失效,被重定向到登录/访客页") - return [] + max_retries = 3 + for attempt in range(max_retries): + topics = [] + try: + # 不自动跟随重定向,手动检测 302 + async with httpx.AsyncClient(timeout=15, follow_redirects=False) as client: + resp = await client.get("https://weibo.com/", headers=WEIBO_HEADERS, cookies=cookies) - xsrf = client.cookies.get("XSRF-TOKEN", "") - headers = {**WEIBO_HEADERS, "X-Requested-With": "XMLHttpRequest"} - if xsrf: - headers["X-XSRF-TOKEN"] = xsrf + # 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 [] + + # 其他 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 + + headers = {**WEIBO_HEADERS, "X-Requested-With": "XMLHttpRequest"} + if xsrf: + headers["X-XSRF-TOKEN"] = xsrf + + page = 1 + while page <= 10: + resp = await client.get( + "https://weibo.com/ajax/profile/topicContent", + params={"tabid": "231093_-_chaohua", "page": str(page)}, + headers=headers, cookies=cookies, + ) + + # 超话接口也可能被 302 + if resp.status_code in (301, 302): + 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 + else: + logger.warning("超话接口多次重定向,放弃") + return [] + + try: + data = resp.json() + except Exception: + logger.warning(f"超话列表响应非 JSON: {resp.text[:200]}") + break + if data.get("ok") != 1: + break + tlist = data.get("data", {}).get("list", []) + if not tlist: + break + for item in tlist: + title = item.get("topic_name", "") or item.get("title", "") + cid = "" + for field in ("oid", "scheme"): + m = re.search(r"100808[0-9a-fA-F]+", item.get(field, "")) + if m: + cid = m.group(0) + break + if title and cid: + topics.append({"title": title, "containerid": cid}) + if page >= data.get("data", {}).get("max_page", 1): + break + page += 1 + else: + # while 正常结束(没有 break),说明成功 + if topics: + return topics + + # 如果有 topics 说明成功了 + if topics: + return topics + + except Exception as e: + logger.error(f"获取超话列表失败(尝试 {attempt+1}/{max_retries}): {e}") + if attempt < max_retries - 1: + await asyncio.sleep(3) - page = 1 - while page <= 10: - resp = await client.get( - "https://weibo.com/ajax/profile/topicContent", - params={"tabid": "231093_-_chaohua", "page": str(page)}, - headers=headers, cookies=cookies, - ) - try: - data = resp.json() - except Exception: - logger.warning(f"超话列表响应非 JSON: {resp.text[:200]}") - break - if data.get("ok") != 1: - break - tlist = data.get("data", {}).get("list", []) - if not tlist: - break - for item in tlist: - title = item.get("topic_name", "") or item.get("title", "") - cid = "" - for field in ("oid", "scheme"): - m = re.search(r"100808[0-9a-fA-F]+", item.get(field, "")) - if m: - cid = m.group(0) - break - if title and cid: - topics.append({"title": title, "containerid": cid}) - if page >= data.get("data", {}).get("max_page", 1): - break - page += 1 - except Exception as e: - logger.error(f"获取超话列表失败: {e}") return topics diff --git a/frontend/Dockerfile b/frontend/Dockerfile index e8ab7a3..08ddc60 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -32,6 +32,6 @@ USER appuser EXPOSE 5000 HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ - CMD curl -f http://localhost:5000/ || exit 1 + CMD curl -f http://localhost:5000/health || exit 1 CMD ["python", "app.py"] diff --git a/frontend/app.py b/frontend/app.py index 32e0fac..4f6dac3 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -105,6 +105,10 @@ def index(): return redirect(url_for('dashboard')) return redirect(url_for('login')) +@app.route('/health') +def health(): + return 'ok', 200 + @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST':