本机IO失败载荷恢复detail
- 错误枚举:clientSessionPersistFailed / runtimeSessionInstallFailed 恢复成带 detail 的载荷变体,重新导出两个 ts-rs 绑定文件 - auth_session:IO 失败把原始 error 放进 detail 的同时,仍记一行本地日志 - 前端维持不展示 detail,继续用本操作的固定文案(登录检查 / 发码 / 登录) - carrier 用例改回对两个载荷变体 as 取 detail,钉住"带载荷变体先取具名类型"
This commit is contained in:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user