cleanup.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. """Clean expired registration tokens from backend auth storage."""
  2. from __future__ import annotations
  3. import argparse
  4. import time
  5. from datetime import datetime, timezone
  6. from pathlib import Path
  7. from curl_cffi import requests
  8. from .common import CpaClient, DEFAULT_MANAGEMENT_BASE_URL, DEFAULT_POOL_DIR, now
  9. def is_expired(data: dict) -> bool:
  10. expired_str = str(data.get("expired") or "").strip()
  11. if not expired_str:
  12. return False
  13. try:
  14. expired_at = datetime.fromisoformat(expired_str.replace("Z", "+00:00"))
  15. except ValueError:
  16. return False
  17. return expired_at < datetime.now(timezone.utc)
  18. def try_refresh(refresh_token: str, proxy: str | None) -> bool:
  19. try:
  20. proxies = {"http": proxy, "https": proxy} if proxy else None
  21. response = requests.post(
  22. "https://auth0.openai.com/oauth/token",
  23. json={
  24. "redirect_uri": "com.openai.chat://auth0.openai.com/ios/com.openai.chat/callback",
  25. "grant_type": "refresh_token",
  26. "client_id": "app_EMoamEEZ73f0CkXaXp7hrann",
  27. "refresh_token": refresh_token,
  28. },
  29. proxies=proxies,
  30. impersonate="chrome",
  31. timeout=15,
  32. )
  33. return response.status_code == 200 and bool(response.json().get("access_token"))
  34. except Exception:
  35. return False
  36. def _hard_delete_pool_file(pool_dir: Path, name: str, reason: str) -> None:
  37. pool_file = pool_dir / name
  38. if not pool_file.exists():
  39. return
  40. pool_file.unlink(missing_ok=True)
  41. print(f"[{now()}] [清理] ❌ pool {name} deleted ({reason})")
  42. def cleanup_once(
  43. proxy: str | None = None,
  44. pool_dir: Path = DEFAULT_POOL_DIR,
  45. *,
  46. client: object | None = None,
  47. management_base_url: str = DEFAULT_MANAGEMENT_BASE_URL,
  48. management_key: str | None = None,
  49. ) -> tuple[int, int, int]:
  50. backend_client = client or CpaClient(management_base_url, management_key=management_key)
  51. if not getattr(backend_client, "health_check")():
  52. return 0, 0, 0
  53. reg_files = sorted(
  54. str(entry.get("name") or "").strip()
  55. for entry in getattr(backend_client, "list_auth_files")()
  56. if "@" in str(entry.get("name") or "").strip()
  57. )
  58. checked = 0
  59. deleted = 0
  60. refreshed = 0
  61. for name in reg_files:
  62. checked += 1
  63. data = getattr(backend_client, "get_auth_file")(name)
  64. if not isinstance(data, dict):
  65. continue
  66. refresh_token = str(data.get("refresh_token") or "").strip()
  67. if not refresh_token:
  68. print(f"[{now()}] [清理] ⚠️ {name} 无 refresh_token, 删除")
  69. if getattr(backend_client, "delete_auth_file")(name):
  70. deleted += 1
  71. _hard_delete_pool_file(pool_dir, name, "no_refresh_token")
  72. continue
  73. if not is_expired(data):
  74. continue
  75. if try_refresh(refresh_token, proxy):
  76. refreshed += 1
  77. print(f"[{now()}] [清理] 🔄 {name} 已过期但刷新成功, 保留")
  78. continue
  79. print(f"[{now()}] [清理] ❌ {name} 已过期且刷新失败, 删除")
  80. if getattr(backend_client, "delete_auth_file")(name):
  81. deleted += 1
  82. _hard_delete_pool_file(pool_dir, name, "expired_refresh_failed")
  83. return checked, deleted, refreshed
  84. def main() -> None:
  85. from core.settings import AppSettings
  86. env_settings = AppSettings.from_env()
  87. parser = argparse.ArgumentParser(description="清理 zhuce6 backend 中失效 token")
  88. parser.add_argument("--interval", type=int, default=300, help="清理间隔秒数")
  89. parser.add_argument("--proxy", default=None, help="可选代理地址")
  90. parser.add_argument("--once", action="store_true", help="只执行一轮")
  91. parser.add_argument("--management-base-url", default=env_settings.cpa_management_base_url or DEFAULT_MANAGEMENT_BASE_URL, help="CPA management base url")
  92. parser.add_argument("--management-key", default=env_settings.cpa_management_key, help="可选 CPA management key")
  93. parser.add_argument("--pool-dir", default=str(env_settings.pool_dir or DEFAULT_POOL_DIR), help="本地 pool 目录")
  94. args = parser.parse_args()
  95. interval = max(1, args.interval)
  96. proxy = str(args.proxy or "").strip() or None
  97. pool_dir = Path(args.pool_dir).expanduser().resolve()
  98. print(
  99. "[清理] 启动"
  100. f" | management_base_url: {args.management_base_url}"
  101. f" | 间隔: {interval}s"
  102. f" | proxy: {proxy or 'none'}"
  103. )
  104. while True:
  105. cycle_started_at = time.time()
  106. try:
  107. checked, deleted, refreshed = cleanup_once(
  108. proxy,
  109. pool_dir,
  110. management_base_url=args.management_base_url,
  111. management_key=str(args.management_key or "").strip() or None,
  112. )
  113. elapsed = time.time() - cycle_started_at
  114. if checked > 0 or deleted > 0 or refreshed > 0:
  115. print(f"[{now()}] [清理] 本轮: 检查 {checked}, 删除 {deleted}, 刷新验证 {refreshed}")
  116. print(f"[{now()}] [清理] 本轮耗时: {elapsed:.2f}s")
  117. except Exception as exc:
  118. print(f"[{now()}] [错误] 清理异常: {exc}")
  119. elapsed = time.time() - cycle_started_at
  120. if args.once:
  121. break
  122. time.sleep(max(0, interval - elapsed))
  123. if __name__ == "__main__":
  124. main()