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 13dd6c794..4f1ebbe30 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 @@ -7,9 +7,10 @@ //! 响应契约的 5 种破损各自只占一个变体:前端 `switch (failure.type)` 选到类别后,再用 //! `switch (payload.reason)` 在**类型化**的细分上分流,仍然不碰文案。 //! -//! Rust **不预拼用户可见文案**:载荷只装原始事实(服务端 400 的原文、HTTP 状态码),服务端没给原文 -//! 就是 `None`;前缀与句式由前端调用方在自己的 catch 分支按当前操作拼接。本机 IO 的原始错误只写本地 -//! 日志,不进载荷(避免把 AppData 路径带进登录页与上报包),对应变体无字段、前端给固定文案。 +//! Rust **不预拼用户可见文案**:载荷只装原始事实(服务端 400 的原文、HTTP 状态码、本机 IO / +//! 网络客户端构建失败的原始错误 `detail`),服务端没给原文就是 `None`;前缀与句式由前端调用方在 +//! 自己的 catch 分支按当前操作拼接。`detail` 是给调用方分流与诊断用的原始事实,**不直接贴在界面上**; +//! 报告侧由 sanitize 把路径等替换成占位符。 use serde::Serialize; use ts_rs::TS; @@ -56,10 +57,10 @@ pub(crate) enum ClientAuthError { UnexpectedRejection(UnexpectedRejection), /// 登录服务响应的契约破损,细分原因见 [`AuthResponseInvalidReason`]。 AuthResponseInvalid(AuthResponseInvalid), - /// 本机登录凭据文件读写失败;原始错误只写本地日志。 - ClientSessionPersistFailed, - /// 本机运行时会话安装 / 清理失败;原始错误只写本地日志。 - RuntimeSessionInstallFailed, + /// 本机登录凭据文件读写失败;`detail` 是原始错误,供调用方与诊断用、不直接展示。 + ClientSessionPersistFailed(ClientSessionPersistFailed), + /// 本机运行时会话安装 / 清理失败;`detail` 同上。 + RuntimeSessionInstallFailed(RuntimeSessionInstallFailed), /// 认证网络客户端构建失败。 AuthClientInitFailed, } @@ -187,6 +188,22 @@ pub(crate) struct AuthResponseInvalid { pub(crate) server_message: Option, } +/// 本机凭据文件读写的原始错误明细。 +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))] +pub(crate) struct ClientSessionPersistFailed { + pub(crate) detail: String, +} + +/// 本机运行时会话安装 / 清理的原始错误明细。 +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))] +pub(crate) struct RuntimeSessionInstallFailed { + pub(crate) detail: String, +} + impl ClientAuthError { /// 会话路由的 401/403 是「登录态失效」:调用方据此清会话、按未登录处理, /// 既不给用户报错,也不进错误报告池。 @@ -262,11 +279,15 @@ mod tests { "authResponseInvalid", ), ( - ClientAuthError::ClientSessionPersistFailed, + ClientAuthError::ClientSessionPersistFailed(ClientSessionPersistFailed { + detail: "x".to_string(), + }), "clientSessionPersistFailed", ), ( - ClientAuthError::RuntimeSessionInstallFailed, + ClientAuthError::RuntimeSessionInstallFailed(RuntimeSessionInstallFailed { + detail: "x".to_string(), + }), "runtimeSessionInstallFailed", ), ( @@ -285,20 +306,9 @@ mod tests { #[test] fn unit_variants_serialize_without_payload_fields() { - for (error, expected_type) in [ - (ClientAuthError::LoginCodeMissing, "loginCodeMissing"), - ( - ClientAuthError::ClientSessionPersistFailed, - "clientSessionPersistFailed", - ), - ( - ClientAuthError::RuntimeSessionInstallFailed, - "runtimeSessionInstallFailed", - ), - ] { - let value = serde_json::to_value(error).expect("serialize auth error"); - assert_eq!(value, serde_json::json!({ "type": expected_type })); - } + let value = + serde_json::to_value(ClientAuthError::LoginCodeMissing).expect("serialize auth error"); + assert_eq!(value, serde_json::json!({ "type": "loginCodeMissing" })); } #[test] @@ -346,6 +356,14 @@ mod tests { .expect("serialize auth error"); assert_eq!(invalid["reason"], "serverRejected"); assert_eq!(invalid["serverMessage"], "登录服务请求失败"); + + let persist = serde_json::to_value(ClientAuthError::ClientSessionPersistFailed( + ClientSessionPersistFailed { + detail: "磁盘只读".to_string(), + }, + )) + .expect("serialize auth error"); + assert_eq!(persist["detail"], "磁盘只读"); } #[test] 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 d08e697cb..9a711da2b 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 @@ -6,8 +6,9 @@ use crate::auth_error::{ AuthNetworkFailure, AuthNetworkReason, AuthResponseInvalid, AuthResponseInvalidReason, - AuthServiceUnavailable, ClientAuthError, PasswordLoginRejected, PhoneCodeLoginRejected, - SendCodeRejected, ServerAddressReason, ServerAddressRejected, UnexpectedRejection, + AuthServiceUnavailable, ClientAuthError, ClientSessionPersistFailed, PasswordLoginRejected, + PhoneCodeLoginRejected, RuntimeSessionInstallFailed, SendCodeRejected, ServerAddressReason, + ServerAddressRejected, UnexpectedRejection, }; use crate::http_client::agc_main_site_client_builder; use crate::platform_session::{current_platform_session, PlatformSessionSnapshot}; @@ -221,21 +222,24 @@ fn session_file_path(app: &tauri::AppHandle) -> Result .app_data_dir() .map(|root| root.join(SESSION_FILE_NAME)) .map_err(|error| { - app_log!("无法读取 AGC 应用数据目录:{error}"); - ClientAuthError::ClientSessionPersistFailed + let detail = format!("无法读取 AGC 应用数据目录:{error}"); + app_log!("{detail}"); + ClientAuthError::ClientSessionPersistFailed(ClientSessionPersistFailed { detail }) }) } /// 凭据文件相关的字符串错误统一收口成凭据落盘失败变体。 fn session_persist_error(message: impl Into) -> ClientAuthError { - app_log!("{}", message.into()); - ClientAuthError::ClientSessionPersistFailed + let detail = message.into(); + app_log!("{detail}"); + ClientAuthError::ClientSessionPersistFailed(ClientSessionPersistFailed { detail }) } /// 本机运行时安装 / 清理失败的字符串错误统一收口。 fn runtime_session_error(message: impl Into) -> ClientAuthError { - app_log!("{}", message.into()); - ClientAuthError::RuntimeSessionInstallFailed + let detail = message.into(); + app_log!("{detail}"); + ClientAuthError::RuntimeSessionInstallFailed(RuntimeSessionInstallFailed { detail }) } /// 服务地址校验失败统一收口成带类型化原因的变体。 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 8b402ac56..6cb924232 100644 --- a/apps/ai-game-creator-shell/src/services/generated/ClientAuthError.ts +++ b/apps/ai-game-creator-shell/src/services/generated/ClientAuthError.ts @@ -2,8 +2,10 @@ import type { AuthNetworkFailure } from './AuthNetworkFailure'; import type { AuthResponseInvalid } from './AuthResponseInvalid'; import type { AuthServiceUnavailable } from './AuthServiceUnavailable'; +import type { ClientSessionPersistFailed } from './ClientSessionPersistFailed'; import type { PasswordLoginRejected } from './PasswordLoginRejected'; import type { PhoneCodeLoginRejected } from './PhoneCodeLoginRejected'; +import type { RuntimeSessionInstallFailed } from './RuntimeSessionInstallFailed'; import type { SendCodeRejected } from './SendCodeRejected'; import type { ServerAddressRejected } from './ServerAddressRejected'; import type { UnexpectedRejection } from './UnexpectedRejection'; @@ -28,6 +30,6 @@ export type ClientAuthError = | ({ type: 'authServiceUnavailable' } & AuthServiceUnavailable) | ({ type: 'unexpectedRejection' } & UnexpectedRejection) | ({ type: 'authResponseInvalid' } & AuthResponseInvalid) - | { type: 'clientSessionPersistFailed' } - | { type: 'runtimeSessionInstallFailed' } + | ({ type: 'clientSessionPersistFailed' } & ClientSessionPersistFailed) + | ({ type: 'runtimeSessionInstallFailed' } & RuntimeSessionInstallFailed) | { type: 'authClientInitFailed' }; diff --git a/apps/ai-game-creator-shell/src/services/generated/ClientSessionPersistFailed.ts b/apps/ai-game-creator-shell/src/services/generated/ClientSessionPersistFailed.ts new file mode 100644 index 000000000..258f43fb0 --- /dev/null +++ b/apps/ai-game-creator-shell/src/services/generated/ClientSessionPersistFailed.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * 本机凭据文件读写的原始错误明细。 + */ +export type ClientSessionPersistFailed = { detail: string }; diff --git a/apps/ai-game-creator-shell/src/services/generated/RuntimeSessionInstallFailed.ts b/apps/ai-game-creator-shell/src/services/generated/RuntimeSessionInstallFailed.ts new file mode 100644 index 000000000..df2940b5d --- /dev/null +++ b/apps/ai-game-creator-shell/src/services/generated/RuntimeSessionInstallFailed.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * 本机运行时会话安装 / 清理的原始错误明细。 + */ +export type RuntimeSessionInstallFailed = { detail: string }; diff --git a/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts b/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts index ed6a5c902..c343247a6 100644 --- a/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts +++ b/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts @@ -16,8 +16,10 @@ import type { AuthNetworkFailure } from '../src/services/generated/AuthNetworkFa import type { AuthResponseInvalid } from '../src/services/generated/AuthResponseInvalid'; import type { AuthServiceUnavailable } from '../src/services/generated/AuthServiceUnavailable'; import type { ClientAuthError } from '../src/services/generated/ClientAuthError'; +import type { ClientSessionPersistFailed } from '../src/services/generated/ClientSessionPersistFailed'; import type { PasswordLoginRejected } from '../src/services/generated/PasswordLoginRejected'; import type { PhoneCodeLoginRejected } from '../src/services/generated/PhoneCodeLoginRejected'; +import type { RuntimeSessionInstallFailed } from '../src/services/generated/RuntimeSessionInstallFailed'; import type { SendCodeRejected } from '../src/services/generated/SendCodeRejected'; import type { ServerAddressRejected } from '../src/services/generated/ServerAddressRejected'; import type { UnexpectedRejection } from '../src/services/generated/UnexpectedRejection'; @@ -113,9 +115,9 @@ function variantMessage(error: ClientAuthError): string { } } case 'clientSessionPersistFailed': - return '本机登录凭据读写失败'; + return (error as ClientSessionPersistFailed).detail; case 'runtimeSessionInstallFailed': - return '本机运行时会话安装失败'; + return (error as RuntimeSessionInstallFailed).detail; case 'authClientInitFailed': return '网络客户端构建失败'; default: {