本机IO失败载荷恢复detail

- 错误枚举:clientSessionPersistFailed / runtimeSessionInstallFailed 恢复成带 detail 的载荷变体,重新导出两个 ts-rs 绑定文件
- auth_session:IO 失败把原始 error 放进 detail 的同时,仍记一行本地日志
- 前端维持不展示 detail,继续用本操作的固定文案(登录检查 / 发码 / 登录)
- carrier 用例改回对两个载荷变体 as 取 detail,钉住"带载荷变体先取具名类型"
This commit is contained in:
2026-10-02 12:08:51 +08:00
parent d518bb2ceb
commit 7128e865c0
6 changed files with 73 additions and 35 deletions
@@ -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<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 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]
@@ -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<PathBuf, ClientAuthError>
.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<String>) -> 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<String>) -> ClientAuthError {
app_log!("{}", message.into());
ClientAuthError::RuntimeSessionInstallFailed
let detail = message.into();
app_log!("{detail}");
ClientAuthError::RuntimeSessionInstallFailed(RuntimeSessionInstallFailed { detail })
}
/// 服务地址校验失败统一收口成带类型化原因的变体。
@@ -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' };
@@ -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 };
@@ -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 };
@@ -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: {