From a51278699600c33cb4f14f9017ab041aec553d25 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:47:03 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=BBbody=E5=A4=B1=E8=B4=A5=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E5=B7=B2=E7=A1=AE=E8=AE=A4=E7=8A=B6=E6=80=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth_session:拿到 status 后 response.text() 失败不再一律报网络不可达;非 2xx 按状态码走既有分类(serverMessage 为 None,如 503 → authServiceUnavailable),只有 2xx 响应没收完才归传输层故障 - 抽出 classify_unreadable_body 并补单测(503 / 会话 401 / 400 / 2xx) - 决策记录追加该口径 --- .../src-tauri/src/auth_session.rs | 50 ++++++++++++++++--- .../shared-memory/decision-log.md | 1 + 2 files changed, 45 insertions(+), 6 deletions(-) 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 d10e65779..25ea5b770 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 @@ -573,6 +573,20 @@ fn map_auth_failure(status: StatusCode, body: &str, route: AuthRoute) -> ClientA }) } +/// body 读取失败时的归类。 +/// +/// 状态码此时已经拿到:非 2xx 按状态码走已有分类(body 没读到,服务端原文就是 `None`), +/// 不把服务端失败误报成传输层故障;只有 2xx 响应没收完才归网络不可达。 +fn classify_unreadable_body(status: StatusCode, route: AuthRoute) -> ClientAuthError { + if status.is_success() { + ClientAuthError::AuthNetworkFailure(AuthNetworkFailure { + reason: AuthNetworkReason::Unreachable, + }) + } else { + map_auth_failure(status, "", route) + } +} + fn response_data(body: &str) -> Result { let value: Value = serde_json::from_str(body) .map_err(|_| response_invalid(AuthResponseInvalidReason::NotJson))?; @@ -656,12 +670,10 @@ async fn request_auth( CookiePolicy::Capture | CookiePolicy::Require => refresh_cookie_from_response(&response), CookiePolicy::Ignore => None, }; - // 拿到状态码后读 body 失败属于传输层故障:按网络不可达上报,不猜契约。 - let text = response.text().await.map_err(|_| { - ClientAuthError::AuthNetworkFailure(AuthNetworkFailure { - reason: AuthNetworkReason::Unreachable, - }) - })?; + let text = match response.text().await { + Ok(text) => text, + Err(_) => return Err(classify_unreadable_body(status, auth_route(route))), + }; if !status.is_success() { return Err(map_auth_failure(status, &text, auth_route(route))); } @@ -1407,6 +1419,32 @@ mod tests { ); } + /// 读 body 失败时状态码已经拿到,不能被压成「网络不可达」。 + #[test] + fn unreadable_body_keeps_the_known_status() { + assert_eq!( + classify_unreadable_body(StatusCode::SERVICE_UNAVAILABLE, AuthRoute::PhoneLogin), + ClientAuthError::AuthServiceUnavailable(AuthServiceUnavailable { status: 503 }) + ); + assert_eq!( + classify_unreadable_body(StatusCode::UNAUTHORIZED, AuthRoute::Session), + ClientAuthError::SessionInvalidated + ); + assert_eq!( + classify_unreadable_body(StatusCode::BAD_REQUEST, AuthRoute::PasswordEntry), + ClientAuthError::PasswordLoginRejected(PasswordLoginRejected { + server_message: None + }) + ); + // 只有 2xx 响应没收完才是传输层故障。 + assert_eq!( + classify_unreadable_body(StatusCode::OK, AuthRoute::Other), + ClientAuthError::AuthNetworkFailure(AuthNetworkFailure { + reason: AuthNetworkReason::Unreachable, + }) + ); + } + #[test] fn keepalive_only_refreshes_after_the_freshness_window() { let issued = 1_000_000_u64; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 8cd0522d4..2585c35d1 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -13,6 +13,7 @@ - 决策(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,读 body 失败按已确认状态码归类):AGC 认证请求拿到 `status` 后 `response.text()` 失败,不再一律压成 `authNetworkFailure { unreachable }`;非 2xx 走既有分类(`serverMessage` 为 `None`,如 503 → `authServiceUnavailable { 503 }`),只有 2xx 响应没收完才算传输层故障。分类收敛在 `classify_unreadable_body`。 - 追加(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 用例。