From 478b9c989176de9e9fba1fe4bb3eb9800e131508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 1 Oct 2026 20:33:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=85=88=E8=A1=8C=EF=BC=9AAG?= =?UTF-8?q?C=20=E8=AE=A4=E8=AF=81=E5=A4=B1=E8=B4=A5=E7=9A=84=20JS=20?= =?UTF-8?q?=E4=BE=A7=E8=BD=BD=E4=BD=93=E4=B8=8E=E6=8A=9B=E5=87=BA=E6=97=B6?= =?UTF-8?q?=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ADR:认证命令统一经 invokeClientAuth 把结构化拒绝转成 ClientAuthFailure(payload 判别联合);判定只写在 catch 子句里,系统变体原样抛出经 unhandledrejection 入池,default: expectNever 编译期挡住漏接变体 - 修订【ADR】AGC命令错误结构化与错误报告口径 §3 与后果:撤销"未识别变体上调是故意的",改为编译期穷尽 - 技术方案同步认证命令的封装形态与两个 ADR 链接 - decision-log / pitfalls 记录第二轮口径:删除形状读取与文案回落层、Rust 失败不再降级成字符串 - docs/README.md 登记新 ADR --- docs/README.md | 1 + ...GC命令错误结构化与错误报告口径-2026-10-01.md | 22 ++-- ...GC认证失败的JS侧载体与抛出时机-2026-10-01.md | 121 ++++++++++++++++++ .../shared-memory/decision-log.md | 4 + docs/project-memory/shared-memory/pitfalls.md | 9 +- ...€术方案】AGC错误报告与诊断上传-2026-08-31.md | 4 +- 6 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md diff --git a/docs/README.md b/docs/README.md index 536b1346c..07278dae9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -52,6 +52,7 @@ - [DirectProject 命令接单化实施计划](./technical/【实施计划】DirectProject命令接单化-2026-09-23.md):四步落地顺序、每步不变式与验收;四步均已落地。 - [DirectProject 命令入队化与待发消息队列归宿主](./adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md):命令只负责入队,放行归 Thread Manager;待发消息队列作为运行态事件归宿主、前端只投影;CLI 直连入口与调用身份守卫一并退役。 - [AGC 命令错误结构化与错误报告口径](./adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md):AGC 命令失败按具体变体建模并用 ts-rs 导出,前端按变体分流、不匹配文案;报告池只收没人处理的错误。 +- [AGC 认证失败的 JS 侧载体与抛出时机](./adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md):认证命令统一经 `invokeClientAuth` 把结构化拒绝转成 `ClientAuthFailure`(`payload` 判别联合);判定只写在 catch 子句里,系统变体原样抛出经 `unhandledrejection` 入池,`default: expectNever` 编译期挡住漏接变体。 - [DirectProject 命令入队化与待发消息队列归宿主实施计划](./technical/【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md):五步落地顺序、每步不变式与验收;待实施。 - [GameAgent 对话工具调用卡片](./technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md):把右侧对话里的执行命令 / 写文件投影成 Codex 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。 - [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。 diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index 8bf16e0d7..a580d5b04 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -59,18 +59,16 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 (`shouldCaptureClientError` 只认测试构造过、生产代码从不产生的 `{status}` / `{networkError}` 形状,随本 ADR 删除);4xx 一律不报,交给上层调用方。 -### 3. 前端按变体分流,认不出就交池 +### 3. 前端按变体分流(2026-10-01 修订) -- `isClientAuthError` 只做形状读取(`type` 是稳定判别键),**不做分类、不产出派生值**。 -- 调用方自己 `switch (error.type)`,逐个列出具体变体:业务输入 / 会话变体给提示,`default`(系统变体 - 与 Rust 新增而界面没接的变体)带上文交池。判据是具体变体名,不是聚合出来的类别,也不是文案匹配; - 每个 `case` 里 `error` 已窄化成 §1 的具名载荷类型(如 `PhoneNumberInvalid`)。 -- 认不出、系统类、非结构化拒绝 → 调用方包成 `ClientActionError(message, context, cause)` 交给 - `captureClientError`:`instanceof` 解包 `context` 与 `cause`,指纹/展示字段与今天一致。 - **不把 rejection 留在没人接手的 Promise 上**:`onSubmit` / `onClick` 这类 `void` 掉的 handler 抛错 - 最终以 `unhandledrejection` 结算,生产 WebView 里也能入池,但在 jsdom 下既不触发 `window` - 的 `unhandledrejection` 事件、又会让 `vitest run` 以 unhandled error 失败;调用方判定完直接 - 交给错误池,语义相同、可断言。全局 handler 仍保留同一套解包,接住真正漏出的 `ClientActionError`。 +- 本节原先的"调用方用 `isClientAuthError` 读形状、`switch (error.type)` 分流、`default` 交池"已被 + [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](./【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md) + 取代:`invokeClientAuth` 把结构化拒绝转成 `ClientAuthFailure`(`payload` 是判别联合),判定只写在 + catch 子句里,每个 `case` 用 `as` 取具名载荷类型。 +- "未识别变体上调是**故意**的"不再成立:`default` 改为 `expectNever(error.payload)`,漏接变体是 + **编译错误**,不再是运行时报进池。 +- 系统变体与非结构化拒绝仍由调用方原样 `throw`,经全局 `unhandledrejection` 入池; + `captureClientError` 的 `instanceof ClientActionError` 解包保持不变。 - 删除 `shouldCaptureClientError`:不再存在"叶子自己判定要不要报"的口径。 ### 4. 报告面板与通知行为不变 @@ -84,7 +82,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 及其共用链路(`request_auth` / `map_auth_failure` / `response_data` / `network_error_message`)全量改为 `Result<_, ClientAuthError>`;`read_client_auth_state`、`refresh_client_auth_session`、 `logout_client_session` 的失败面同步结构化(会话 401/403 仍是"未登录"路径,不是错误)。 -- 未识别变体上调是**故意**的:Rust 与 TS 同包发布,"认不出"意味着有人加了变体忘了接界面,属于缺陷。 +- 未识别变体不再靠运行时"上调"兜底:前端 switch 必须列全变体,靠 `expectNever` 在编译期挡住漏接。 - 仍保留的显式采集点(`captureAgentRuntimeError`、`ResourceReferenceInput` 偏好写盘、`invokeDiagnostic`) 在后续变更里按同一口径重抛/删除,本 ADR 不改它们的行为。 diff --git a/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md b/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md new file mode 100644 index 000000000..5bf93e66c --- /dev/null +++ b/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md @@ -0,0 +1,121 @@ +# 【ADR】AGC 认证失败的 JS 侧载体与抛出时机 + +状态:已接受(2026-10-01 落地,实施顺序见同日的决策记录与 +[`【技术方案】AGC错误报告与诊断上传-2026-08-31`](../technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md)) + +前置:[`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](./【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) +已经把 Rust 侧的认证命令失败建模成 `ClientAuthError`,ts-rs 生成 +`{ type: 'x' } & X` 的判别联合。本 ADR 只解决这份结构化拒绝到了 WebView 之后 +"怎么传、谁来判、什么时候抛"。 + +## 背景 + +Rust 已经返回结构化错误,但 Tauri 的 `invoke` 拒绝值是**普通对象**,不是 `Error`: + +- 调用方 `catch (error)` 拿到的是 `{ type, message, ... }`,没有栈。原样 `throw` 它, + 上报链路的 `error instanceof Error` 判断会把它降级成 `new Error(String(error))` + (`[object Object]`),文案与类型一起丢掉。 +- 前一版在渲染层加了 `isClientAuthError` / `getClientAuthErrorMessage` / + `presentAuthFailure` 三层:形状读取、文案回落、分类提示。它们既不是类型事实源,又在 + "取文案"里悄悄承担了"要不要上报"的判断,与"由调用方判定"的口径冲突。 + +## 决策 + +### 1. 一个 JS 载体类:`ClientAuthFailure`,结构化字段名 `payload` + +```ts +class ClientAuthFailure extends ClientActionError { + readonly payload: ClientAuthError; // ts-rs 判别联合,payload.type 是分流键 +} +``` + +- 类名不叫 `ClientAuthError`:那个名字已经被 ts-rs 生成物占用;JS 类与生成类型同名会让 + 两个事实源混在一起。 +- 它继承 `ClientActionError`,所以 `captureClientError` 能用 `instanceof` 解包 + `source: 'auth'`、`action: 命令名`(错误池指纹维度)与 `cause: 原始拒绝值`。 + 上报方不需要知道上游是谁。 +- `payload` 是完整的结构化拒绝(判别联合),不是再抄一份的派生字段;`payload.type` + 就是分流键。**不再新增 `kind` / `notice` / `severity` 之类的派生值。** + +### 2. 一个包装函数:`invokeClientAuth` + +`clientAuth.ts` 里所有认证命令都经它调用: + +```ts +async function invokeClientAuth(command, args): Promise { + try { + return await requireInvoke()(command, args); + } catch (error) { + const rejection = error as { type?: unknown }; + if (typeof rejection?.type !== 'string') throw error; // 非结构化:原样抛出 + throw new ClientAuthFailure(...); + } +} +``` + +- `type` 是 `string` 即认为是 Rust 结构化拒绝:形状由 ts-rs 保证,**不校验字段名与文案**。 +- 非结构化拒绝原样抛出:那是 Tauri / JS 运行时自己的错误,不属于认证命令契约。 +- 该包装是"Rust 结构化错误 → JS 错误对象"的唯一转换点:不做分类、不读文案判断、不兜底文案。 + +### 3. 判定只写在 catch 子句里,用具体变体 + +```ts +catch (error) { + if (!(error instanceof ClientAuthFailure)) throw error; + switch (error.payload.type) { + case 'phoneNumberInvalid': { + const payload = error.payload as PhoneNumberInvalid; + setLoginStatus(payload.message); + break; + } + // ... 每个业务 / 会话变体一个分支 + case 'authNetworkUnavailable': + // ... 系统变体逐个列出后原样抛出 + throw error; + default: + expectNever(error.payload); + } +} +``` + +- **每个 `case` 用 `as` 取自己的具名载荷类型**,不写 `error.payload.message` 这种跨变体的 + 通用读取;等价于 Java 的 `catch (PhoneNumberInvalid e)`。 +- 业务 / 会话变体:把载荷自带的 `message` 原样交给用户,**不加兜底文案**(那就是 Rust 生成的 + 那一份)。 +- 系统变体:调用方处理不了,**原样 `throw`**。`onSubmit` / `onClick` 这类 `void` 掉的 handler + 抛出的拒绝最终以 `unhandledrejection` 结算,由全局 handler 交给错误池。 +- `default: expectNever(error.payload)`(`expectNever(value: never)`)让"Rust 加了变体而这里 + 没接"变成**编译错误**。这是上一版"未识别变体上调是故意的"的替代方案:判据从运行时前移到 + 编译期。 +- 不把这段 switch 抽成 presenter / helper 函数:判定必须发生在 catch 里,包装函数只负责 + "把结构化拒绝转成 JS 错误"。 + +### 4. Rust 侧失败不再降级成字符串 + +结构化必须一路到底:`refresh_session_inner` 的非权威失败直接返回 `Err(ClientAuthError)`, +`ClientAuthStateView` / `ClientAuthRefreshView` 不再有 `errorMessage` 字段,续期结果不再有 +`failed` 状态(`authoritative` 只在"未登录"上为 true,`failed` 恒为 false,删除它不丢信息)。 + +- `ClientAuthState` 收敛为 `authenticated | unauthenticated`:读状态失败就是命令失败,由 + `invokeClientAuth` 转成 `ClientAuthFailure`,不再有第三种 "unavailable 投影"。 +- `ClientAuthRefreshResult` 收敛为 `refreshed | unauthenticated | stale`。 + +## 后果与边界 + +- 新增认证命令或新增 `ClientAuthError` 变体,必须同时改两个 catch 的 switch,否则 `tsc` 失败。 +- `platformSession` 续期失败继续按"网络类失败不降级身份、不标权威失败"处理 + (`authoritative: false`),与旧 `failed` 分支语义一致。 +- 用户可见行为:业务拒绝仍是登录页一行提示;系统失败仍是错误池事件 + 「发现问题」通知。 +- 全局 `unhandledrejection` 是系统变体的唯一出口,调用方不再直接调 `captureClientError`。 + +## 验收 + +```text +npm run ai-game-creator-shell:typecheck +npx vitest run apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx apps/ai-game-creator-shell/tests/clientAuthHost.test.ts apps/ai-game-creator-shell/tests/platformSession.test.ts +cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml auth_error +npm run check:generated-bindings +npm run check:doc-index +npm run check:encoding +git diff --check +``` diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 673a0de80..63db45b0f 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -8,6 +8,10 @@ - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{auth_error.rs,auth_session.rs}`、`apps/ai-game-creator-shell/src/services/{clientAuthError.ts,clientActionError.ts,errorReporting.ts,clientAuth.ts}`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src/services/generated/`(ts-rs 生成:`ClientAuthError.ts` + 每个变体一个载荷文件)。 - 决策(补充):TS 形状用**每个变体一个具名载荷类型**——Rust 枚举是 newtype 变体持有同名 `#[ts(export)]` 结构体,ts-rs 生成 `{ type: 'x' } & X` 与 `src/services/generated/X.ts`,前端 `switch (error.type)` 的每个分支直接拿到具名类型。不允许在前端手写这层类型,也不再包派生分类 / 提示文案函数(`clientAuthErrorKind`、`clientAuthErrorNotice`、`resolveClientAuthFailure` 已删除)。 - 验证:见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 的验收清单;关键判据是"登录 400/401 业务变体不产生 `report_client_error`、不弹「发现问题」"。 +- 决策(2026-10-01,JS 侧载体与抛出时机):认证命令统一经 `invokeClientAuth(command, args)` 调用;结构化拒绝转成 `ClientAuthFailure`(`extends ClientActionError`,`readonly payload` 是 ts-rs 判别联合,`context = { source: 'auth', action: command }`,`cause` 是原始拒绝值),非结构化拒绝原样抛出。形状读取 / 文案回落 / 提示分类三层(`isClientAuthError`、`getClientAuthErrorMessage`、`presentAuthFailure`)全部删除。 +- 决策(2026-10-01,判定位置与出口):要不要上报只由 catch 子句里的 `switch (error.payload.type)` 判,每个 `case` 用 `as` 取具名载荷类型;业务 / 会话变体把载荷自带 `message` 原样给用户(无兜底文案),系统变体 `throw error` 经全局 `unhandledrejection` 入池(`captureClientError` 用 `instanceof` 解包上下文),`default: expectNever(error.payload)` 让漏接变体编译失败。取代"未识别变体上调是故意的"。 +- 决策(2026-10-01,Rust 侧不再降级):`refresh_session_inner` 的非权威失败直接 `Err(ClientAuthError)`,`ClientAuthStateView` / `ClientAuthRefreshView` 删除 `errorMessage`,续期结果删除 `failed`;`ClientAuthState` 收敛为 `authenticated | unauthenticated`,`ClientAuthRefreshResult` 收敛为 `refreshed | unauthenticated | stale`。 +- 影响范围(2026-10-01 第二轮):`apps/ai-game-creator-shell/src/services/{clientAuthFailure.ts,clientAuth.ts,platformSession.ts}`(`clientAuthError.ts` 删除)、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、对应 vitest 用例。 ## 2026-10-01 DirectProject 审批拒绝原因留痕 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 54a4d1fcc..a9830eed7 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6,10 +6,11 @@ - **现象**:登录页密码输错(或密码长度不合规)后弹出「发现问题」,报告面板「错误事件(2)」列出 `密码长度需要在 6 到 128 位之间 — auth · 1 次` 与 `手机号或密码错误 — auth · 1 次`,默认全选,与 react-render / 5xx / agent-runtime 终态失败视觉等价。 - **原因**:① 登录已下沉 Rust,`login_client_with_password` 等命令失败返回 `Err(String)`,Tauri 以**裸字符串**拒绝 `invoke`,前端拿不到任何类型信息;② `shouldCaptureClientError` 对非 object 值走默认 `return true`,`handleLoginSubmit` 的 catch 把预期业务拒绝报进了错误池。技术方案里"预期 4xx 登录/鉴权失败不进池"的口径早就成立,是错误通道的实现方式违背了它。 -- **处理(现行口径)**:命令错误一律按具体变体结构化(`Result<_, ClientAuthError>` + ts-rs 导出),UI 调用方按 `type` 分流:认得的业务变体只给用户反馈,系统变体/未识别变体/非结构化拒绝由调用方包成 `ClientActionError` 交给错误池;删除 `shouldCaptureClientError`。详见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md)。 -- **判据/取证**:`npx vitest run apps/ai-game-creator-shell/tests/errorReporting.test.ts`——登录返回结构化业务变体时 `report_client_error` 不被调用;系统变体带上文只上报一次且 `source=auth`。Rust 侧 `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml auth_error` 钉住变体 `type` 与 400/401/429/5xx/网络映射。 -- **形状约定**:`ClientAuthError` 的每个变体在 TS 里是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体)。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律 `switch (error.type)`,每个分支里 `error` 已经窄化成具名载荷类型。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物随后交给 prettier。 -- **关联**:`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、`apps/ai-game-creator-shell/src/services/errorReporting.ts`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`。 +- **处理(现行口径)**:命令错误一律按具体变体结构化(`Result<_, ClientAuthError>` + ts-rs 导出),认证命令统一经 `invokeClientAuth` 调用:结构化拒绝转成 `ClientAuthFailure`(`payload` 是判别联合),UI 在 catch 里按具体变体分流——认得的业务 / 会话变体只给用户反馈,系统变体原样 `throw` 经 `unhandledrejection` 入池,非结构化拒绝原样抛出;删除 `shouldCaptureClientError`。详见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 与 [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](../../adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md)。 +- **判据/取证**:`npx vitest run apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx`——登录返回结构化业务变体时 `report_client_error` 不被调用;系统变体只上报一次且 `source=auth`(`ClientAuthFailure.context.action` 是命令名)。Rust 侧 `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml auth_error` 钉住变体 `type` 与 400/401/429/5xx/网络映射。 +- **形状约定**:`ClientAuthError` 的每个变体在 TS 里是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体)。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律写在 catch 子句里:`switch (error.payload.type)`,每个 `case` 用 `as` 取具名载荷类型,`default: expectNever(error.payload)` 保证漏接变体编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物随后交给 prettier。 +- **Rust 侧不得把结构化错误降级成字符串**:`refresh_session_inner` 的非权威失败直接返回 `Err(ClientAuthError)`,视图不带 `errorMessage`;一旦折成 `String`,前端就只能拿文案判断,变体信息永久丢失。 +- **关联**:`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、`apps/ai-game-creator-shell/src/services/{clientAuthFailure.ts,clientAuth.ts,errorReporting.ts,platformSession.ts}`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`。 ## 2026-09-30 构建期 staging 撞上不装 npm 依赖的 Linux 门禁:AGC 壳 Rust lane 全红 diff --git a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md index fae769780..73c35757a 100644 --- a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md +++ b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md @@ -8,10 +8,10 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事 - 诊断 URL 保留可定位的 API 路由路径,隐藏 origin、URL 账号密码、查询参数、fragment 和路径中的敏感标识;普通资源 URL 与本地文件路径继续隐藏。网络错误、HTTP 错误与响应体超时均应带安全路由,不能只剩 ``。历史已经脱敏的归档不推测或补造原路由。 -- 报告池只收**没有任何调用方处理**的错误:React render error、`window.onerror`、`unhandledrejection`,以及调用方判定为真故障后带上下文交给错误池(`ClientActionError`)的错误。Agent Runtime 的终态失败、预算耗尽和启动确认失败由 Rust 失败投影统一入池;Direct Codex 与专业 Agent 的前台裸 Tauri invoke catch 作为补充入口,重复事件由同一 fingerprint 合并,主动取消和“同一 turn 已在运行”不作为错误采集。分层口径见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md)。 +- 报告池只收**没有任何调用方处理**的错误:React render error、`window.onerror`、`unhandledrejection`,以及调用方判定为真故障后带上下文交给错误池(`ClientActionError`)的错误。Agent Runtime 的终态失败、预算耗尽和启动确认失败由 Rust 失败投影统一入池;Direct Codex 与专业 Agent 的前台裸 Tauri invoke catch 作为补充入口,重复事件由同一 fingerprint 合并,主动取消和“同一 turn 已在运行”不作为错误采集。分层口径见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 与 [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](../adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md)。 - 事件字段包括 eventId、fingerprint、source、message、stack、时间和次数;重复事件合并。不再携带 severity、errorCode、page、action、requestId 等无法稳定关联的字段。 - 指纹计算可使用调用方的 page/action 及脱敏后的首个调用点作为进程内区分输入,但这些上下文不会作为事件字段上传;消息与 stack 在入池前统一脱敏,WebCrypto 失败时降级为稳定可读指纹,采集本身不得产生新的未处理拒绝。 -- 命令失败按具体变体建模(Rust 枚举 + ts-rs 导出的 `type` 判别联合;每个变体持有一个同名载荷结构体,生成 `{ type } & 变体名`,前端每个分支拿到具名类型),前端按变体分流,**任何地方都不对错误文案做判断**:认得的业务变体(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。 +- 命令失败按具体变体建模(Rust 枚举 + ts-rs 导出的 `type` 判别联合;每个变体持有一个同名载荷结构体,生成 `{ type } & 变体名`,前端每个分支拿到具名类型),前端按变体分流,**任何地方都不对错误文案做判断**:认得的业务变体(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。认证命令的封装形态:`invokeClientAuth` 把结构化拒绝转成 `ClientAuthFailure`(`extends ClientActionError`,`payload` 为判别联合,`context.action` 是命令名),判定只写在 catch 子句里,每个 `case` 用 `as` 取具名载荷类型,`default` 用 `expectNever` 在编译期挡住漏接变体。 - 客户端 API 自动采集只覆盖网络错误、408 和 5xx(`clientApi` 作为 `fetch` 的调用方在抛出前判定);预期的 4xx 登录/鉴权失败不进入错误报告池。 - Rust 侧通过 `app_log!` 将普通文本日志同时输出到 stderr 和 AppData `diagnostics/application.log`,超出 256 KiB 滚动到 `application.previous.log`;WebView 的 console 输出通过 `append_application_log` 镜像到同一 raw log,并在客户端桥接处再次脱敏;`read_diagnostic_logs` 只读取应用级日志。 - 这里有两套互不相干的东西,不要互相代入:**错误报告事件池**是进程内 `error_report` 的结构化事件(本次变更不动它,仍然只在内存里、提交时才生成 `events.jsonl`);**统一 Agent Runtime 错误事件**是项目内 sidecar `.agent/runtime/errors/.json`,既不进事件池也不进报告包。因为报告包里的日志附件只有 AppData 应用日志,所以 sidecar 的同一份已脱敏诊断再作为**日志行**(不是报告事件)投影成两行:`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef,全部是程序生成或调用方常量)与 `agent.runtime.error.detail`(详情行:hint / summary / detail / metadata,自由文本只出现在这里)。两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,不新增字段来源;落到日志前 summary 按 320 字符、detail / metadata 按(1200 / 200 字符)预算脱敏截断(summary 由调用方给,`direct_tool_bridge` 会传工具错误原文,而 `app_log!` 同时把整行写 stderr,那里没有 `sanitize_diagnostic_message` 兜底)。`sanitize_diagnostic_message` 命中凭据标记时替换的是**整行**,自由文本因此只放详情行:详情行被吃掉也不影响身份行定位事件。