From 35f10c205158d043b47bbfe82359e34ebaa3da21 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 01:11:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=8C=85=E8=A3=85=E6=94=B9=E4=B8=BA=E4=BF=A1?= =?UTF-8?q?=E4=BB=BB=20ts-rs=20=E6=98=A0=E5=B0=84=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=81=9A=E8=BF=90=E8=A1=8C=E6=97=B6=E5=BD=A2=E7=8A=B6=E5=97=85?= =?UTF-8?q?=E6=8E=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ADR §2 去掉 type 存在性检查:Rust 与 TS 同包发布、形状由 ts-rs 保证,异常形状属 Tauri/Rust 缺陷,仍由 switch 的 default 抛出去上报 - requireInvoke 移到 try 之外,认证桥未安装的错误保持原样抛出 - Error.message 只从拒绝值读可展示字符串,不是分流判据 - ADR §1 记录保留 19 个具名载荷类型的理由:它们是每个分支 as 的目标;内联 struct 变体虽能少 19 个文件,但分支就拿不到可 as 的具名类型 --- ...GC认证失败的JS侧载体与抛出时机-2026-10-01.md | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md b/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md index 9763dd0fa..ece808530 100644 --- a/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md +++ b/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md @@ -16,7 +16,7 @@ Rust 已经返回结构化错误,但 Tauri 的 `invoke` 拒绝值是**普通 上报链路的 `error instanceof Error` 判断会把它降级成 `new Error(String(error))` (`[object Object]`),文案与类型一起丢掉。 - 前一版在渲染层加了 `isClientAuthError` / `getClientAuthErrorMessage` / - `presentAuthFailure` 三层:形状读取、文案回落、分类提示。它们既不是类型事实源,又在 + `presentAuthFailure` 三层:形状读取、文案回落、分类提示(均已删除)。它们既不是类型事实源,又在 "取文案"里悄悄承担了"要不要上报"的判断,与"由调用方判定"的口径冲突。 ## 决策 @@ -36,6 +36,10 @@ class ClientAuthFailure extends ClientActionError { 上报方不需要知道上游是谁。 - `payload` 是完整的结构化拒绝(判别联合),不是再抄一份的派生字段;`payload.type` 就是分流键。**不再新增 `kind` / `notice` / `severity` 之类的派生值。** +- 19 个具名载荷类型是有意保留的:它们是 §3 每个 `case` 里 `as X` 的目标,也正是"不要假设 + 所有变体字段相同"的落点。改成内联 struct 变体确实能让 ts-rs 把字段内联进联合成员、少掉 + 19 个生成文件,但每个分支就再也拿不到可 `as` 的具名类型(只能手写内联对象类型,等于放弃 + ts-rs)。本 ADR 选择保留具名载荷。 ### 2. 一个包装函数:`invokeClientAuth` @@ -43,18 +47,29 @@ class ClientAuthFailure extends ClientActionError { ```ts async function invokeClientAuth(command, args): Promise { + const invoke = requireInvoke(); // 认证桥未装:我们自己的失败关闭错误,原样抛出 try { - return await requireInvoke()(command, args); + return await invoke(command, args); } catch (error) { - const rejection = error as { type?: unknown }; - if (typeof rejection?.type !== 'string') throw error; // 非结构化:原样抛出 - throw new ClientAuthFailure(...); + // 信任 tauri + ts-rs 的映射:认证命令的拒绝就是 ClientAuthError,不做运行时形状校验。 + const rejection = error as { message?: unknown }; + throw new ClientAuthFailure( + typeof rejection.message === 'string' ? rejection.message : '', + error as ClientAuthError, + command, + error, + ); } } ``` -- `type` 是 `string` 即认为是 Rust 结构化拒绝:形状由 ts-rs 保证,**不校验字段名与文案**。 -- 非结构化拒绝原样抛出:那是 Tauri / JS 运行时自己的错误,不属于认证命令契约。 +- **不做运行时形状嗅探**:不再检查 `type` 存不存在。Rust 与 TS 同包发布,形状由 ts-rs 保证; + 出现别的形状属于 Tauri / Rust 侧的缺陷,`switch` 的 `default` 分支仍会把它抛出去上报,不会 + 静默吞掉——只是不再在包装层替 Tauri 兜底。 +- `requireInvoke()` 放在 `try` 之外:认证桥未安装是我们自己的失败关闭错误,不是命令拒绝,保持 + 原样抛出(`需要在 Tauri App 内登录`)。 +- `Error.message` 只从拒绝值里读一个可展示字符串(`typeof === 'string'`),**不是分流判据**; + 分流永远只按 `payload.type`。 - 该包装是"Rust 结构化错误 → JS 错误对象"的唯一转换点:不做分类、不读文案判断、不兜底文案。 ### 3. 判定只写在 catch 子句里,用具体变体