文档先行:AGC 认证错误的 TS 形状改为每个变体一个具名载荷类型
- ADR §1 增加"每个变体一个具名载荷结构体 + ts-rs 生成 `{ type } & X`"的决策与代价
- ADR §3 说明 switch 分支里错误已窄化成具名类型
- 技术方案同步命令错误形状描述
- decision-log / pitfalls 记录形状约定:不手写这层类型、不再包派生分类与提示文案函数
This commit is contained in:
@@ -33,6 +33,12 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
- Rust 侧定义具体变体枚举(auth 首个落地:`ClientAuthError`),`#[derive(Serialize, TS)]` +
|
||||
`#[serde(tag = "type", rename_all = "camelCase")]`,用 ts-rs 导出到
|
||||
`apps/ai-game-creator-shell/src/services/generated/`;生成物不手改。
|
||||
- **每个变体一个具名载荷结构体**,枚举用 newtype 变体持有它(`PhoneNumberInvalid(PhoneNumberInvalid)`)。
|
||||
ts-rs 因此生成 `{ type: 'phoneNumberInvalid' } & PhoneNumberInvalid`,以及每个变体一个
|
||||
`generated/<变体名>.ts`:前端 `switch (error.type)` 的每个分支都落到一个有名字的类型,等价于 Java 的
|
||||
`catch (PhoneNumberInvalid e)`,不需要 `as` 断言,载荷类型自己带 JSDoc。载荷必须能序列化成 map
|
||||
(serde 的 internally tagged 表示只接受 struct / map),所以没有无字段变体;代价是 Rust 构造点统一写成
|
||||
`ClientAuthError::X(X { message })`。这个形状在本仓已有先例(`DirectCodexUserContentPart`)。
|
||||
- `#[tauri::command]` 的 `Err` 直接携带该枚举(Tauri 2 的 `InvokeError(pub serde_json::Value)` 支持结构化错误)。
|
||||
这是 DirectProject 已有的做法(`enqueue_direct_codex_turn -> Result<(), DirectTurnEnqueueFailure>`),不是新约定。
|
||||
- 变体按**可判定的事实**命名。服务端 400 只提供 `status + message`(`AppError.code` 仍是通用
|
||||
@@ -57,7 +63,8 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
|
||||
- `isClientAuthError` 只做形状读取(`type` 是稳定判别键),**不做分类、不产出派生值**。
|
||||
- 调用方自己 `switch (error.type)`,逐个列出具体变体:业务输入 / 会话变体给提示,`default`(系统变体
|
||||
与 Rust 新增而界面没接的变体)带上文交池。判据是具体变体名,不是聚合出来的类别,也不是文案匹配。
|
||||
与 Rust 新增而界面没接的变体)带上文交池。判据是具体变体名,不是聚合出来的类别,也不是文案匹配;
|
||||
每个 `case` 里 `error` 已窄化成 §1 的具名载荷类型(如 `PhoneNumberInvalid`)。
|
||||
- 认不出、系统类、非结构化拒绝 → 调用方包成 `ClientActionError(message, context, cause)` 交给
|
||||
`captureClientError`:`instanceof` 解包 `context` 与 `cause`,指纹/展示字段与今天一致。
|
||||
**不把 rejection 留在没人接手的 Promise 上**:`onSubmit` / `onClick` 这类 `void` 掉的 handler 抛错
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
- 决策:AGC 命令失败按**具体变体**建模(Rust `#[derive(Serialize, TS)]` 枚举 + `#[serde(tag = "type", rename_all = "camelCase")]` + ts-rs 导出,生成物不手改),`#[tauri::command]` 的 `Err` 直接携带结构化枚举;前端只按 `type` 分流,**任何地方都不对错误文案做判断**。做法沿用 DirectProject 既有约定(`enqueue_direct_codex_turn -> Result<(), DirectTurnEnqueueFailure>`),不是新机制。
|
||||
- 决策:错误报告池只收**没有任何调用方处理**的错误。预期业务拒绝(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给反馈,永不进池;真故障由调用方带上下文交给错误池(`ClientActionError` 承载 `source/action/page`,`captureClientError` 用 `instanceof` 解包),`window.onerror` / `unhandledrejection` 只兜底没人接手的错误;408/5xx/网络的判定由调用方在 catch 里做(4xx 一律不报);Rust agent 终态失败仍由失败投影入池。删除 WebView 侧 `shouldCaptureClientError`。
|
||||
- 边界:变体按**可判定的事实**命名——服务端 400 只给 `status + message`(`AppError.code` 仍是通用 `BAD_REQUEST`),所以 400 变体按"哪条请求的输入被拒"命名(如 `passwordEntryInputRejected`),不假装能区分密码长度/手机号格式。报告面板默认全选、只由通知打开的既有承诺不变。`captureAgentRuntimeError`、`ResourceReferenceInput` 偏好写盘、`invokeDiagnostic` 三处显式采集点保持原行为,按同一口径改造或删除留在后续变更。
|
||||
- 影响范围:`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/ClientAuthError.ts`(ts-rs 生成)。
|
||||
- 影响范围:`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 DirectProject 审批拒绝原因留痕
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- **原因**:① 登录已下沉 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`。
|
||||
|
||||
## 2026-09-30 构建期 staging 撞上不装 npm 依赖的 Linux 门禁:AGC 壳 Rust lane 全红
|
||||
|
||||
@@ -11,7 +11,7 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事
|
||||
- 报告池只收**没有任何调用方处理**的错误: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)。
|
||||
- 事件字段包括 eventId、fingerprint、source、message、stack、时间和次数;重复事件合并。不再携带 severity、errorCode、page、action、requestId 等无法稳定关联的字段。
|
||||
- 指纹计算可使用调用方的 page/action 及脱敏后的首个调用点作为进程内区分输入,但这些上下文不会作为事件字段上传;消息与 stack 在入池前统一脱敏,WebCrypto 失败时降级为稳定可读指纹,采集本身不得产生新的未处理拒绝。
|
||||
- 命令失败按具体变体建模(Rust 枚举 + ts-rs 导出的 `type` 判别联合),前端按变体分流,**任何地方都不对错误文案做判断**:认得的业务变体(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。
|
||||
- 命令失败按具体变体建模(Rust 枚举 + ts-rs 导出的 `type` 判别联合;每个变体持有一个同名载荷结构体,生成 `{ type } & 变体名`,前端每个分支拿到具名类型),前端按变体分流,**任何地方都不对错误文案做判断**:认得的业务变体(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。
|
||||
- 客户端 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/<eventId>.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` 命中凭据标记时替换的是**整行**,自由文本因此只放详情行:详情行被吃掉也不影响身份行定位事件。
|
||||
|
||||
Reference in New Issue
Block a user