稳定性: 签到失败10分钟自动重试+前端显示Cookie剩余天数+每天9点Cookie过期预警
This commit is contained in:
@@ -148,7 +148,26 @@ async def get_account(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
account = await _get_owned_account(account_id, user, 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 ----
|
# ---- UPDATE ----
|
||||||
|
|||||||
@@ -254,16 +254,58 @@ def run_signin(task_id: str, account_id: str, cron_expr: str = ""):
|
|||||||
elapsed = _time.time() - start
|
elapsed = _time.time() - start
|
||||||
result["elapsed_seconds"] = round(elapsed, 1)
|
result["elapsed_seconds"] = round(elapsed, 1)
|
||||||
logger.info(f"✅ 签到完成: task={task_id}, 耗时={elapsed:.1f}s, result={result}")
|
logger.info(f"✅ 签到完成: task={task_id}, 耗时={elapsed:.1f}s, result={result}")
|
||||||
# 签到完成后立即推送通知
|
|
||||||
_push_signin_result(account_id, result, elapsed)
|
_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:
|
except asyncio.TimeoutError:
|
||||||
elapsed = _time.time() - start
|
elapsed = _time.time() - start
|
||||||
logger.error(f"⏰ 签到超时(5分钟): task={task_id}, account={account_id}")
|
logger.error(f"⏰ 签到超时(5分钟): task={task_id}, account={account_id}")
|
||||||
_push_signin_result(account_id, {"status": "timeout"}, elapsed)
|
_push_signin_result(account_id, {"status": "timeout"}, elapsed)
|
||||||
|
_schedule_retry(task_id, account_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
elapsed = _time.time() - start
|
elapsed = _time.time() - start
|
||||||
logger.error(f"❌ 签到失败: task={task_id}, error={e}")
|
logger.error(f"❌ 签到失败: task={task_id}, error={e}")
|
||||||
_push_signin_result(account_id, {"status": "error", "reason": str(e)}, elapsed)
|
_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):
|
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]}")
|
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():
|
def cleanup_old_signin_logs():
|
||||||
@@ -1030,6 +1133,15 @@ if __name__ == "__main__":
|
|||||||
misfire_grace_time=3600,
|
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 已调度则跳过)
|
# 每天定时推送签到日报到 Webhook(如果 load_config_from_db 已调度则跳过)
|
||||||
if WEBHOOK_URL and not scheduler.get_job("daily_report"):
|
if WEBHOOK_URL and not scheduler.get_job("daily_report"):
|
||||||
scheduler.add_job(
|
scheduler.add_job(
|
||||||
|
|||||||
@@ -74,6 +74,22 @@
|
|||||||
<tr><td>备注</td><td>{{ account.remark or '-' }}</td></tr>
|
<tr><td>备注</td><td>{{ account.remark or '-' }}</td></tr>
|
||||||
<tr><td>添加时间</td><td>{{ account.created_at[:10] }}</td></tr>
|
<tr><td>添加时间</td><td>{{ account.created_at[:10] }}</td></tr>
|
||||||
<tr><td>上次检查</td><td>{{ account.last_checked_at[:10] if account.last_checked_at else '-' }}</td></tr>
|
<tr><td>上次检查</td><td>{{ account.last_checked_at[:10] if account.last_checked_at else '-' }}</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>Cookie 有效期</td>
|
||||||
|
<td>
|
||||||
|
{% if account.cookie_expire_days is defined and account.cookie_expire_days is not none %}
|
||||||
|
{% if account.cookie_expire_days <= 3 %}
|
||||||
|
<span class="badge badge-danger">{{ account.cookie_expire_date }} (剩 {{ account.cookie_expire_days }} 天)</span>
|
||||||
|
{% elif account.cookie_expire_days <= 7 %}
|
||||||
|
<span class="badge badge-warning">{{ account.cookie_expire_date }} (剩 {{ account.cookie_expire_days }} 天)</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-success">{{ account.cookie_expire_date }} (剩 {{ account.cookie_expire_days }} 天)</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span style="color:#94a3b8;">未知</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Reference in New Issue
Block a user