From aed2bb9c341b8f33e185ab6a19a3ede2d40a9129 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 02:25:02 +0800 Subject: [PATCH] =?UTF-8?q?AGC=E8=AE=A4=E8=AF=81=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=8C=89=E6=96=B0=E5=8F=98=E4=BD=93=E5=88=86=E6=B5=81=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E4=B8=8A=E4=B8=8B=E6=96=87=20-=20AuthenticatedClient?= =?UTF-8?q?=20=E7=9A=84=E4=B8=89=E4=B8=AA=20catch=20=E5=88=97=E5=85=A8?= =?UTF-8?q?=E6=96=B0=E5=8F=98=E4=BD=93=EF=BC=9A=E6=97=A0=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=8F=98=E4=BD=93=E7=9B=B4=E6=8E=A5=E7=BB=99=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E6=96=87=E6=A1=88=EF=BC=8C=E5=B8=A6=E8=BD=BD=E8=8D=B7=E5=8F=98?= =?UTF-8?q?=E4=BD=93=E5=85=88=20as=20=E5=86=8D=E8=AF=BB=20serverMessage=20?= =?UTF-8?q?-=20=E7=B3=BB=E7=BB=9F=E5=8F=98=E4=BD=93=E9=80=90=E4=B8=AA?= =?UTF-8?q?=E5=88=97=E5=87=BA=E5=90=8E=E5=8E=9F=E6=A0=B7=E6=8A=9B=E5=87=BA?= =?UTF-8?q?=EF=BC=8Cdefault=20=E4=BB=8D=E7=94=A8=20expectNever=20=E6=8A=8A?= =?UTF-8?q?=E6=BC=8F=E6=8E=A5=E5=8F=98=E4=BD=93=E5=8D=A1=E5=9C=A8=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E6=9C=9F=20-=20clientAuth/ClientAuthErrorWrapper=20?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E5=8E=BB=E6=8E=89=E5=AF=B9=20message=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=9A=84=E5=81=87=E8=AE=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/AuthenticatedClient.tsx | 322 +++++++++++------- .../src/services/clientAuth.ts | 4 +- .../src/services/clientAuthErrorWrapper.ts | 4 +- 3 files changed, 210 insertions(+), 120 deletions(-) diff --git a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx index f380ab5d6..a03f53d93 100644 --- a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx +++ b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx @@ -35,18 +35,9 @@ import { captureClientError, installWebviewLogBridge, } from '../services/errorReporting'; -import type { LoginCodeMissing } from '../services/generated/LoginCodeMissing'; -import type { PasswordEntryInputRejected } from '../services/generated/PasswordEntryInputRejected'; -import type { PasswordMissing } from '../services/generated/PasswordMissing'; -import type { PermissionDenied } from '../services/generated/PermissionDenied'; -import type { PhoneLoginInputRejected } from '../services/generated/PhoneLoginInputRejected'; -import type { PhoneNumberInvalid } from '../services/generated/PhoneNumberInvalid'; -import type { PhoneOrPasswordMismatch } from '../services/generated/PhoneOrPasswordMismatch'; -import type { SendCodeInputRejected } from '../services/generated/SendCodeInputRejected'; -import type { ServerAddressRejected } from '../services/generated/ServerAddressRejected'; -import type { SessionAuthorityRejected } from '../services/generated/SessionAuthorityRejected'; -import type { SmsCodeInvalidOrExpired } from '../services/generated/SmsCodeInvalidOrExpired'; -import type { SmsCodeThrottled } from '../services/generated/SmsCodeThrottled'; +import type { PasswordLoginRejected } from '../services/generated/PasswordLoginRejected'; +import type { PhoneCodeLoginRejected } from '../services/generated/PhoneCodeLoginRejected'; +import type { SendCodeRejected } from '../services/generated/SendCodeRejected'; import { beginPlatformSessionClearTransition, beginPlatformSessionTransition, @@ -280,76 +271,120 @@ export function AuthenticatedClient({ if (!(error instanceof ClientAuthErrorWrapper)) throw error; const failure = error.error; switch (failure.type) { - // 用户自己能改的输入 / 前置条件:展示原因并停在登录页,不进错误池。 - case 'serverAddressRejected': { - const payload = failure as ServerAddressRejected; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + // 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。 + case 'serverAddressEmptyOrTooLong': { + const text = '服务器地址非法: 不能为空或超长'; + setAuthCheckError(text); + setLoginStatus(text); + break; + } + case 'serverAddressNotAUrl': { + const text = '服务器地址非法: 不是合法地址'; + setAuthCheckError(text); + setLoginStatus(text); + break; + } + case 'serverAddressHasCredentials': { + const text = '服务器地址非法: 不能包含用户名或密码'; + setAuthCheckError(text); + setLoginStatus(text); + break; + } + case 'serverAddressHasPathOrQueryOrFragment': { + const text = '服务器地址非法: 不能带路径、查询或片段'; + setAuthCheckError(text); + setLoginStatus(text); + break; + } + case 'serverAddressNotHttps': { + const text = '服务器地址非法: 远程地址必须使用 https'; + setAuthCheckError(text); + setLoginStatus(text); + break; + } + case 'serverAddressUnsupportedScheme': { + const text = '服务器地址非法: 只支持 http 或 https'; + setAuthCheckError(text); + setLoginStatus(text); + break; + } + case 'serverAddressOutsideChannel': { + const text = '服务器地址非法: 不在当前构建渠道范围内'; + setAuthCheckError(text); + setLoginStatus(text); break; } case 'phoneNumberInvalid': { - const payload = failure as PhoneNumberInvalid; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + const text = '手机号无效: 需为纯数字且不超过 32 位'; + setAuthCheckError(text); + setLoginStatus(text); break; } case 'passwordMissing': { - const payload = failure as PasswordMissing; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + const text = '密码无效: 不能为空'; + setAuthCheckError(text); + setLoginStatus(text); break; } case 'loginCodeMissing': { - const payload = failure as LoginCodeMissing; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + const text = '验证码无效: 不能为空'; + setAuthCheckError(text); + setLoginStatus(text); break; } - case 'passwordEntryInputRejected': { - const payload = failure as PasswordEntryInputRejected; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + case 'passwordLoginRejected': { + const payload = failure as PasswordLoginRejected; + const text = `登录失败: ${payload.serverMessage ?? '服务端拒绝了本次登录'}`; + setAuthCheckError(text); + setLoginStatus(text); break; } case 'phoneOrPasswordMismatch': { - const payload = failure as PhoneOrPasswordMismatch; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + const text = '手机号或密码错误'; + setAuthCheckError(text); + setLoginStatus(text); break; } - case 'sendCodeInputRejected': { - const payload = failure as SendCodeInputRejected; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + case 'sendCodeRejected': { + const payload = failure as SendCodeRejected; + const text = `发送验证码失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`; + setAuthCheckError(text); + setLoginStatus(text); break; } case 'smsCodeThrottled': { - const payload = failure as SmsCodeThrottled; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + const text = '发送验证码过于频繁,请稍后再试'; + setAuthCheckError(text); + setLoginStatus(text); break; } - case 'phoneLoginInputRejected': { - const payload = failure as PhoneLoginInputRejected; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + case 'phoneCodeLoginRejected': { + const payload = failure as PhoneCodeLoginRejected; + const text = `验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`; + setAuthCheckError(text); + setLoginStatus(text); break; } - case 'smsCodeInvalidOrExpired': { - const payload = failure as SmsCodeInvalidOrExpired; - setAuthCheckError(payload.message); - setLoginStatus(payload.message); + case 'smsCodeRejected': { + const text = '验证码错误或已过期'; + setAuthCheckError(text); + setLoginStatus(text); break; } // 会话权威失效:Rust 已按未登录处理;走到这里也只按未登录呈现。 - case 'sessionAuthorityRejected': + case 'sessionInvalidated': case 'permissionDenied': break; // 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。 - case 'authNetworkUnavailable': + case 'authNetworkTimeout': + case 'authNetworkUnreachable': case 'authServiceUnavailable': case 'unexpectedRejection': - case 'authResponseMalformed': + case 'authResponseNotJson': + case 'authResponseInvalidBody': + case 'authResponseMissingRefreshCookie': + case 'authResponseMissingUserIdentity': + case 'authResponseServerRejected': case 'clientSessionPersistFailed': case 'runtimeSessionInstallFailed': case 'authClientInitFailed': @@ -421,7 +456,8 @@ export function AuthenticatedClient({ * * - 承载:命令失败由 `invokeClientAuth` 装进 `ClientAuthErrorWrapper`,`error` 就是判别联合, * `failure.type` 是唯一分流键; - * - 业务 / 会话变体:把载荷自带的 `message` 原样给用户,永不进错误池; + * - 业务 / 会话变体:无载荷变体用本 catch 的固定文案;带载荷变体先 `as` 取具名类型, + * 再用它自己的 `serverMessage` 拼上下文(Rust 不预拼用户可见文案),永不进错误池; * - 系统变体:原样 `throw`,经全局 `unhandledrejection` 交给错误池; * - `default: expectNever` 保证 Rust 新增变体时这里编译失败。 * @@ -452,71 +488,98 @@ export function AuthenticatedClient({ if (!(error instanceof ClientAuthErrorWrapper)) throw error; const failure = error.error; switch (failure.type) { - case 'serverAddressRejected': { - const payload = failure as ServerAddressRejected; - setLoginStatus(payload.message); + // 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。 + case 'serverAddressEmptyOrTooLong': { + setLoginStatus('服务器地址非法: 不能为空或超长'); + break; + } + case 'serverAddressNotAUrl': { + setLoginStatus('服务器地址非法: 不是合法地址'); + break; + } + case 'serverAddressHasCredentials': { + setLoginStatus('服务器地址非法: 不能包含用户名或密码'); + break; + } + case 'serverAddressHasPathOrQueryOrFragment': { + setLoginStatus('服务器地址非法: 不能带路径、查询或片段'); + break; + } + case 'serverAddressNotHttps': { + setLoginStatus('服务器地址非法: 远程地址必须使用 https'); + break; + } + case 'serverAddressUnsupportedScheme': { + setLoginStatus('服务器地址非法: 只支持 http 或 https'); + break; + } + case 'serverAddressOutsideChannel': { + setLoginStatus('服务器地址非法: 不在当前构建渠道范围内'); break; } case 'phoneNumberInvalid': { - const payload = failure as PhoneNumberInvalid; - setLoginStatus(payload.message); + setLoginStatus('手机号无效: 需为纯数字且不超过 32 位'); break; } case 'passwordMissing': { - const payload = failure as PasswordMissing; - setLoginStatus(payload.message); + setLoginStatus('密码无效: 不能为空'); break; } case 'loginCodeMissing': { - const payload = failure as LoginCodeMissing; - setLoginStatus(payload.message); + setLoginStatus('验证码无效: 不能为空'); break; } - case 'passwordEntryInputRejected': { - const payload = failure as PasswordEntryInputRejected; - setLoginStatus(payload.message); + case 'passwordLoginRejected': { + const payload = failure as PasswordLoginRejected; + setLoginStatus( + `登录失败: ${payload.serverMessage ?? '服务端拒绝了本次登录'}`, + ); break; } case 'phoneOrPasswordMismatch': { - const payload = failure as PhoneOrPasswordMismatch; - setLoginStatus(payload.message); + setLoginStatus('手机号或密码错误'); break; } - case 'sendCodeInputRejected': { - const payload = failure as SendCodeInputRejected; - setLoginStatus(payload.message); + case 'sendCodeRejected': { + const payload = failure as SendCodeRejected; + setLoginStatus( + `发送验证码失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`, + ); break; } case 'smsCodeThrottled': { - const payload = failure as SmsCodeThrottled; - setLoginStatus(payload.message); + setLoginStatus('发送验证码过于频繁,请稍后再试'); break; } - case 'phoneLoginInputRejected': { - const payload = failure as PhoneLoginInputRejected; - setLoginStatus(payload.message); + case 'phoneCodeLoginRejected': { + const payload = failure as PhoneCodeLoginRejected; + setLoginStatus( + `验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`, + ); break; } - case 'smsCodeInvalidOrExpired': { - const payload = failure as SmsCodeInvalidOrExpired; - setLoginStatus(payload.message); + case 'smsCodeRejected': { + setLoginStatus('验证码错误或已过期'); break; } - case 'sessionAuthorityRejected': { - const payload = failure as SessionAuthorityRejected; - setLoginStatus(payload.message); + case 'sessionInvalidated': { + setLoginStatus('登录状态已失效,请重新登录'); break; } case 'permissionDenied': { - const payload = failure as PermissionDenied; - setLoginStatus(payload.message); + setLoginStatus('当前账号没有权限执行此操作'); break; } // 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。 - case 'authNetworkUnavailable': + case 'authNetworkTimeout': + case 'authNetworkUnreachable': case 'authServiceUnavailable': case 'unexpectedRejection': - case 'authResponseMalformed': + case 'authResponseNotJson': + case 'authResponseInvalidBody': + case 'authResponseMissingRefreshCookie': + case 'authResponseMissingUserIdentity': + case 'authResponseServerRejected': case 'clientSessionPersistFailed': case 'runtimeSessionInstallFailed': case 'authClientInitFailed': @@ -593,71 +656,98 @@ export function AuthenticatedClient({ if (!(error instanceof ClientAuthErrorWrapper)) throw error; const failure = error.error; switch (failure.type) { - case 'serverAddressRejected': { - const payload = failure as ServerAddressRejected; - setLoginStatus(payload.message); + // 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。 + case 'serverAddressEmptyOrTooLong': { + setLoginStatus('服务器地址非法: 不能为空或超长'); + break; + } + case 'serverAddressNotAUrl': { + setLoginStatus('服务器地址非法: 不是合法地址'); + break; + } + case 'serverAddressHasCredentials': { + setLoginStatus('服务器地址非法: 不能包含用户名或密码'); + break; + } + case 'serverAddressHasPathOrQueryOrFragment': { + setLoginStatus('服务器地址非法: 不能带路径、查询或片段'); + break; + } + case 'serverAddressNotHttps': { + setLoginStatus('服务器地址非法: 远程地址必须使用 https'); + break; + } + case 'serverAddressUnsupportedScheme': { + setLoginStatus('服务器地址非法: 只支持 http 或 https'); + break; + } + case 'serverAddressOutsideChannel': { + setLoginStatus('服务器地址非法: 不在当前构建渠道范围内'); break; } case 'phoneNumberInvalid': { - const payload = failure as PhoneNumberInvalid; - setLoginStatus(payload.message); + setLoginStatus('手机号无效: 需为纯数字且不超过 32 位'); break; } case 'passwordMissing': { - const payload = failure as PasswordMissing; - setLoginStatus(payload.message); + setLoginStatus('密码无效: 不能为空'); break; } case 'loginCodeMissing': { - const payload = failure as LoginCodeMissing; - setLoginStatus(payload.message); + setLoginStatus('验证码无效: 不能为空'); break; } - case 'passwordEntryInputRejected': { - const payload = failure as PasswordEntryInputRejected; - setLoginStatus(payload.message); + case 'passwordLoginRejected': { + const payload = failure as PasswordLoginRejected; + setLoginStatus( + `登录失败: ${payload.serverMessage ?? '服务端拒绝了本次登录'}`, + ); break; } case 'phoneOrPasswordMismatch': { - const payload = failure as PhoneOrPasswordMismatch; - setLoginStatus(payload.message); + setLoginStatus('手机号或密码错误'); break; } - case 'sendCodeInputRejected': { - const payload = failure as SendCodeInputRejected; - setLoginStatus(payload.message); + case 'sendCodeRejected': { + const payload = failure as SendCodeRejected; + setLoginStatus( + `发送验证码失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`, + ); break; } case 'smsCodeThrottled': { - const payload = failure as SmsCodeThrottled; - setLoginStatus(payload.message); + setLoginStatus('发送验证码过于频繁,请稍后再试'); break; } - case 'phoneLoginInputRejected': { - const payload = failure as PhoneLoginInputRejected; - setLoginStatus(payload.message); + case 'phoneCodeLoginRejected': { + const payload = failure as PhoneCodeLoginRejected; + setLoginStatus( + `验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`, + ); break; } - case 'smsCodeInvalidOrExpired': { - const payload = failure as SmsCodeInvalidOrExpired; - setLoginStatus(payload.message); + case 'smsCodeRejected': { + setLoginStatus('验证码错误或已过期'); break; } - case 'sessionAuthorityRejected': { - const payload = failure as SessionAuthorityRejected; - setLoginStatus(payload.message); + case 'sessionInvalidated': { + setLoginStatus('登录状态已失效,请重新登录'); break; } case 'permissionDenied': { - const payload = failure as PermissionDenied; - setLoginStatus(payload.message); + setLoginStatus('当前账号没有权限执行此操作'); break; } // 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。 - case 'authNetworkUnavailable': + case 'authNetworkTimeout': + case 'authNetworkUnreachable': case 'authServiceUnavailable': case 'unexpectedRejection': - case 'authResponseMalformed': + case 'authResponseNotJson': + case 'authResponseInvalidBody': + case 'authResponseMissingRefreshCookie': + case 'authResponseMissingUserIdentity': + case 'authResponseServerRejected': case 'clientSessionPersistFailed': case 'runtimeSessionInstallFailed': case 'authClientInitFailed': diff --git a/apps/ai-game-creator-shell/src/services/clientAuth.ts b/apps/ai-game-creator-shell/src/services/clientAuth.ts index 1f93ce84b..4ab11e049 100644 --- a/apps/ai-game-creator-shell/src/services/clientAuth.ts +++ b/apps/ai-game-creator-shell/src/services/clientAuth.ts @@ -41,8 +41,8 @@ function requireInvoke() { * * **信任映射,不做运行时形状嗅探**:Rust 与 TS 同包发布,认证命令的拒绝就是 ts-rs 生成的 * `ClientAuthError` 判别联合;出现别的形状属于 Tauri / Rust 侧缺陷,调用方 `switch` 的 - * `default` 分支仍会把它抛出去上报。包装本身不读变体字段(`message` 之类不保证每个变体都有)、 - * 不注入上下文、不兜底文案。 + * `default` 分支仍会把它抛出去上报。包装本身不读变体字段、 + * 不注入上下文、不拼用户可见文案。 */ async function invokeClientAuth( command: string, diff --git a/apps/ai-game-creator-shell/src/services/clientAuthErrorWrapper.ts b/apps/ai-game-creator-shell/src/services/clientAuthErrorWrapper.ts index d72e7483e..0f9b0a4f2 100644 --- a/apps/ai-game-creator-shell/src/services/clientAuthErrorWrapper.ts +++ b/apps/ai-game-creator-shell/src/services/clientAuthErrorWrapper.ts @@ -15,8 +15,8 @@ export type ClientErrorReportContext = { * 认证命令失败的 JS 侧载体:`error` 就是 Rust 的结构化拒绝(ts-rs 生成的 `ClientAuthError` * 判别联合)。 * - * 本类只把原始拒绝值装成 JS `Error`,不读、不产任何派生值:不读变体上的 `message`(变体不 - * 保证都有这个字段),不注入 `source` / `action`,`Error.message` 留空。分流只看类型化的变体, + * 本类只把原始拒绝值装成 JS `Error`,不读、不产任何派生值:不读变体字段,不注入 + * `source` / `action`,`Error.message` 留空。分流只看类型化的变体, * **不要用文案判断**;上报上下文由调用 `captureClientError` 时的显式入参决定。 */ export class ClientAuthErrorWrapper extends Error {