diff --git a/apps/ai-game-creator-shell/src/services/clientAuth.ts b/apps/ai-game-creator-shell/src/services/clientAuth.ts index be1470022..1633b2937 100644 --- a/apps/ai-game-creator-shell/src/services/clientAuth.ts +++ b/apps/ai-game-creator-shell/src/services/clientAuth.ts @@ -52,6 +52,27 @@ export function clearStoredAuthAccessToken() { let clientAuthRefreshPromise: Promise | null = null; +const CLIENT_AUTH_NETWORK_ERROR_MESSAGE = + '无法连接登录服务,请确认配套后端或 API 代理已启动后重试'; + +function getClientAuthNetworkErrorMessage(error: unknown) { + const detail = + error instanceof Error ? error.message.trim() : String(error).trim(); + if (/timed? ?out|timeout|超时/iu.test(detail)) { + return '无法连接登录服务:连接超时,请检查服务器地址和网络后重试'; + } + if (/econnrefused|connection refused|拒绝连接/iu.test(detail)) { + return '无法连接登录服务:服务器拒绝连接,请确认服务已启动并检查端口'; + } + if (/dns|resolve|name or service not known|无法解析/iu.test(detail)) { + return '无法连接登录服务:服务器地址无法解析,请检查服务器选择'; + } + if (/certificate|tls|ssl|证书/iu.test(detail)) { + return '无法连接登录服务:安全连接失败,请检查服务器地址和证书'; + } + return CLIENT_AUTH_NETWORK_ERROR_MESSAGE; +} + class ClientAuthRequestError extends Error { readonly status: number | null; readonly networkError: boolean; @@ -123,14 +144,9 @@ async function requestAuthJson( headers, }); } catch (error) { - const detail = - error instanceof Error ? error.message.trim() : String(error).trim(); - throw new ClientAuthRequestError( - detail - ? `无法连接登录服务:${detail}` - : '无法连接登录服务,请确认配套后端或 API 代理已启动后重试', - { networkError: true }, - ); + throw new ClientAuthRequestError(getClientAuthNetworkErrorMessage(error), { + networkError: true, + }); } if (!response.ok) { throw new ClientAuthRequestError( 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 3a40b8ddf..64eb16017 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts @@ -511,6 +511,44 @@ export function registerAuthTests() { expect(screen.queryByLabelText('已登录')).toBeNull(); }); + it('explains a refused login connection without exposing transport details', async () => { + vi.spyOn(globalThis, 'fetch').mockImplementation( + async (input: RequestInfo | URL) => { + const url = String(input); + if (url === '/api/auth/refresh') { + return new Response('', { status: 401 }); + } + if (url === '/api/auth/phone/login') { + throw new TypeError('connect ECONNREFUSED 127.0.0.1:8082'); + } + throw new Error(`unexpected fetch ${url}`); + }, + ); + + render( + React.createElement(AuthenticatedClient, null, () => + React.createElement('main', { 'aria-label': '已登录' }, 'ready'), + ), + ); + + await screen.findByRole('main', { name: '登录' }); + fireEvent.change(screen.getByLabelText('手机号'), { + target: { value: '13800000000' }, + }); + fireEvent.change(screen.getByLabelText('验证码'), { + target: { value: '123456' }, + }); + fireEvent.click(screen.getByRole('button', { name: '登录' })); + + expect( + await screen.findByText( + '无法连接登录服务:服务器拒绝连接,请确认服务已启动并检查端口', + ), + ).not.toBeNull(); + expect(screen.queryByText(/ECONNREFUSED|127\.0\.0\.1:8082/u)).toBeNull(); + expect(screen.queryByLabelText('已登录')).toBeNull(); + }); + it('keeps the stored token when startup auth check cannot reach the service', async () => { window.localStorage.setItem( 'genarrative.auth.access-token.v1', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 53f9dd166..b97ce30a8 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -14256,3 +14256,9 @@ - custom 只接受纯 HTTPS origin;`localhost` / loopback 的 HTTP 也允许用于本机服务,禁止把路径、查询参数、凭据或非本机明文 HTTP 地址作为服务器地址。 - Tauri release 的 HTTP capability scope 必须覆盖 release、dev、custom HTTPS 以及 loopback HTTP,否则前端选择虽能保存,plugin-http 仍会在请求层拒绝登录。 - 直连 Codex 的本机 External Editor API Key 必须按服务器 origin 独立存储。登录服务器切换后禁止复用另一 origin 的历史 Key 或 base URL;否则会出现登录走新服务器、平台资源生成仍请求旧服务器的漂移。 + +## 2026-08-18 AGC 登录网络错误与 Web Build 门禁对齐 + +- 网络 transport 的原始错误(例如浏览器 `Load failed`、URL、底层连接文本)不得直接进入登录页;客户端按超时、拒绝连接、地址解析和 TLS/证书四类可操作原因归一化,其余情况使用统一服务不可达提示。 +- 本地 `master` 推送门禁 `scripts/check-repository-ci.sh` 必须在 lint 后运行 Jenkins Web Build 所覆盖的 AGC AppSurface 套件,再执行 build、content 和 diff 检查,避免登录 UI 回归只在远端构建阶段暴露;完整 Vitest 仍由 Jenkins 生产构建执行。 +- 验证:`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts -t "shows a clear login service error|explains a refused login connection|keeps the stored token when startup auth check cannot reach the service"`、`npm run check:git-hooks`、`npm run check:encoding`、`git diff --check`。 diff --git a/scripts/check-repository-ci.sh b/scripts/check-repository-ci.sh index 40760e727..562853de1 100755 --- a/scripts/check-repository-ci.sh +++ b/scripts/check-repository-ci.sh @@ -20,6 +20,7 @@ git cat-file -e "${head_ref}^{commit}" 2>/dev/null || { echo "[repository-ci] base=${base_ref} head=$(git rev-parse "${head_ref}")" SPACETIME_SCHEMA_BASE_REF="${base_ref}" npm run lint +npm run test -- apps/ai-game-creator-shell/tests/appSurface.test.ts npm run build npm run check:content git diff --check "${base_ref}"..."${head_ref}" diff --git a/scripts/git-hooks.test.mjs b/scripts/git-hooks.test.mjs index 7a51eeb4f..64c1743a3 100644 --- a/scripts/git-hooks.test.mjs +++ b/scripts/git-hooks.test.mjs @@ -257,6 +257,10 @@ test('Gitea Repository checks and master pre-push share the same repository comm repositoryScript, /SPACETIME_SCHEMA_BASE_REF="\$\{base_ref\}" npm run lint/u, ); + assert.match( + repositoryScript, + /npm run lint[\s\S]*npm run test -- apps\/ai-game-creator-shell\/tests\/appSurface\.test\.ts[\s\S]*npm run build/u, + ); assert.match(repositoryScript, /npm run build/u); assert.match(repositoryScript, /npm run check:content/u); assert.match(repositoryScript, /git diff --check/u);