From b0d7d403d0b0ba1aaabec35770914f39c6f0a4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 2 Oct 2026 01:12:25 +0800 Subject: [PATCH] =?UTF-8?q?AGC=20=E8=AE=A4=E8=AF=81=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=8C=85=E8=A3=85=E5=8E=BB=E6=8E=89=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E5=BD=A2=E7=8A=B6=E5=97=85=E6=8E=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - invokeClientAuth 不再检查 type 是否存在:认证命令的拒绝按 ts-rs 映射直接转成 ClientAuthFailure - requireInvoke 提前到 try 之外:认证桥未安装是我们自己的失败关闭错误,保持原样抛出 - Error.message 仍只取拒绝值里的可展示字符串,只做展示,不参与分流 - 测试:新增"桥未安装保持原样抛出"与"Tauri 意外形状也按映射包装"两条判据;authFailureReporting 的意外形状用例改为断言 source=auth + 命令名 --- .../src/services/clientAuth.ts | 15 +++++----- .../tests/authFailureReporting.test.tsx | 5 ++-- .../tests/clientAuthFailure.test.ts | 30 +++++++++++++++---- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/apps/ai-game-creator-shell/src/services/clientAuth.ts b/apps/ai-game-creator-shell/src/services/clientAuth.ts index de7369bcb..19c751d20 100644 --- a/apps/ai-game-creator-shell/src/services/clientAuth.ts +++ b/apps/ai-game-creator-shell/src/services/clientAuth.ts @@ -37,25 +37,26 @@ function requireInvoke() { } /** - * 认证命令的统一入口:把 Rust 结构化拒绝转成 JS 侧的 `ClientAuthFailure`, - * 非结构化拒绝(Tauri / JS 运行时自己的错误)原样抛出。 + * 认证命令的统一入口:把 Tauri 的拒绝按 ts-rs 映射转成 JS 侧的 `ClientAuthFailure`。 * - * 只判 `type` 是不是字符串(这是 ts-rs 判别联合的稳定键),**不校验字段名、不读文案判断、 - * 不兜底文案**。要不要上报、给不给用户提示,由调用方在 catch 里按 `payload.type` 决定。 + * **信任映射,不做运行时形状嗅探**:Rust 与 TS 同包发布,认证命令的拒绝就是 + * `ClientAuthError`;出现别的形状属于 Tauri / Rust 侧的缺陷,调用方 `switch` 的 `default` + * 分支仍会把它抛出去上报。也不读文案判断、不兜底文案。 */ async function invokeClientAuth( command: string, args?: Record, ): Promise { + // 认证桥未安装是我们自己的失败关闭错误,不是命令拒绝:放在 try 之外,原样抛出。 + const invoke = requireInvoke(); try { - const invoke = requireInvoke(); // 不带参数时保持 `invoke(command)` 的单参调用形态,别给命令多塞一个 undefined。 return args === undefined ? await invoke(command) : await invoke(command, args); } catch (error) { - const rejection = error as { type?: unknown; message?: unknown }; - if (typeof rejection?.type !== 'string') throw error; + // `message` 只是给 Error 用的可展示字符串,不是分流判据;分流只按 `payload.type`。 + const rejection = error as { message?: unknown }; throw new ClientAuthFailure( typeof rejection.message === 'string' ? rejection.message : '', error as ClientAuthError, diff --git a/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx b/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx index 3ff8a3cec..435d56e37 100644 --- a/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx +++ b/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx @@ -132,7 +132,7 @@ describe('认证失败的上报判据', () => { } }); - it('非结构化拒绝原样抛出,由 unhandledrejection 兜底上报', async () => { + it('Tauri 的意外拒绝形状也被包装后原样抛出上报', async () => { const uninstall = installUnhandledRejectionBridge(); try { await submitPasswordLogin(async (command) => { @@ -147,7 +147,8 @@ describe('认证失败的上报判据', () => { await waitFor(() => expect(reportCalls()).toHaveLength(1)); expect(reportCalls()[0]?.[1]).toMatchObject({ - source: 'unhandledrejection', + source: 'auth', + action: 'login_client_with_password', message: 'IPC 桥接不可用', }); } finally { diff --git a/apps/ai-game-creator-shell/tests/clientAuthFailure.test.ts b/apps/ai-game-creator-shell/tests/clientAuthFailure.test.ts index 4f87031b9..c093d98a2 100644 --- a/apps/ai-game-creator-shell/tests/clientAuthFailure.test.ts +++ b/apps/ai-game-creator-shell/tests/clientAuthFailure.test.ts @@ -115,11 +115,8 @@ describe('ClientAuthFailure', () => { expect((failure as Error).message).toBe('请输入正确的手机号'); }); - it('非结构化拒绝原样抛出,不包装也不改写', async () => { - const rejection = new Error('需要在 Tauri App 内登录'); - installInvoke(async () => { - throw rejection; - }); + it('认证桥未安装时保持原样抛出,不包装成命令失败', async () => { + delete window.__TAURI__; await expect( loginClientWithPassword( @@ -127,7 +124,28 @@ describe('ClientAuthFailure', () => { 'secret', 'https://dev.genarrative.world', ), - ).rejects.toBe(rejection); + ).rejects.toThrow('需要在 Tauri App 内登录'); + }); + + it('Tauri 的意外形状也按映射包装:不做嗅探,但仍是可上报的 Error', async () => { + const rejection = new Error('IPC 桥接异常'); + installInvoke(async () => { + throw rejection; + }); + + const failure = await loginClientWithPassword( + '13800000000', + 'secret', + 'https://dev.genarrative.world', + ).catch((error: unknown) => error); + + expect(failure).toBeInstanceOf(ClientAuthFailure); + expect((failure as Error).message).toBe('IPC 桥接异常'); + expect((failure as ClientAuthFailure).payload).toBe(rejection); + expect((failure as ClientAuthFailure).context).toEqual({ + source: 'auth', + action: 'login_client_with_password', + }); }); it('结构化拒绝缺 message 时 Error.message 为空,不造兜底文案', async () => {