diff --git a/apps/ai-game-creator-shell/src/services/clientAuth.ts b/apps/ai-game-creator-shell/src/services/clientAuth.ts index 54c4c27e8..c0b5e0750 100644 --- a/apps/ai-game-creator-shell/src/services/clientAuth.ts +++ b/apps/ai-game-creator-shell/src/services/clientAuth.ts @@ -72,6 +72,24 @@ export function getClientAuthRefreshOperation(apiBaseUrl: string) { const CLIENT_AUTH_NETWORK_ERROR_MESSAGE = '无法连接登录服务,请确认配套后端或 API 代理已启动后重试'; +function getClientAuthHttpErrorMessage(status: number, fallback: string) { + switch (status) { + case 408: + case 504: + return '登录服务响应超时,请检查服务器地址和网络后重试'; + case 429: + return '登录请求过于频繁,请稍后重试'; + case 500: + return '登录服务内部错误(HTTP 500),请稍后重试'; + case 502: + return '登录服务暂不可用:上游服务请求失败,请稍后重试'; + case 503: + return '登录服务暂不可用(HTTP 503),服务器可能正在维护,请稍后重试'; + default: + return fallback; + } +} + function getClientAuthNetworkErrorMessage(error: unknown) { const detail = error instanceof Error ? error.message.trim() : String(error).trim(); @@ -121,24 +139,26 @@ export function getClientAuthErrorMessage(error: unknown, fallback: string) { } async function readAuthErrorMessage(response: Response, fallback: string) { + const httpFallback = getClientAuthHttpErrorMessage(response.status, fallback); const text = await readClientHttpResponseText(response, { url: 'auth error response', }); if (!text.trim()) { - return fallback; + return httpFallback; } let parsed: unknown; try { parsed = JSON.parse(text) as unknown; } catch { - return fallback; + return httpFallback; } try { unwrapApiResponse(parsed); } catch (error) { - return error instanceof Error ? error.message : fallback; + const message = error instanceof Error ? error.message.trim() : ''; + return message && message !== '请求失败' ? message : httpFallback; } - return fallback; + return httpFallback; } async function requestAuthJson( diff --git a/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts index ffeb58e41..bfb771cb0 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts @@ -1272,6 +1272,37 @@ export function registerAuthTests() { ).toHaveLength(1); }); + it('shows the HTTP maintenance error when startup auth receives a 503', async () => { + window.localStorage.setItem( + 'genarrative.auth.access-token.v1', + 'existing-token', + ); + vi.spyOn(globalThis, 'fetch').mockImplementation( + async (input: RequestInfo | URL) => { + if (String(input) === '/api/auth/me') { + return new Response( + '503 Service Unavailable', + { status: 503, headers: { 'Content-Type': 'text/html' } }, + ); + } + throw new Error(`unexpected fetch ${String(input)}`); + }, + ); + + render( + React.createElement(AuthenticatedClient, null, () => + React.createElement('main', { 'aria-label': '已登录' }), + ), + ); + + expect(await screen.findByRole('main', { name: '登录' })).not.toBeNull(); + expect( + screen.getByText( + '登录服务暂不可用(HTTP 503),服务器可能正在维护,请稍后重试', + ), + ).not.toBeNull(); + }); + it('still calls logout when token refresh fails during logout retry', async () => { window.localStorage.setItem( 'genarrative.auth.access-token.v1', diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index a232c7612..8d8af7f25 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -63,6 +63,7 @@ npm 游戏的可预览产物固定为对应 package 目录下的 `dist/index.htm - AGC 客户端启动恢复按“读取本地凭据 → 刷新会话(无 token 或失效时)→ 读取当前用户 → Tauri 本地运行时会话安装”阶段执行。界面必须展示当前阶段和已等待时间;不能以无期限的单一 loading 文案隐藏网络或 Runner 故障。 - 客户端 HTTP 传输默认使用 15 秒超时并通过独立 `AbortController` 终止请求;调用方可为确需长耗时的请求显式传入 `timeoutMs: null`。调用方主动取消仍保留原始 `AbortError`,超时使用稳定的 `ClientHttpTimeoutError`,由认证层转换为可操作的中文提示。 - 会话恢复或本地 Runner 连接超时后必须进入登录页并提供“重试登录状态检查”。重试递增恢复代次并以运行标识忽略旧恢复任务的迟到 UI 写回;不得清除仍可用于后续重试的 access token,也不得重复并发刷新同一服务器的 refresh 请求。 +- AGC 壳启动认证遇到空响应、非 JSON 维护页或 5xx 时,必须按 HTTP 状态生成可操作的中文提示(503 明确标记服务暂不可用/可能维护),不能退化为 `读取当前用户失败`;后端返回的结构化错误 message 仍优先展示。 - Tauri Runner 的启动与 IPC 超时继续以 `runner/protocol.rs` 的 30 秒启动、10 秒读写为权威;启动等待循环会把 endpoint 探测预算裁剪到剩余启动期限,避免单次 ping 把 30 秒门禁延长。考虑复用旧 endpoint 前可能先消耗一次 IPC 等待,前端 UI 兜底取 45 秒,不改变 Runner 协议、启动策略或认证接口。 ## 2026-08-26 运行中自主扩图提案