diff --git a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx index 825c7e98d..c26b260bf 100644 --- a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx +++ b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx @@ -35,10 +35,16 @@ import { captureClientError, installWebviewLogBridge, } from '../services/errorReporting'; +import type { AuthNetworkFailure } from '../services/generated/AuthNetworkFailure'; +import type { AuthResponseInvalid } from '../services/generated/AuthResponseInvalid'; +import type { AuthServiceUnavailable } from '../services/generated/AuthServiceUnavailable'; +import type { ClientSessionPersistFailed } from '../services/generated/ClientSessionPersistFailed'; import type { PasswordLoginRejected } from '../services/generated/PasswordLoginRejected'; import type { PhoneCodeLoginRejected } from '../services/generated/PhoneCodeLoginRejected'; +import type { RuntimeSessionInstallFailed } from '../services/generated/RuntimeSessionInstallFailed'; import type { SendCodeRejected } from '../services/generated/SendCodeRejected'; import type { ServerAddressRejected } from '../services/generated/ServerAddressRejected'; +import type { UnexpectedRejection } from '../services/generated/UnexpectedRejection'; import { beginPlatformSessionClearTransition, beginPlatformSessionTransition, @@ -269,7 +275,13 @@ export function AuthenticatedClient({ if (!isActiveRun()) return; // 失败一律先离开检查态:系统变体虽然要原样抛出上报,界面也不能卡在 loading。 setAuthStatus('unauthenticated'); - if (!(error instanceof ClientAuthErrorWrapper)) throw error; + // 启动检查超时等我们自己的失败:改动前会显示原因 + 重试按钮,这里恢复同样的可见反馈。 + if (!(error instanceof ClientAuthErrorWrapper)) { + const text = error instanceof Error ? error.message : String(error); + setAuthCheckError(text); + setLoginStatus(text); + throw error; + } const failure = error.error; switch (failure.type) { // 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。 @@ -368,15 +380,83 @@ export function AuthenticatedClient({ case 'sessionInvalidated': case 'permissionDenied': break; - // 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。 - case 'authNetworkFailure': - case 'authServiceUnavailable': - case 'unexpectedRejection': - case 'authResponseInvalid': - case 'clientSessionPersistFailed': - case 'runtimeSessionInstallFailed': - case 'authClientInitFailed': + // 系统失败:调用方处理不了,先按原始事实给用户可见反馈,再原样抛出 → 全局 unhandledrejection 交给错误池。 + case 'authNetworkFailure': { + const payload = failure as AuthNetworkFailure; + const text = + payload.reason === 'timeout' + ? '登录服务响应超时,请检查服务器地址和网络后重试' + : '无法连接登录服务,请确认配套后端或 API 代理已启动后重试'; + setAuthCheckError(text); + setLoginStatus(text); throw error; + } + case 'authServiceUnavailable': { + const payload = failure as AuthServiceUnavailable; + const text = `登录服务暂时不可用(HTTP ${payload.status})`; + setAuthCheckError(text); + setLoginStatus(text); + throw error; + } + case 'unexpectedRejection': { + const payload = failure as UnexpectedRejection; + const text = payload.serverMessage + ? `登录服务请求失败(HTTP ${payload.status}):${payload.serverMessage}` + : `登录服务请求失败(HTTP ${payload.status})`; + setAuthCheckError(text); + setLoginStatus(text); + throw error; + } + case 'authResponseInvalid': { + const payload = failure as AuthResponseInvalid; + let text: string; + switch (payload.reason) { + case 'notJson': + text = '登录服务响应不是合法 JSON'; + break; + case 'invalidBody': + text = '登录服务响应结构不符合契约'; + break; + case 'missingRefreshCookie': + text = '登录服务未返回新的续期凭据'; + break; + case 'missingUserIdentity': + text = '登录服务未返回用户身份'; + break; + case 'serverRejected': + text = payload.serverMessage + ? `登录服务拒绝了本次请求:${payload.serverMessage}` + : '登录服务拒绝了本次请求'; + break; + default: { + expectNever(payload.reason); + text = '登录服务响应不符合契约'; + } + } + setAuthCheckError(text); + setLoginStatus(text); + throw error; + } + case 'clientSessionPersistFailed': { + const payload = failure as ClientSessionPersistFailed; + const text = `本机登录凭据读写失败:${payload.detail}`; + setAuthCheckError(text); + setLoginStatus(text); + throw error; + } + case 'runtimeSessionInstallFailed': { + const payload = failure as RuntimeSessionInstallFailed; + const text = `本机运行时会话安装失败:${payload.detail}`; + setAuthCheckError(text); + setLoginStatus(text); + throw error; + } + case 'authClientInitFailed': { + const text = '创建登录网络客户端失败'; + setAuthCheckError(text); + setLoginStatus(text); + throw error; + } default: { expectNever(failure); throw error; 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 0fad9ed16..9ead2250f 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts @@ -127,9 +127,15 @@ export function registerAuthTests() { ).toBe(true), { timeout: 20_000 }, ); + // 启动检查失败在改动前就会显示原因 + 重试按钮;上报是叠加的,不是替换。 + expect( + await screen.findByText( + '检查登录状态超时,请检查服务器地址和网络后重试', + ), + ).not.toBeNull(); expect( screen.queryByRole('button', { name: '重试登录状态检查' }), - ).toBeNull(); + ).not.toBeNull(); await screen.findByRole('main', { name: '登录' }); } finally { window.removeEventListener('unhandledrejection', onRejection);