From 836f8a7348d619418130c9222e3a6ed65031ec09 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 13:35:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7=E7=99=BB=E5=BD=9540?= =?UTF-8?q?1=E5=BD=92phoneCodeLoginRejected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth_session:/api/auth/phone/login 的 401(只来自「用户不存在」)改判为 phoneCodeLoginRejected{serverMessage},不再冒充「验证码错误或过期」 - auth_error:退役顶层变体 smsCodeRejected(唯一产生点就是这个错配的 401 分支) - 前端三个 catch 删掉 smsCodeRejected 分支;phoneCodeLoginRejected 文案统一为「验证码登录失败:<服务端原文>」,冒号改全角 - ts-rs 重新生成 ClientAuthError 联合;carrier 用例同步删掉 smsCodeRejected - 文档:ADR + decision-log 记录 401 归属与 smsCodeRejected 退役 --- .../src-tauri/src/auth_error.rs | 7 +++---- .../src-tauri/src/auth_session.rs | 16 +++++++++++---- .../src/app/AuthenticatedClient.tsx | 20 +++---------------- .../src/services/generated/ClientAuthError.ts | 1 - .../tests/clientAuthErrorCarrier.test.ts | 2 -- ...GC命令错误结构化与错误报告口径-2026-10-01.md | 2 ++ .../shared-memory/decision-log.md | 1 + 7 files changed, 21 insertions(+), 28 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/auth_error.rs b/apps/ai-game-creator-shell/src-tauri/src/auth_error.rs index 97fa9fb07..8c62e8deb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/auth_error.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/auth_error.rs @@ -37,10 +37,10 @@ pub(crate) enum ClientAuthError { SendCodeRejected(SendCodeRejected), /// `/api/auth/phone/send-code` 返回 429:发送过于频繁。 SmsCodeThrottled, - /// `/api/auth/phone/login` 返回 400:服务端拒绝本次输入。 + /// `/api/auth/phone/login` 返回 400/401:服务端拒绝本次验证码登录。 + /// + /// 401 目前只来自「用户不存在」;该路由验证通过后会即时建号,所以这条分支实际很少触发。 PhoneCodeLoginRejected(PhoneCodeLoginRejected), - /// `/api/auth/phone/login` 返回 401:验证码错误或过期。 - SmsCodeRejected, // ---- 会话:调用方按"未登录"处理,不给用户报错 ---- /// 会话路由 401:登录态失效。 @@ -259,7 +259,6 @@ mod tests { }), "phoneCodeLoginRejected", ), - (ClientAuthError::SmsCodeRejected, "smsCodeRejected"), (ClientAuthError::SessionInvalidated, "sessionInvalidated"), (ClientAuthError::PermissionDenied, "permissionDenied"), ( diff --git a/apps/ai-game-creator-shell/src-tauri/src/auth_session.rs b/apps/ai-game-creator-shell/src-tauri/src/auth_session.rs index 026fa176f..d10e65779 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/auth_session.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/auth_session.rs @@ -515,7 +515,9 @@ fn map_auth_failure(status: StatusCode, body: &str, route: AuthRoute) -> ClientA return match route { AuthRoute::Session => ClientAuthError::SessionInvalidated, AuthRoute::PasswordEntry => ClientAuthError::PhoneOrPasswordMismatch, - AuthRoute::PhoneLogin => ClientAuthError::SmsCodeRejected, + AuthRoute::PhoneLogin => { + ClientAuthError::PhoneCodeLoginRejected(PhoneCodeLoginRejected { server_message }) + } _ => ClientAuthError::UnexpectedRejection(UnexpectedRejection { status: status_code, server_message, @@ -1391,12 +1393,18 @@ mod tests { }) ); - let bad_code = map_auth_failure( + // 登录路由的 401 只来自「用户不存在」:仍是业务变体,保留服务端原文,不能进系统变体。 + let user_not_found = map_auth_failure( StatusCode::UNAUTHORIZED, - r#"{"error":{"message":"验证码错误"}}"#, + r#"{"error":{"message":"用户不存在"}}"#, AuthRoute::PhoneLogin, ); - assert_eq!(bad_code, ClientAuthError::SmsCodeRejected); + assert_eq!( + user_not_found, + ClientAuthError::PhoneCodeLoginRejected(PhoneCodeLoginRejected { + server_message: Some("用户不存在".to_string()), + }) + ); } #[test] diff --git a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx index b4bb237b4..42c8e687f 100644 --- a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx +++ b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx @@ -363,13 +363,7 @@ export function AuthenticatedClient({ } case 'phoneCodeLoginRejected': { const payload = failure as PhoneCodeLoginRejected; - const text = `验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`; - setAuthCheckError(text); - setLoginStatus(text); - break; - } - case 'smsCodeRejected': { - const text = '验证码错误或已过期'; + const text = `验证码登录失败:${payload.serverMessage ?? '服务端拒绝了本次请求'}`; setAuthCheckError(text); setLoginStatus(text); break; @@ -632,14 +626,10 @@ export function AuthenticatedClient({ case 'phoneCodeLoginRejected': { const payload = failure as PhoneCodeLoginRejected; setLoginStatus( - `验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`, + `验证码登录失败:${payload.serverMessage ?? '服务端拒绝了本次请求'}`, ); break; } - case 'smsCodeRejected': { - setLoginStatus('验证码错误或已过期'); - break; - } case 'sessionInvalidated': { setLoginStatus('登录状态已失效,请重新登录'); break; @@ -863,14 +853,10 @@ export function AuthenticatedClient({ case 'phoneCodeLoginRejected': { const payload = failure as PhoneCodeLoginRejected; setLoginStatus( - `验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`, + `验证码登录失败:${payload.serverMessage ?? '服务端拒绝了本次请求'}`, ); break; } - case 'smsCodeRejected': { - setLoginStatus('验证码错误或已过期'); - break; - } case 'sessionInvalidated': { setLoginStatus('登录状态已失效,请重新登录'); break; diff --git a/apps/ai-game-creator-shell/src/services/generated/ClientAuthError.ts b/apps/ai-game-creator-shell/src/services/generated/ClientAuthError.ts index d52f178dc..9e87dc86d 100644 --- a/apps/ai-game-creator-shell/src/services/generated/ClientAuthError.ts +++ b/apps/ai-game-creator-shell/src/services/generated/ClientAuthError.ts @@ -24,7 +24,6 @@ export type ClientAuthError = | ({ type: 'sendCodeRejected' } & SendCodeRejected) | { type: 'smsCodeThrottled' } | ({ type: 'phoneCodeLoginRejected' } & PhoneCodeLoginRejected) - | { type: 'smsCodeRejected' } | { type: 'sessionInvalidated' } | { type: 'permissionDenied' } | ({ type: 'authNetworkFailure' } & AuthNetworkFailure) diff --git a/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts b/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts index 4ae2f922b..d8b0b27a2 100644 --- a/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts +++ b/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts @@ -73,8 +73,6 @@ function variantMessage(error: ClientAuthError): string { return ( (error as PhoneCodeLoginRejected).serverMessage ?? '验证码登录被拒绝' ); - case 'smsCodeRejected': - return '验证码错误或过期'; case 'sessionInvalidated': return '登录态失效'; case 'permissionDenied': diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index 105172cce..3ee3053a5 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -59,6 +59,8 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - `429` 同样按路由判定:发码路由是频控(`smsCodeThrottled`),登录路由是「验证码错误次数过多」 (复用 `phoneCodeLoginRejected`,仍是用户可修正的输入问题、不进池),其余路由的 `429` 才落到 `unexpectedRejection`。 +- `/api/auth/phone/login` 的 `401` 只来自「用户不存在」(验证码错误/失效/过期都是 `400`),同样归 + `phoneCodeLoginRejected`;为此退役的 `smsCodeRejected` 曾把 401 冒充成「验证码错误或过期」,属于错配。 - 本地前置校验只做布尔判定、拿不到更细事实时不编字段:手机号校验 `phone_is_valid` 只回 true/false, 所以 `phoneNumberInvalid` 保持无字段,提示由调用方给固定文案;编不出事实的"细分原因"不硬造。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0a646bdc7..8cd0522d4 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -12,6 +12,7 @@ - 决策(2026-10-01,判定位置与出口):要不要上报只由 catch 子句里的 `switch (failure.type)` 判,`failure = error.error`;无字段业务 / 会话变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名类型、再用它自己的 `reason` / `serverMessage` / `status` 拼上本次操作的上下文前缀(`reason` 是枚举时再 `switch (payload.reason)`);Rust 不预拼用户可见文案、服务端原文缺失就是 `null`(无兜底文案)。系统变体原样 `throw` 经全局 `unhandledrejection` 入池(`captureClientError` 用 `instanceof` 解包 `error` 字段取原始错误),`default: expectNever(failure)` 让漏接变体编译失败。取代"未识别变体上调是故意的"。 - 决策(2026-10-01,Rust 侧不再降级):`refresh_session_inner` 的非权威失败直接 `Err(ClientAuthError)`,`ClientAuthStateView` / `ClientAuthRefreshView` 删除 `errorMessage`,续期结果删除 `failed`;`ClientAuthState` 收敛为 `authenticated | unauthenticated`,`ClientAuthRefreshResult` 收敛为 `refreshed | unauthenticated | stale`。 - 追加(2026-10-02,429 按路由判定):`/api/auth/phone/login` 验证码错误次数耗尽返回的 429 是用户可修正的输入问题,映射为 `phoneCodeLoginRejected`(复用现有业务变体、不进错误池);发码路由仍是 `smsCodeThrottled`,其余路由的 429 仍是 `unexpectedRejection`。 +- 追加(2026-10-02,401 归 phoneCodeLoginRejected):`/api/auth/phone/login` 的 401 只来自「用户不存在」(验证码错误/失效/过期在服务端都是 400,已由 `phoneCodeLoginRejected { serverMessage }` 带原文);顶层变体 `smsCodeRejected` 退役删除,前端三个 catch 去掉了它那个「验证码错误或已过期」的固定分支,`phoneCodeLoginRejected` 的文案统一为「验证码登录失败:<服务端原文>」。 - 追加(2026-10-02,系统类失败保留原始错误载荷):`clientSessionPersistFailed` / `runtimeSessionInstallFailed` / `authClientInitFailed` 都带 `detail: string`(原始 error),既让调用方有机会分流处理,也让报告包带够诊断信息;原始 error 同时经 `app_log!`(落盘前过 `sanitize_diagnostic_message`)记一行本地日志。`detail` 不贴到界面上:三个 catch 用本操作的固定文案(登录检查 / 发码 / 登录各自不同)。取代上一版"本机 IO 失败不进载荷、原始 error 只进日志"。 - 影响范围(2026-10-01 第二轮):`apps/ai-game-creator-shell/src/services/{clientAuth.ts,platformSession.ts}`(`clientAuthError.ts` 删除)、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、对应 vitest 用例。