From 13f9d17dadb3a1716573b1a628eca3e02c41aadf Mon Sep 17 00:00:00 2001 From: yunxilyf <2574379425@qq.com> Date: Fri, 20 Mar 2026 23:03:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20OAuth=20token=20?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E4=B8=80=E6=AC=A1=E6=80=A7=E4=BB=A4=E7=89=8C?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E5=8F=8A=E6=89=B9=E9=87=8F=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=8D=A1=E6=AD=BB=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增强了 OAuth 刷新错误解析,遇到一次性 refresh_token 已失效时返回明确中文指引,合并了多余的 status_code 401 判断逻辑 - 为通用 API 请求增加可选超时与中断能力 (utils.js) - 为前端账号列表的单账号刷新和批量验证增加并发保护及超时控制,避免请求悬挂导致界面卡死 (accounts.js) --- src/core/openai/token_refresh.py | 31 ++++++++++++++++++++++++++++++- static/js/accounts.js | 19 ++++++++++++++++++- static/js/utils.js | 17 +++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/core/openai/token_refresh.py b/src/core/openai/token_refresh.py index 394c56e..b387ddb 100644 --- a/src/core/openai/token_refresh.py +++ b/src/core/openai/token_refresh.py @@ -57,6 +57,35 @@ class TokenRefreshManager: session = cffi_requests.Session(impersonate="chrome120", proxy=self.proxy_url) return session + def _parse_oauth_error(self, response: cffi_requests.Response) -> str: + """解析 OAuth 错误信息""" + body_text = (response.text or "").strip() + error_message = "" + + try: + body = response.json() + error_obj = body.get("error") if isinstance(body, dict) else None + if isinstance(error_obj, dict): + error_message = str(error_obj.get("message") or "").strip() + elif isinstance(body, dict): + error_message = str(body.get("error_description") or body.get("message") or "").strip() + except Exception: + pass + + error_lower = error_message.lower() + if "refresh token has already been used" in error_lower: + return "OAuth refresh_token 已失效(一次性令牌已被使用),请重新登录该账号后再上传 CPA" + if response.status_code == 401: + if error_message: + return f"OAuth token 刷新失败: {error_message}" + else: + return "OAuth token 刷新失败: refresh_token 无效或已过期,请重新登录账号" + if error_message: + return f"OAuth token 刷新失败: {error_message}" + if body_text: + return f"OAuth token 刷新失败: HTTP {response.status_code}, 响应: {body_text[:200]}" + return f"OAuth token 刷新失败: HTTP {response.status_code}" + def refresh_by_session_token(self, session_token: str) -> TokenRefreshResult: """ 使用 Session Token 刷新 @@ -167,7 +196,7 @@ class TokenRefreshManager: ) if response.status_code != 200: - result.error_message = f"OAuth token 刷新失败: HTTP {response.status_code}" + result.error_message = self._parse_oauth_error(response) logger.warning(f"{result.error_message}, 响应: {response.text[:200]}") return result diff --git a/static/js/accounts.js b/static/js/accounts.js index fe9848c..10f83d4 100644 --- a/static/js/accounts.js +++ b/static/js/accounts.js @@ -11,6 +11,8 @@ let selectedAccounts = new Set(); let isLoading = false; let selectAllPages = false; // 是否选中了全部页 let currentFilters = { status: '', email_service: '', search: '' }; // 当前筛选条件 +const refreshingAccountIds = new Set(); +let isBatchValidating = false; // DOM 元素 const elements = { @@ -488,6 +490,12 @@ function updateBatchButtons() { // 刷新单个账号Token async function refreshToken(id) { + if (refreshingAccountIds.has(id)) { + toast.info('该账号正在刷新,请稍候...'); + return; + } + refreshingAccountIds.add(id); + try { toast.info('正在刷新Token...'); const result = await api.post(`/accounts/${id}/refresh`); @@ -500,6 +508,8 @@ async function refreshToken(id) { } } catch (error) { toast.error('刷新失败: ' + error.message); + } finally { + refreshingAccountIds.delete(id); } } @@ -528,17 +538,24 @@ async function handleBatchRefresh() { // 批量验证Token async function handleBatchValidate() { if (getEffectiveCount() === 0) return; + if (isBatchValidating) { + toast.info('批量验证进行中,请稍候...'); + return; + } + + isBatchValidating = true; elements.batchValidateBtn.disabled = true; elements.batchValidateBtn.textContent = '验证中...'; try { - const result = await api.post('/accounts/batch-validate', buildBatchPayload()); + const result = await api.post('/accounts/batch-validate', buildBatchPayload(), { timeoutMs: 120000 }); toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`); loadAccounts(); } catch (error) { toast.error('批量验证失败: ' + error.message); } finally { + isBatchValidating = false; updateBatchButtons(); } } diff --git a/static/js/utils.js b/static/js/utils.js index b7b5dab..9862969 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -187,12 +187,21 @@ class ApiClient { }; const finalOptions = { ...defaultOptions, ...options }; + const timeoutMs = Number(finalOptions.timeoutMs || 0); + delete finalOptions.timeoutMs; if (finalOptions.body && typeof finalOptions.body === 'object') { finalOptions.body = JSON.stringify(finalOptions.body); } + let timeoutId = null; try { + if (timeoutMs > 0) { + const controller = new AbortController(); + finalOptions.signal = controller.signal; + timeoutId = setTimeout(() => controller.abort(), timeoutMs); + } + const response = await fetch(url, finalOptions); const data = await response.json().catch(() => ({})); @@ -205,11 +214,19 @@ class ApiClient { return data; } catch (error) { + if (error.name === 'AbortError') { + const timeoutError = new Error('请求超时,请稍后重试'); + throw timeoutError; + } // 网络错误处理 if (!error.response) { toast.error('网络连接失败,请检查网络'); } throw error; + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } } }