Merge pull request 'Fix/修复登录用户参数等错误被上报' (#567) from fix/wrong-report into master
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Reviewed-on: #567
This commit was merged in pull request #567.
This commit is contained in:
@@ -128,14 +128,17 @@ test('协议与 Agent 模式是独立字段,可按模型单独选择', async (
|
||||
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
|
||||
await screen.findByDisplayValue('claude-opus-5-5');
|
||||
|
||||
const agentMode = screen.getByLabelText('模型 1 Agent 模式') as HTMLSelectElement;
|
||||
const agentMode = screen.getByLabelText(
|
||||
'模型 1 Agent 模式',
|
||||
) as HTMLSelectElement;
|
||||
const protocol = screen.getByLabelText('模型 1 协议') as HTMLSelectElement;
|
||||
expect(agentMode.value).toBe('cc');
|
||||
expect(protocol.value).toBe('anthropic');
|
||||
// 执行器下拉里不再混入协议取值。
|
||||
expect(
|
||||
Array.from(agentMode.options).map((option) => option.value),
|
||||
).toEqual(['codex', 'cc']);
|
||||
expect(Array.from(agentMode.options).map((option) => option.value)).toEqual([
|
||||
'codex',
|
||||
'cc',
|
||||
]);
|
||||
|
||||
fireEvent.change(agentMode, { target: { value: 'codex' } });
|
||||
fireEvent.change(protocol, { target: { value: 'openai_chat' } });
|
||||
|
||||
@@ -433,6 +433,7 @@ const APP_INVOKE_BARE_CALL_NAMES = new Set([
|
||||
'directInvoke',
|
||||
'invokeInput',
|
||||
'invokeAuthenticatedInput',
|
||||
'invokeClientAuth',
|
||||
'invokeDiagnostic',
|
||||
]);
|
||||
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
//!
|
||||
//! 事件载荷(`thread_manager::wire::DirectTurnFailure`)仍然只有 `{kind, message}` 两个字段:
|
||||
//! 那是**线上协议**,由 [`DirectTurnError::wire_kind`] 与 `Display` 在这一个出口投影出来,不是
|
||||
//! 另一种状态模型。跨进程边界(`#[tauri::command]`)同样只给前端一个字符串:那是**序列化**,
|
||||
//! 由 `Display` 一处生成;Rust 侧任何地方都不再解析这个字符串。
|
||||
//! 另一种状态模型。跨进程边界(`#[tauri::command]`)的**入队失败**载荷是结构化的
|
||||
//! [`DirectTurnEnqueueFailure`](变体 + 一条可展示 `message`);`message` 有两条来源:
|
||||
//! 不需要留痕的失败用 [`DirectTurnError`] 的 `Display`,需要留痕的宿主 / 环境事实改用
|
||||
//! `record_direct_codex_failure` 生成的脱敏诊断收口文案(`direct-codex-failure:v2 …`,同时写
|
||||
//! `.agent/runtime/errors` 与错误上报池)。其余只需要一句文案的出口仍由 `Display` 生成。
|
||||
//! 无论哪条来源,Rust 侧任何地方都不解析这个字符串。
|
||||
//!
|
||||
//! 谁负责产生哪个变体:
|
||||
//! - 命令入口与回合编排(`direct_runtime`):调用级拒绝、阶段失败;
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
//! AGC 认证命令的结构化错误:从命令入口到出口只传这一种错误。
|
||||
//!
|
||||
//! 变体名就是线上的分流键(`type`):前端只按它选通道,**不解析任何文案**,也不对错误文本做匹配。
|
||||
//!
|
||||
//! 顶层只放**调用方要分流的类别**;同一类里可枚举的细分原因收进**类型化 `reason` 字段**,
|
||||
//! 而不是各拆一个变体,也不是字符串。例如服务地址校验的 7 种失败、网络失败的超时/不可达、
|
||||
//! 响应契约的 5 种破损各自只占一个变体:前端 `switch (failure.type)` 选到类别后,再用
|
||||
//! `switch (payload.reason)` 在**类型化**的细分上分流,仍然不碰文案。
|
||||
//!
|
||||
//! Rust **不预拼用户可见文案**:载荷只装原始事实(服务端 400 的原文、HTTP 状态码、本机 IO /
|
||||
//! 网络客户端构建失败的原始错误 `detail`),服务端没给原文就是 `None`;前缀与句式由前端调用方在
|
||||
//! 自己的 catch 分支按当前操作拼接。`detail` 是给调用方分流与诊断用的原始事实,**不直接贴在界面上**;
|
||||
//! 报告侧由 sanitize 把路径等替换成占位符。
|
||||
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// 变体名就是线上的分流键(`type`),带载荷的变体持有同名载荷类型。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))]
|
||||
pub(crate) enum ClientAuthError {
|
||||
// ---- 业务:用户自己能改,调用方给提示,不上报 ----
|
||||
/// 服务地址校验失败(含渠道范围门禁),细分原因见 [`ServerAddressReason`]。
|
||||
ServerAddressRejected(ServerAddressRejected),
|
||||
/// 本地前置校验:手机号为空、超长或不是纯数字(校验器只回 bool,没有更细的事实)。
|
||||
PhoneNumberInvalid,
|
||||
/// 本地前置校验:密码为空。
|
||||
PasswordMissing,
|
||||
/// 本地前置校验:验证码为空。
|
||||
LoginCodeMissing,
|
||||
/// `/api/auth/entry` 返回 400:服务端拒绝本次输入。
|
||||
PasswordLoginRejected(PasswordLoginRejected),
|
||||
/// `/api/auth/entry` 返回 401:手机号或密码错误。
|
||||
PhoneOrPasswordMismatch,
|
||||
/// `/api/auth/phone/send-code` 返回 400:服务端拒绝本次输入。
|
||||
SendCodeRejected(SendCodeRejected),
|
||||
/// `/api/auth/phone/send-code` 返回 429:发送过于频繁。
|
||||
SmsCodeThrottled,
|
||||
/// `/api/auth/phone/login` 返回 400/401:服务端拒绝本次验证码登录。
|
||||
///
|
||||
/// 401 目前只来自「用户不存在」;该路由验证通过后会即时建号,所以这条分支实际很少触发。
|
||||
PhoneCodeLoginRejected(PhoneCodeLoginRejected),
|
||||
|
||||
// ---- 会话:调用方按"未登录"处理,不给用户报错 ----
|
||||
/// 会话路由 401:登录态失效。
|
||||
SessionInvalidated,
|
||||
/// 会话路由 403:当前账号没有执行此操作的权限。
|
||||
PermissionDenied,
|
||||
|
||||
// ---- 系统:调用方处理不了,带上下文重抛 ----
|
||||
/// 连接登录服务的传输层失败,细分原因见 [`AuthNetworkReason`]。
|
||||
AuthNetworkFailure(AuthNetworkFailure),
|
||||
/// 登录服务 5xx。
|
||||
AuthServiceUnavailable(AuthServiceUnavailable),
|
||||
/// 其它未识别的拒绝(未列举的 4xx、登录路由 403 等)。
|
||||
UnexpectedRejection(UnexpectedRejection),
|
||||
/// 登录服务响应的契约破损,细分原因见 [`AuthResponseInvalidReason`]。
|
||||
AuthResponseInvalid(AuthResponseInvalid),
|
||||
/// 本机登录凭据文件读写失败;`detail` 是原始错误,供调用方与诊断用、不直接展示。
|
||||
ClientSessionPersistFailed(ClientSessionPersistFailed),
|
||||
/// 本机运行时会话安装 / 清理失败;`detail` 同上。
|
||||
RuntimeSessionInstallFailed(RuntimeSessionInstallFailed),
|
||||
/// 认证网络客户端构建失败;`detail` 是原始错误。
|
||||
AuthClientInitFailed(AuthClientInitFailed),
|
||||
}
|
||||
|
||||
// ---- 可枚举的细分原因:类型化字段,不是字符串 ----
|
||||
|
||||
/// 服务地址校验失败的具体原因。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))]
|
||||
pub(crate) enum ServerAddressReason {
|
||||
/// 为空或超长。
|
||||
EmptyOrTooLong,
|
||||
/// 不是合法 URL。
|
||||
NotAUrl,
|
||||
/// 带用户名 / 密码。
|
||||
HasCredentials,
|
||||
/// 带路径、查询或 fragment。
|
||||
HasPathOrQueryOrFragment,
|
||||
/// 非本机地址不是 HTTPS。
|
||||
NotHttps,
|
||||
/// scheme 不是 http(s)。
|
||||
UnsupportedScheme,
|
||||
/// 发布构建里不在当前构建渠道范围内。
|
||||
OutsideChannel,
|
||||
}
|
||||
|
||||
/// 连接登录服务失败的具体原因。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))]
|
||||
pub(crate) enum AuthNetworkReason {
|
||||
/// 超时。
|
||||
Timeout,
|
||||
/// DNS / 连接被拒 / 读响应失败等。
|
||||
Unreachable,
|
||||
}
|
||||
|
||||
/// 登录响应契约破损的具体原因。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))]
|
||||
pub(crate) enum AuthResponseInvalidReason {
|
||||
/// 不是合法 JSON。
|
||||
NotJson,
|
||||
/// 不是预期结构(缺字段 / 类型不符 / 凭据格式无效)。
|
||||
InvalidBody,
|
||||
/// 没有下发新的续期凭据。
|
||||
MissingRefreshCookie,
|
||||
/// 没有带上会话主体(用户身份)。
|
||||
MissingUserIdentity,
|
||||
/// 响应体里显式拒绝(`ok: false`)。
|
||||
ServerRejected,
|
||||
}
|
||||
|
||||
// ---- 载荷:只装类型说不出来的事实 ----
|
||||
|
||||
/// 服务地址被拒的具体原因。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))]
|
||||
pub(crate) struct ServerAddressRejected {
|
||||
pub(crate) reason: ServerAddressReason,
|
||||
}
|
||||
|
||||
/// `/api/auth/entry` 返回 400 时服务端给的原文。
|
||||
#[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 PasswordLoginRejected {
|
||||
/// 服务端原文可能缺失:缺失是 `null`,Rust 不编造兜底文案。
|
||||
pub(crate) server_message: Option<String>,
|
||||
}
|
||||
|
||||
/// `/api/auth/phone/send-code` 返回 400 时服务端给的原文。
|
||||
#[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 SendCodeRejected {
|
||||
/// 服务端原文可能缺失:缺失是 `null`,Rust 不编造兜底文案。
|
||||
pub(crate) server_message: Option<String>,
|
||||
}
|
||||
|
||||
/// `/api/auth/phone/login` 返回 400 时服务端给的原文。
|
||||
#[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 PhoneCodeLoginRejected {
|
||||
/// 服务端原文可能缺失:缺失是 `null`,Rust 不编造兜底文案。
|
||||
pub(crate) server_message: Option<String>,
|
||||
}
|
||||
|
||||
/// 连接登录服务的传输层失败原因。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))]
|
||||
pub(crate) struct AuthNetworkFailure {
|
||||
pub(crate) reason: AuthNetworkReason,
|
||||
}
|
||||
|
||||
/// 登录服务 5xx 的状态码。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/services/generated/"))]
|
||||
pub(crate) struct AuthServiceUnavailable {
|
||||
pub(crate) status: u16,
|
||||
}
|
||||
|
||||
/// 其它未识别拒绝的状态码与服务端原文。
|
||||
#[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 UnexpectedRejection {
|
||||
pub(crate) status: u16,
|
||||
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 AuthResponseInvalid {
|
||||
pub(crate) reason: AuthResponseInvalidReason,
|
||||
/// 只有 `serverRejected` 可能带服务端原文;其余是 `null`。
|
||||
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,
|
||||
}
|
||||
|
||||
/// 认证网络客户端构建失败的原始错误明细。
|
||||
#[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 AuthClientInitFailed {
|
||||
pub(crate) detail: String,
|
||||
}
|
||||
|
||||
impl ClientAuthError {
|
||||
/// 会话路由的 401/403 是「登录态失效」:调用方据此清会话、按未登录处理,
|
||||
/// 既不给用户报错,也不进错误报告池。
|
||||
pub(crate) fn is_authority_failure(&self) -> bool {
|
||||
matches!(self, Self::SessionInvalidated | Self::PermissionDenied)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wire_variant_names_are_the_frontend_dispatch_keys() {
|
||||
let cases = [
|
||||
(
|
||||
ClientAuthError::ServerAddressRejected(ServerAddressRejected {
|
||||
reason: ServerAddressReason::NotHttps,
|
||||
}),
|
||||
"serverAddressRejected",
|
||||
),
|
||||
(ClientAuthError::PhoneNumberInvalid, "phoneNumberInvalid"),
|
||||
(ClientAuthError::PasswordMissing, "passwordMissing"),
|
||||
(ClientAuthError::LoginCodeMissing, "loginCodeMissing"),
|
||||
(
|
||||
ClientAuthError::PasswordLoginRejected(PasswordLoginRejected {
|
||||
server_message: Some("x".to_string()),
|
||||
}),
|
||||
"passwordLoginRejected",
|
||||
),
|
||||
(
|
||||
ClientAuthError::PhoneOrPasswordMismatch,
|
||||
"phoneOrPasswordMismatch",
|
||||
),
|
||||
(
|
||||
ClientAuthError::SendCodeRejected(SendCodeRejected {
|
||||
server_message: Some("x".to_string()),
|
||||
}),
|
||||
"sendCodeRejected",
|
||||
),
|
||||
(ClientAuthError::SmsCodeThrottled, "smsCodeThrottled"),
|
||||
(
|
||||
ClientAuthError::PhoneCodeLoginRejected(PhoneCodeLoginRejected {
|
||||
server_message: Some("x".to_string()),
|
||||
}),
|
||||
"phoneCodeLoginRejected",
|
||||
),
|
||||
(ClientAuthError::SessionInvalidated, "sessionInvalidated"),
|
||||
(ClientAuthError::PermissionDenied, "permissionDenied"),
|
||||
(
|
||||
ClientAuthError::AuthNetworkFailure(AuthNetworkFailure {
|
||||
reason: AuthNetworkReason::Timeout,
|
||||
}),
|
||||
"authNetworkFailure",
|
||||
),
|
||||
(
|
||||
ClientAuthError::AuthServiceUnavailable(AuthServiceUnavailable { status: 503 }),
|
||||
"authServiceUnavailable",
|
||||
),
|
||||
(
|
||||
ClientAuthError::UnexpectedRejection(UnexpectedRejection {
|
||||
status: 409,
|
||||
server_message: None,
|
||||
}),
|
||||
"unexpectedRejection",
|
||||
),
|
||||
(
|
||||
ClientAuthError::AuthResponseInvalid(AuthResponseInvalid {
|
||||
reason: AuthResponseInvalidReason::ServerRejected,
|
||||
server_message: None,
|
||||
}),
|
||||
"authResponseInvalid",
|
||||
),
|
||||
(
|
||||
ClientAuthError::ClientSessionPersistFailed(ClientSessionPersistFailed {
|
||||
detail: "x".to_string(),
|
||||
}),
|
||||
"clientSessionPersistFailed",
|
||||
),
|
||||
(
|
||||
ClientAuthError::RuntimeSessionInstallFailed(RuntimeSessionInstallFailed {
|
||||
detail: "x".to_string(),
|
||||
}),
|
||||
"runtimeSessionInstallFailed",
|
||||
),
|
||||
(
|
||||
ClientAuthError::AuthClientInitFailed(AuthClientInitFailed {
|
||||
detail: "x".to_string(),
|
||||
}),
|
||||
"authClientInitFailed",
|
||||
),
|
||||
];
|
||||
for (error, expected_type) in cases {
|
||||
let value = serde_json::to_value(&error).expect("serialize auth error");
|
||||
assert_eq!(
|
||||
value.get("type").and_then(|value| value.as_str()),
|
||||
Some(expected_type)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_variants_serialize_without_payload_fields() {
|
||||
let value =
|
||||
serde_json::to_value(ClientAuthError::LoginCodeMissing).expect("serialize auth error");
|
||||
assert_eq!(value, serde_json::json!({ "type": "loginCodeMissing" }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reason_fields_are_typed_enums_not_strings() {
|
||||
let rejected = serde_json::to_value(ClientAuthError::ServerAddressRejected(
|
||||
ServerAddressRejected {
|
||||
reason: ServerAddressReason::HasPathOrQueryOrFragment,
|
||||
},
|
||||
))
|
||||
.expect("serialize auth error");
|
||||
assert_eq!(rejected["type"], "serverAddressRejected");
|
||||
assert_eq!(rejected["reason"], "hasPathOrQueryOrFragment");
|
||||
|
||||
let network =
|
||||
serde_json::to_value(ClientAuthError::AuthNetworkFailure(AuthNetworkFailure {
|
||||
reason: AuthNetworkReason::Unreachable,
|
||||
}))
|
||||
.expect("serialize auth error");
|
||||
assert_eq!(network["reason"], "unreachable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_variants_keep_machine_facts_and_server_text() {
|
||||
let unavailable = serde_json::to_value(ClientAuthError::AuthServiceUnavailable(
|
||||
AuthServiceUnavailable { status: 503 },
|
||||
))
|
||||
.expect("serialize auth error");
|
||||
assert_eq!(unavailable["type"], "authServiceUnavailable");
|
||||
assert_eq!(unavailable["status"], 503);
|
||||
|
||||
let rejection =
|
||||
serde_json::to_value(ClientAuthError::UnexpectedRejection(UnexpectedRejection {
|
||||
status: 409,
|
||||
server_message: Some("冲突".to_string()),
|
||||
}))
|
||||
.expect("serialize auth error");
|
||||
assert_eq!(rejection["status"], 409);
|
||||
assert_eq!(rejection["serverMessage"], "冲突");
|
||||
|
||||
let invalid =
|
||||
serde_json::to_value(ClientAuthError::AuthResponseInvalid(AuthResponseInvalid {
|
||||
reason: AuthResponseInvalidReason::ServerRejected,
|
||||
server_message: Some("登录服务请求失败".to_string()),
|
||||
}))
|
||||
.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"], "磁盘只读");
|
||||
|
||||
let init = serde_json::to_value(ClientAuthError::AuthClientInitFailed(
|
||||
AuthClientInitFailed {
|
||||
detail: "tls handshake failed".to_string(),
|
||||
},
|
||||
))
|
||||
.expect("serialize auth error");
|
||||
assert_eq!(init["type"], "authClientInitFailed");
|
||||
assert_eq!(init["detail"], "tls handshake failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_session_failures_count_as_authority_failures() {
|
||||
assert!(ClientAuthError::SessionInvalidated.is_authority_failure());
|
||||
assert!(ClientAuthError::PermissionDenied.is_authority_failure());
|
||||
assert!(!ClientAuthError::PhoneOrPasswordMismatch.is_authority_failure());
|
||||
assert!(!ClientAuthError::AuthNetworkFailure(AuthNetworkFailure {
|
||||
reason: AuthNetworkReason::Timeout,
|
||||
})
|
||||
.is_authority_failure());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -79,6 +79,7 @@ mod agent_native_tools;
|
||||
mod analytics;
|
||||
mod asset_generation_tasks;
|
||||
mod assets;
|
||||
mod auth_error;
|
||||
mod auth_session;
|
||||
mod browser;
|
||||
mod builtin_plugins;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,12 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
||||
};
|
||||
|
||||
type RuntimeSettingsSection =
|
||||
'general' | 'workspace' | 'agents' | 'extensions' | 'advanced' | 'about';
|
||||
| 'general'
|
||||
| 'workspace'
|
||||
| 'agents'
|
||||
| 'extensions'
|
||||
| 'advanced'
|
||||
| 'about';
|
||||
|
||||
type RuntimeConfigToast = {
|
||||
tone: 'success' | 'error';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { AuthUser } from '../../../../packages/shared/src/contracts/auth';
|
||||
import { resolveTauriInvoke } from '../app/tauri';
|
||||
import { ClientAuthErrorWrapper } from './clientAuthErrorWrapper';
|
||||
import type { ClientAuthError } from './generated/ClientAuthError';
|
||||
import { subscribeTauriEvent } from './tauriEventSubscription';
|
||||
|
||||
/** Rust 认证态事件:只承载状态投影,不含 token 或 refresh 凭据。 */
|
||||
@@ -7,14 +9,12 @@ export const CLIENT_AUTH_STATE_CHANGED_EVENT = 'agc-client-auth-state-changed';
|
||||
|
||||
export type ClientAuthState =
|
||||
| { status: 'authenticated'; user: AuthUser; apiBaseUrl: string }
|
||||
| { status: 'unauthenticated' }
|
||||
| { status: 'unavailable'; message: string };
|
||||
| { status: 'unauthenticated' };
|
||||
|
||||
export type ClientAuthRefreshResult =
|
||||
| { status: 'refreshed'; user: AuthUser }
|
||||
| { status: 'unauthenticated' }
|
||||
| { status: 'stale' }
|
||||
| { status: 'failed'; message: string; authoritative: boolean };
|
||||
| { status: 'stale' };
|
||||
|
||||
export type ClientLoginCodeResult = {
|
||||
cooldownSeconds: number;
|
||||
@@ -36,46 +36,64 @@ function requireInvoke() {
|
||||
return invoke;
|
||||
}
|
||||
|
||||
export function getClientAuthErrorMessage(error: unknown, fallback: string) {
|
||||
if (error instanceof Error && error.message.trim()) return error.message;
|
||||
const message = String(error ?? '').trim();
|
||||
return message || fallback;
|
||||
/**
|
||||
* 认证命令的统一入口:把 Tauri 的拒绝原样装进已有的 `ClientAuthErrorWrapper`。
|
||||
*
|
||||
* **信任映射,不做运行时形状嗅探**:Rust 与 TS 同包发布,认证命令的拒绝就是 ts-rs 生成的
|
||||
* `ClientAuthError` 判别联合;出现别的形状属于 Tauri / Rust 侧缺陷,调用方 `switch` 的
|
||||
* `default` 分支仍会把它抛出去上报。包装本身不读变体字段、
|
||||
* 不注入上下文、不拼用户可见文案。
|
||||
*/
|
||||
async function invokeClientAuth<T>(
|
||||
command: string,
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
// 认证桥未安装是我们自己的失败关闭错误,不是命令拒绝:放在 try 之外,原样抛出。
|
||||
const invoke = requireInvoke();
|
||||
try {
|
||||
// 不带参数时保持 `invoke(command)` 的单参调用形态,别给命令多塞一个 undefined。
|
||||
return args === undefined
|
||||
? await invoke<T>(command)
|
||||
: await invoke<T>(command, args);
|
||||
} catch (error) {
|
||||
// 薄包装:原样把 Rust 的拒绝装成 JS Error;不读字段、不加字段。
|
||||
throw new ClientAuthErrorWrapper(error as ClientAuthError);
|
||||
}
|
||||
}
|
||||
|
||||
type RustAuthStateView = {
|
||||
status?: string;
|
||||
user?: AuthUser | null;
|
||||
apiBaseUrl?: string | null;
|
||||
errorKind?: string | null;
|
||||
errorMessage?: string | null;
|
||||
};
|
||||
/** Rust 认证态投影,与 `ClientAuthStateView` 一一对应。 */
|
||||
type RustAuthStateView =
|
||||
| { status: 'authenticated'; user: AuthUser; apiBaseUrl: string }
|
||||
| { status: 'unauthenticated' };
|
||||
|
||||
/** Rust 续期结果投影,与 `ClientAuthRefreshView` 一一对应。 */
|
||||
type RustAuthRefreshView =
|
||||
| { status: 'refreshed'; user: AuthUser }
|
||||
| { status: 'unauthenticated' }
|
||||
| { status: 'stale' };
|
||||
|
||||
/**
|
||||
* 恢复登录态。
|
||||
*
|
||||
* 凭据续期、当前用户复核与本机运行时会话安装都在 Rust 内完成;这里只把结果投影成
|
||||
* `authenticated` / `unauthenticated` / `unavailable` 三态,供登录页决定展示分支。
|
||||
* 凭据续期、当前用户复核与本机运行时会话安装都在 Rust 内完成;读状态失败就是命令失败,
|
||||
* 由 `invokeClientAuth` 装进 `ClientAuthErrorWrapper`(`error` 是判别联合),不再有第三态投影。
|
||||
*/
|
||||
export async function readClientAuthState(
|
||||
expectedApiBaseUrl?: string,
|
||||
): Promise<ClientAuthState> {
|
||||
const invoke = requireInvoke();
|
||||
const view = await invoke<RustAuthStateView>('read_client_auth_state', {
|
||||
expectedApiBaseUrl: expectedApiBaseUrl?.trim() || null,
|
||||
});
|
||||
if (view?.status === 'authenticated' && view.user && view.apiBaseUrl) {
|
||||
const view = await invokeClientAuth<RustAuthStateView>(
|
||||
'read_client_auth_state',
|
||||
{
|
||||
expectedApiBaseUrl: expectedApiBaseUrl?.trim() || null,
|
||||
},
|
||||
);
|
||||
if (view.status === 'authenticated') {
|
||||
return {
|
||||
status: 'authenticated',
|
||||
user: view.user,
|
||||
apiBaseUrl: view.apiBaseUrl,
|
||||
};
|
||||
}
|
||||
if (view?.status === 'unavailable') {
|
||||
return {
|
||||
status: 'unavailable',
|
||||
message: view.errorMessage?.trim() || '登录服务暂时不可用,请稍后重试',
|
||||
};
|
||||
}
|
||||
return { status: 'unauthenticated' };
|
||||
}
|
||||
|
||||
@@ -83,8 +101,7 @@ export async function sendClientPhoneLoginCode(
|
||||
phone: string,
|
||||
apiBaseUrl: string,
|
||||
): Promise<ClientLoginCodeResult> {
|
||||
const invoke = requireInvoke();
|
||||
const result = await invoke<{
|
||||
const result = await invokeClientAuth<{
|
||||
cooldownSeconds?: number;
|
||||
expiresInSeconds?: number;
|
||||
}>('send_client_phone_login_code', {
|
||||
@@ -102,8 +119,7 @@ export async function loginClientWithPassword(
|
||||
password: string,
|
||||
apiBaseUrl: string,
|
||||
): Promise<AuthUser> {
|
||||
const invoke = requireInvoke();
|
||||
return invoke<AuthUser>('login_client_with_password', {
|
||||
return invokeClientAuth<AuthUser>('login_client_with_password', {
|
||||
apiBaseUrl,
|
||||
phone: normalizeAuthPhoneInput(phone),
|
||||
password: password.trim(),
|
||||
@@ -115,8 +131,7 @@ export async function loginClientWithPhoneCode(
|
||||
code: string,
|
||||
apiBaseUrl: string,
|
||||
): Promise<AuthUser> {
|
||||
const invoke = requireInvoke();
|
||||
return invoke<AuthUser>('login_client_with_phone_code', {
|
||||
return invokeClientAuth<AuthUser>('login_client_with_phone_code', {
|
||||
apiBaseUrl,
|
||||
phone: normalizeAuthPhoneInput(phone),
|
||||
code: code.trim(),
|
||||
@@ -125,43 +140,23 @@ export async function loginClientWithPhoneCode(
|
||||
|
||||
/** 登出:Rust 负责服务端撤销、凭据清除与本机运行时会话清理。 */
|
||||
export async function logoutClientAuthSession(): Promise<void> {
|
||||
const invoke = requireInvoke();
|
||||
await invoke('logout_client_session');
|
||||
await invokeClientAuth('logout_client_session');
|
||||
}
|
||||
|
||||
export async function refreshClientAuthSession(
|
||||
expectedUserId?: string,
|
||||
): Promise<ClientAuthRefreshResult> {
|
||||
const invoke = requireInvoke();
|
||||
const view = await invoke<{
|
||||
status?: string;
|
||||
user?: AuthUser | null;
|
||||
authoritative?: boolean;
|
||||
errorMessage?: string | null;
|
||||
}>('refresh_client_auth_session', {
|
||||
expectedUserId: expectedUserId?.trim() || null,
|
||||
});
|
||||
switch (view?.status) {
|
||||
case 'refreshed':
|
||||
if (!view.user) {
|
||||
return {
|
||||
status: 'failed',
|
||||
message: '刷新登录状态失败',
|
||||
authoritative: false,
|
||||
};
|
||||
}
|
||||
return { status: 'refreshed', user: view.user };
|
||||
case 'unauthenticated':
|
||||
return { status: 'unauthenticated' };
|
||||
case 'stale':
|
||||
return { status: 'stale' };
|
||||
default:
|
||||
return {
|
||||
status: 'failed',
|
||||
message: view?.errorMessage?.trim() || '刷新登录状态失败',
|
||||
authoritative: view?.authoritative === true,
|
||||
};
|
||||
const view = await invokeClientAuth<RustAuthRefreshView>(
|
||||
'refresh_client_auth_session',
|
||||
{ expectedUserId: expectedUserId?.trim() || null },
|
||||
);
|
||||
if (view.status === 'refreshed') {
|
||||
return { status: 'refreshed', user: view.user };
|
||||
}
|
||||
if (view.status === 'unauthenticated') {
|
||||
return { status: 'unauthenticated' };
|
||||
}
|
||||
return { status: 'stale' };
|
||||
}
|
||||
|
||||
/** 订阅 Rust 认证态事件,返回幂等释放函数。 */
|
||||
@@ -172,7 +167,7 @@ export function subscribeClientAuthState(
|
||||
CLIENT_AUTH_STATE_CHANGED_EVENT,
|
||||
(event) => {
|
||||
const view = event.payload;
|
||||
if (view?.status === 'authenticated' && view.user && view.apiBaseUrl) {
|
||||
if (view.status === 'authenticated') {
|
||||
listener({
|
||||
status: 'authenticated',
|
||||
user: view.user,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ClientAuthError } from './generated/ClientAuthError';
|
||||
|
||||
/**
|
||||
* 错误上报上下文:`source` 是错误池的一级维度,`action` / `page` 用于细分指纹。
|
||||
*
|
||||
* 含义与 [`captureClientError`](./errorReporting.ts) 的入参完全一致。
|
||||
*/
|
||||
export type ClientErrorReportContext = {
|
||||
source: string;
|
||||
action?: string;
|
||||
page?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 认证命令失败的 JS 侧载体:`error` 就是 Rust 的结构化拒绝(ts-rs 生成的 `ClientAuthError`
|
||||
* 判别联合)。
|
||||
*
|
||||
* 构造时把整份载荷 `JSON.stringify` 进 `Error.message`:结构化拒绝是普通对象,序列化后
|
||||
* 上报事件拿到的是机器事实(变体名与载荷),而不是 `[object Object]`;不读任何变体字段、
|
||||
* 不拼用户文案、不注入 `source` / `action`。分流只看类型化的变体,
|
||||
* **不要用文案判断**;上报上下文由调用 `captureClientError` 时的显式入参决定。
|
||||
*
|
||||
* Tauri 缺陷抛出的真 `Error` 序列化只有 `{}`,但上报链路对真 `Error` 优先用其自身
|
||||
* message/stack,不受影响。
|
||||
*/
|
||||
export class ClientAuthErrorWrapper extends Error {
|
||||
readonly error: ClientAuthError;
|
||||
|
||||
constructor(error: ClientAuthError) {
|
||||
super(JSON.stringify(error));
|
||||
this.name = 'ClientAuthErrorWrapper';
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import {
|
||||
ClientAuthErrorWrapper,
|
||||
type ClientErrorReportContext,
|
||||
} from './clientAuthErrorWrapper';
|
||||
import {
|
||||
ackErrorReports,
|
||||
getPendingErrorReports,
|
||||
@@ -21,16 +25,6 @@ export type DiagnosticLogFile = { name: string; content: string };
|
||||
|
||||
type WebviewLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'log';
|
||||
|
||||
export function shouldCaptureClientError(error: unknown) {
|
||||
if (!error || typeof error !== 'object') return true;
|
||||
const candidate = error as { status?: unknown; networkError?: unknown };
|
||||
if (candidate.networkError === true) return true;
|
||||
if (typeof candidate.status === 'number') {
|
||||
return candidate.status === 408 || candidate.status >= 500;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function invokeDiagnostic<T>(
|
||||
invokeFn: (command: string, args?: Record<string, unknown>) => Promise<T>,
|
||||
command: string,
|
||||
@@ -84,11 +78,23 @@ export function normalizeDiagnosticText(value: string) {
|
||||
|
||||
export async function captureClientError(
|
||||
error: unknown,
|
||||
context: { source?: string; action?: string; page?: string } = {},
|
||||
context: Partial<ClientErrorReportContext> = {},
|
||||
) {
|
||||
const errorValue = error instanceof Error ? error : new Error(String(error));
|
||||
// ClientAuthErrorWrapper 构造时已把整份载荷序列化进 message:结构化拒绝在这里直接作为事件
|
||||
// 文案上报;真 Error 仍优先用它自己的 message / stack。
|
||||
const actionError = error instanceof ClientAuthErrorWrapper ? error : null;
|
||||
// 类型上 `error` 是 ClientAuthError;Tauri 在映射外抛 Error 属于它的缺陷,这里按 unknown 兜底取文案/栈。
|
||||
const carried: unknown = actionError?.error;
|
||||
let errorValue: Error;
|
||||
if (carried instanceof Error) {
|
||||
errorValue = carried;
|
||||
} else if (error instanceof Error) {
|
||||
errorValue = error;
|
||||
} else {
|
||||
errorValue = new Error(String(error));
|
||||
}
|
||||
const message = errorValue.message || '未知客户端错误';
|
||||
const stack = errorValue.stack ? errorValue.stack.slice(0, 8_000) : undefined;
|
||||
const stack = errorValue.stack?.slice(0, 8_000);
|
||||
return reportClientError({
|
||||
source: context.source ?? 'client',
|
||||
message,
|
||||
|
||||
@@ -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 AuthClientInitFailed = { detail: string };
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AuthNetworkReason } from './AuthNetworkReason';
|
||||
|
||||
/**
|
||||
* 连接登录服务的传输层失败原因。
|
||||
*/
|
||||
export type AuthNetworkFailure = { reason: AuthNetworkReason };
|
||||
@@ -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 AuthNetworkReason = 'timeout' | 'unreachable';
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AuthResponseInvalidReason } from './AuthResponseInvalidReason';
|
||||
|
||||
/**
|
||||
* 登录响应契约破损的原因与服务端原文。
|
||||
*/
|
||||
export type AuthResponseInvalid = {
|
||||
reason: AuthResponseInvalidReason;
|
||||
/**
|
||||
* 只有 `serverRejected` 可能带服务端原文;其余是 `null`。
|
||||
*/
|
||||
serverMessage: string | null;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 登录响应契约破损的具体原因。
|
||||
*/
|
||||
export type AuthResponseInvalidReason =
|
||||
| 'notJson'
|
||||
| 'invalidBody'
|
||||
| 'missingRefreshCookie'
|
||||
| 'missingUserIdentity'
|
||||
| 'serverRejected';
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 登录服务 5xx 的状态码。
|
||||
*/
|
||||
export type AuthServiceUnavailable = { status: number };
|
||||
@@ -0,0 +1,35 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AuthClientInitFailed } from './AuthClientInitFailed';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 变体名就是线上的分流键(`type`),带载荷的变体持有同名载荷类型。
|
||||
*/
|
||||
export type ClientAuthError =
|
||||
| ({ type: 'serverAddressRejected' } & ServerAddressRejected)
|
||||
| { type: 'phoneNumberInvalid' }
|
||||
| { type: 'passwordMissing' }
|
||||
| { type: 'loginCodeMissing' }
|
||||
| ({ type: 'passwordLoginRejected' } & PasswordLoginRejected)
|
||||
| { type: 'phoneOrPasswordMismatch' }
|
||||
| ({ type: 'sendCodeRejected' } & SendCodeRejected)
|
||||
| { type: 'smsCodeThrottled' }
|
||||
| ({ type: 'phoneCodeLoginRejected' } & PhoneCodeLoginRejected)
|
||||
| { type: 'sessionInvalidated' }
|
||||
| { type: 'permissionDenied' }
|
||||
| ({ type: 'authNetworkFailure' } & AuthNetworkFailure)
|
||||
| ({ type: 'authServiceUnavailable' } & AuthServiceUnavailable)
|
||||
| ({ type: 'unexpectedRejection' } & UnexpectedRejection)
|
||||
| ({ type: 'authResponseInvalid' } & AuthResponseInvalid)
|
||||
| ({ type: 'clientSessionPersistFailed' } & ClientSessionPersistFailed)
|
||||
| ({ type: 'runtimeSessionInstallFailed' } & RuntimeSessionInstallFailed)
|
||||
| ({ type: 'authClientInitFailed' } & 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,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/entry` 返回 400 时服务端给的原文。
|
||||
*/
|
||||
export type PasswordLoginRejected = {
|
||||
/**
|
||||
* 服务端原文可能缺失:缺失是 `null`,Rust 不编造兜底文案。
|
||||
*/
|
||||
serverMessage: string | null;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/phone/login` 返回 400 时服务端给的原文。
|
||||
*/
|
||||
export type PhoneCodeLoginRejected = {
|
||||
/**
|
||||
* 服务端原文可能缺失:缺失是 `null`,Rust 不编造兜底文案。
|
||||
*/
|
||||
serverMessage: string | null;
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/phone/send-code` 返回 400 时服务端给的原文。
|
||||
*/
|
||||
export type SendCodeRejected = {
|
||||
/**
|
||||
* 服务端原文可能缺失:缺失是 `null`,Rust 不编造兜底文案。
|
||||
*/
|
||||
serverMessage: string | null;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 服务地址校验失败的具体原因。
|
||||
*/
|
||||
export type ServerAddressReason =
|
||||
| 'emptyOrTooLong'
|
||||
| 'notAUrl'
|
||||
| 'hasCredentials'
|
||||
| 'hasPathOrQueryOrFragment'
|
||||
| 'notHttps'
|
||||
| 'unsupportedScheme'
|
||||
| 'outsideChannel';
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ServerAddressReason } from './ServerAddressReason';
|
||||
|
||||
/**
|
||||
* 服务地址被拒的具体原因。
|
||||
*/
|
||||
export type ServerAddressRejected = { reason: ServerAddressReason };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user