fix(chatgpt): 为远端状态同步增加重试机制

This commit is contained in:
小易先生
2026-03-31 16:40:53 +08:00
parent ed178a07f5
commit ef25112f12
2 changed files with 115 additions and 2 deletions

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import base64
import json
import time
from datetime import datetime, timezone
from typing import Any, Optional
@@ -11,6 +12,8 @@ from platforms.chatgpt.status_probe import CODEX_USER_AGENT, extract_chatgpt_acc
from services.chatgpt_account_state import is_account_deactivated_message
DEFAULT_CLIPROXYAPI_BASE_URL = "http://127.0.0.1:8317"
SYNC_RETRY_ATTEMPTS = 3
SYNC_RETRY_DELAY_SECONDS = 0.4
def _utcnow_iso() -> str:
@@ -124,6 +127,35 @@ def _request_json(method: str, path: str, *, api_url: str | None = None, api_key
return response.text
def _is_retryable_sync_error(exc: Exception) -> bool:
text = str(exc or "").strip().lower()
if not text:
return False
markers = (
"无法连接",
"请求超时",
"connection",
"timeout",
"timed out",
)
return any(marker in text for marker in markers)
def _retry_sync_call(func, *, attempts: int = SYNC_RETRY_ATTEMPTS):
last_error = None
for attempt in range(1, max(1, attempts) + 1):
try:
return func()
except Exception as exc:
last_error = exc
if attempt >= attempts or not _is_retryable_sync_error(exc):
raise
time.sleep(SYNC_RETRY_DELAY_SECONDS)
if last_error is not None:
raise last_error
raise RuntimeError("sync retry failed without captured error")
def list_auth_files(*, api_url: str | None = None, api_key: str | None = None) -> list[dict[str, Any]]:
data = _request_json("GET", "/v0/management/auth-files", api_url=api_url, api_key=api_key)
files = data.get("files", []) if isinstance(data, dict) else []
@@ -240,7 +272,7 @@ def sync_chatgpt_cliproxyapi_status(
) -> dict[str, Any]:
synced_at = _utcnow_iso()
try:
files = list_auth_files(api_url=api_url, api_key=api_key)
files = _retry_sync_call(lambda: list_auth_files(api_url=api_url, api_key=api_key))
except Exception as exc:
return {
"uploaded": False,
@@ -279,7 +311,11 @@ def sync_chatgpt_cliproxyapi_status(
"chatgpt_subscription_active_until": str(((matched.get("id_token") or {}).get("chatgpt_subscription_active_until") if isinstance(matched.get("id_token"), dict) else "") or "").strip(),
}
try:
remote.update(_probe_remote_auth(remote["auth_index"], account_id, api_url=api_url, api_key=api_key))
remote.update(
_retry_sync_call(
lambda: _probe_remote_auth(remote["auth_index"], account_id, api_url=api_url, api_key=api_key)
)
)
except Exception as exc:
remote.update(
{

View File

@@ -25,6 +25,46 @@ class CliproxyapiSyncTests(unittest.TestCase):
self.assertEqual(result["remote_state"], "unreachable")
self.assertIn("无法连接", result["message"])
def test_sync_retries_list_auth_files_until_success(self):
account = DummyAccount(email="demo@example.com", user_id="acct-123")
auth_files = [
{
"name": "demo@example.com.json",
"provider": "codex",
"email": "demo@example.com",
"auth_index": "auth-001",
"status": "active",
"status_message": "",
"unavailable": False,
}
]
with mock.patch(
"services.cliproxyapi_sync.list_auth_files",
side_effect=[
RuntimeError("CLIProxyAPI 无法连接,请确认服务已启动或 API URL 是否正确http://127.0.0.1:8317"),
RuntimeError("CLIProxyAPI 请求超时http://127.0.0.1:8317"),
auth_files,
],
) as list_mock:
with mock.patch(
"services.cliproxyapi_sync._probe_remote_auth",
return_value={
"last_probe_at": "2026-03-31T00:00:00Z",
"last_probe_status_code": 200,
"last_probe_error_code": "",
"last_probe_message": "ok",
"remote_state": "usable",
},
):
with mock.patch("services.cliproxyapi_sync.time.sleep") as sleep_mock:
result = sync_chatgpt_cliproxyapi_status(account, api_url="http://127.0.0.1:8317", api_key="demo")
self.assertTrue(result["uploaded"])
self.assertEqual(result["remote_state"], "usable")
self.assertEqual(list_mock.call_count, 3)
self.assertEqual(sleep_mock.call_count, 2)
def test_sync_returns_not_found_when_remote_auth_missing(self):
account = DummyAccount()
@@ -97,6 +137,43 @@ class CliproxyapiSyncTests(unittest.TestCase):
self.assertEqual(result["last_probe_error_code"], "account_deactivated")
self.assertEqual(result["remote_state"], "account_deactivated")
def test_sync_retries_remote_probe_until_success(self):
account = DummyAccount(email="demo@example.com", user_id="acct-123")
auth_files = [
{
"name": "demo@example.com.json",
"provider": "codex",
"email": "demo@example.com",
"auth_index": "auth-001",
"status": "active",
"status_message": "",
"unavailable": False,
}
]
with mock.patch("services.cliproxyapi_sync.list_auth_files", return_value=auth_files):
with mock.patch(
"services.cliproxyapi_sync._probe_remote_auth",
side_effect=[
RuntimeError("CLIProxyAPI 请求超时http://127.0.0.1:8317"),
RuntimeError("CLIProxyAPI 无法连接,请确认服务已启动或 API URL 是否正确http://127.0.0.1:8317"),
{
"last_probe_at": "2026-03-31T00:00:00Z",
"last_probe_status_code": 200,
"last_probe_error_code": "",
"last_probe_message": "ok",
"remote_state": "usable",
},
],
) as probe_mock:
with mock.patch("services.cliproxyapi_sync.time.sleep") as sleep_mock:
result = sync_chatgpt_cliproxyapi_status(account, api_url="http://127.0.0.1:8317", api_key="demo")
self.assertTrue(result["uploaded"])
self.assertEqual(result["remote_state"], "usable")
self.assertEqual(probe_mock.call_count, 3)
self.assertEqual(sleep_mock.call_count, 2)
if __name__ == "__main__":
unittest.main()