From ecb1738d7b5d408400a5ea6b4e1ed5b442b8e960 Mon Sep 17 00:00:00 2001 From: Jeason <1710884619@qq.com> Date: Fri, 17 Apr 2026 10:18:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A8=B3=E5=AE=9A=E6=80=A7:=20=E7=AD=BE?= =?UTF-8?q?=E5=88=B0=E5=A4=B1=E8=B4=A510=E5=88=86=E9=92=9F=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=87=8D=E8=AF=95+=E5=89=8D=E7=AB=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BACookie=E5=89=A9=E4=BD=99=E5=A4=A9=E6=95=B0+=E6=AF=8F?= =?UTF-8?q?=E5=A4=A99=E7=82=B9Cookie=E8=BF=87=E6=9C=9F=E9=A2=84=E8=AD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api_service/app/routers/accounts.py | 21 +++- backend/task_scheduler/app/main.py | 114 +++++++++++++++++++- frontend/templates/account_detail.html | 16 +++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/backend/api_service/app/routers/accounts.py b/backend/api_service/app/routers/accounts.py index bcacb18..d6e68b5 100644 --- a/backend/api_service/app/routers/accounts.py +++ b/backend/api_service/app/routers/accounts.py @@ -148,7 +148,26 @@ async def get_account( db: AsyncSession = Depends(get_db), ): account = await _get_owned_account(account_id, user, db) - return success_response(_account_to_dict(account), "Account retrieved") + data = _account_to_dict(account) + + # 解析 Cookie 中的 ALF 字段获取过期时间 + try: + key = _encryption_key() + cookie_str = decrypt_cookie(account.encrypted_cookies, account.iv, key) + for pair in cookie_str.split(";"): + pair = pair.strip() + if pair.startswith("ALF="): + alf = pair.split("=", 1)[1].strip() + if alf.isdigit(): + from datetime import datetime as _dt + expire_dt = _dt.fromtimestamp(int(alf)) + data["cookie_expire_date"] = expire_dt.strftime("%Y-%m-%d") + data["cookie_expire_days"] = (expire_dt - _dt.now()).days + break + except Exception: + pass + + return success_response(data, "Account retrieved") # ---- UPDATE ---- diff --git a/backend/task_scheduler/app/main.py b/backend/task_scheduler/app/main.py index 02afb34..1fc69b0 100644 --- a/backend/task_scheduler/app/main.py +++ b/backend/task_scheduler/app/main.py @@ -254,16 +254,58 @@ def run_signin(task_id: str, account_id: str, cron_expr: str = ""): elapsed = _time.time() - start result["elapsed_seconds"] = round(elapsed, 1) logger.info(f"✅ 签到完成: task={task_id}, 耗时={elapsed:.1f}s, result={result}") - # 签到完成后立即推送通知 _push_signin_result(account_id, result, elapsed) + + # 签到失败(获取超话失败)时,10 分钟后自动重试一次 + signed = result.get("signed", 0) + total = result.get("total", 0) + msg = result.get("message", "") + if signed == 0 and total == 0 and "no topics" in msg: + _schedule_retry(task_id, account_id) + except asyncio.TimeoutError: elapsed = _time.time() - start logger.error(f"⏰ 签到超时(5分钟): task={task_id}, account={account_id}") _push_signin_result(account_id, {"status": "timeout"}, elapsed) + _schedule_retry(task_id, account_id) except Exception as e: elapsed = _time.time() - start logger.error(f"❌ 签到失败: task={task_id}, error={e}") _push_signin_result(account_id, {"status": "error", "reason": str(e)}, elapsed) + _schedule_retry(task_id, account_id) + + +def _schedule_retry(task_id: str, account_id: str): + """10 分钟后自动重试一次签到。""" + retry_id = f"retry_{task_id}_{int(_time.time())}" + try: + scheduler.add_job( + _run_retry, + trigger="date", + run_date=datetime.now() + timedelta(minutes=10), + id=retry_id, + args=[task_id, account_id], + misfire_grace_time=300, + ) + logger.info(f"🔄 已安排 10 分钟后重试: task={task_id}") + except Exception as e: + logger.warning(f"安排重试失败: {e}") + + +def _run_retry(task_id: str, account_id: str): + """重试签到(不再递归重试)。""" + logger.info(f"🔄 重试签到: task={task_id}, account={account_id}") + start = _time.time() + try: + result = _run_async(asyncio.wait_for(_async_do_signin(account_id, ""), timeout=300)) + elapsed = _time.time() - start + result["elapsed_seconds"] = round(elapsed, 1) + logger.info(f"🔄 重试完成: task={task_id}, 耗时={elapsed:.1f}s, result={result}") + _push_signin_result(account_id, result, elapsed) + except Exception as e: + elapsed = _time.time() - start + logger.error(f"🔄 重试失败: task={task_id}, error={e}") + _push_signin_result(account_id, {"status": "error", "reason": f"重试失败: {e}"}, elapsed) def _push_signin_result(account_id: str, result: dict, elapsed: float): @@ -891,6 +933,67 @@ def _send_webhook(content: str): logger.warning(f"Webhook 业务异常: {resp.text[:300]}") +# =============== Cookie 过期预警 =============== + +def check_cookie_expiry(): + """检查所有账号的 Cookie 过期时间,剩余 ≤5 天时推送提醒。""" + if not WEBHOOK_URL: + return + try: + warnings = _run_async(_check_cookie_expiry_async()) + if warnings: + lines = ["🍪 Cookie 过期预警", ""] + for w in warnings: + lines.append(f" 🔴 {w['name']}: {w['expire']} (剩 {w['remain']} 天)") + lines.append("") + lines.append("请尽快重新扫码续期") + _send_webhook("\n".join(lines)) + logger.info(f"🍪 Cookie 过期预警已推送: {len(warnings)} 个账号") + except Exception as e: + logger.error(f"Cookie 过期检查失败: {e}") + + +async def _check_cookie_expiry_async() -> list: + from sqlalchemy import select + from shared.models.account import Account + from shared.crypto import decrypt_cookie, derive_key + + SessionFactory, eng = _make_session() + warnings = [] + try: + async with SessionFactory() as session: + result = await session.execute( + select(Account.remark, Account.weibo_user_id, + Account.encrypted_cookies, Account.iv) + .where(Account.status.in_(["active", "pending"])) + ) + key = derive_key(shared_settings.COOKIE_ENCRYPTION_KEY) + now = datetime.now() + for remark, uid, enc, iv in result.all(): + name = remark or uid + try: + cookie_str = decrypt_cookie(enc, iv, key) + for pair in cookie_str.split(";"): + pair = pair.strip() + if pair.startswith("ALF="): + alf = pair.split("=", 1)[1].strip() + if alf.isdigit(): + expire_dt = datetime.fromtimestamp(int(alf)) + remain = (expire_dt - now).days + if remain <= 5: + warnings.append({ + "name": name, + "expire": expire_dt.strftime("%m-%d"), + "remain": remain, + }) + break + except Exception: + pass + finally: + await eng.dispose() + return warnings + + # =============== 日志清理 =============== def cleanup_old_signin_logs(): @@ -1030,6 +1133,15 @@ if __name__ == "__main__": misfire_grace_time=3600, ) + # 每天早上 9 点检查 Cookie 过期预警 + scheduler.add_job( + check_cookie_expiry, + trigger=CronTrigger(hour=9, minute=0, timezone="Asia/Shanghai"), + id="check_cookie_expiry", + replace_existing=True, + misfire_grace_time=3600, + ) + # 每天定时推送签到日报到 Webhook(如果 load_config_from_db 已调度则跳过) if WEBHOOK_URL and not scheduler.get_job("daily_report"): scheduler.add_job( diff --git a/frontend/templates/account_detail.html b/frontend/templates/account_detail.html index b3e2f7a..3e3d41a 100644 --- a/frontend/templates/account_detail.html +++ b/frontend/templates/account_detail.html @@ -74,6 +74,22 @@