文档先行:AGC 命令错误结构化与错误报告口径
- 新增 ADR:命令错误按具体变体结构化(ts-rs 导出)、报告池只收没人处理的错误、前端按 type 分流不匹配文案 - 更新【技术方案】AGC错误报告与诊断上传:采集口径改为"只收没有调用方处理的错误",补命令变体分流与 clientApi 边界 - 更新共享记忆 decision-log:记录本次口径与影响范围 - 更新共享记忆 pitfalls:记录"输错密码被当成客户端缺陷上报"的现象、根因与判据 - 修正 DirectTurnError 模块注释中"命令边界只给字符串"的过期描述,改为结构化拒单载荷 - docs/README.md 登记新 ADR
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# 【ADR】AGC 命令错误结构化与错误报告口径
|
||||
|
||||
状态:已接受(2026-10-01 落地,实施顺序见同日的决策记录与
|
||||
[`【技术方案】AGC错误报告与诊断上传-2026-08-31`](../technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md))
|
||||
|
||||
## 背景
|
||||
|
||||
用户在登录页把密码输错一次,报告面板就出现两条"错误事件"并弹出「发现问题」:
|
||||
|
||||
```text
|
||||
登录失败:密码长度需要在 6 到 128 位之间 — auth · 1 次
|
||||
手机号或密码错误 — auth · 1 次
|
||||
```
|
||||
|
||||
根因不是文案,而是两件事叠加:
|
||||
|
||||
1. 登录已经下沉到 Rust(`login_client_with_password` 等命令),命令失败以 `Err(String)` 返回;
|
||||
Tauri 把 `String` 原样交给 JS,`invoke` 以**裸字符串**拒绝,前端拿到的东西没有任何类型信息。
|
||||
2. WebView 侧的 `shouldCaptureClientError` 对"非 object"值走默认 `return true`,于是
|
||||
`handleLoginSubmit` 的 catch 把"用户输错密码"当成缺陷事件报进了错误池。
|
||||
|
||||
这与 [`【技术方案】AGC错误报告与诊断上传-2026-08-31`](../technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md)
|
||||
已写明的"预期的 4xx 登录/鉴权失败不进入错误报告池"直接冲突——口径早就定了,是错误通道的实现方式违背了它。
|
||||
|
||||
DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接单化-2026-09-23`](./【ADR】DirectProject命令接单化-2026-09-23.md) §4):
|
||||
命令返回结构化 typed error,前端按变体分流,"认不得的变体或非结构化错误"原样抛出走上报链路。
|
||||
本 ADR 把这套口径推广成 AGC 命令错误的通用约定,并同时收窄错误报告池的入口。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 命令错误按具体变体建模,不按文案匹配
|
||||
|
||||
- Rust 侧定义具体变体枚举(auth 首个落地:`ClientAuthError`),`#[derive(Serialize, TS)]` +
|
||||
`#[serde(tag = "type", rename_all = "camelCase")]`,用 ts-rs 导出到
|
||||
`apps/ai-game-creator-shell/src/services/generated/`;生成物不手改。
|
||||
- `#[tauri::command]` 的 `Err` 直接携带该枚举(Tauri 2 的 `InvokeError(pub serde_json::Value)` 支持结构化错误)。
|
||||
这是 DirectProject 已有的做法(`enqueue_direct_codex_turn -> Result<(), DirectTurnEnqueueFailure>`),不是新约定。
|
||||
- 变体按**可判定的事实**命名。服务端 400 只提供 `status + message`(`AppError.code` 仍是通用
|
||||
`BAD_REQUEST`),所以 400 变体按"哪条请求的输入被拒"命名(如 `passwordEntryInputRejected`),
|
||||
不假装能区分密码长度/手机号格式;**任何地方都不允许对错误文案做判断**。
|
||||
- 每个变体带一份可展示 `message`,文案仍只在 Rust 生成一次;前端不拼文案。
|
||||
|
||||
### 2. 报告池只收"没有任何调用方处理"的错误
|
||||
|
||||
谁抛出、谁判定。分层规则:
|
||||
|
||||
- **预期业务拒绝**(用户输入、前置条件、预期 4xx):由调用方消化并给用户反馈,**永不进池**。
|
||||
- **真故障**(网络不可达、5xx、写盘/运行时安装失败、agent 终态失败):由调用方带上文重抛,
|
||||
经 `window.onerror` / `unhandledrejection` 入池;Rust 侧 agent 终态失败仍由失败投影入池。
|
||||
- **WebView 全局 handler 是兜底**:任何没人 catch 的错误都进池。
|
||||
- **API 客户端(`clientApi`)在抛出前判定 408/5xx/网络为缺陷**:它是 `fetch` 的调用方,
|
||||
这一判定就发生在它这一层;4xx 一律不报,交给上层调用方。这条边界保持现状,不放宽也不收紧。
|
||||
|
||||
### 3. 前端按变体分流,认不出就抛
|
||||
|
||||
- `isClientAuthError` 只做形状读取(`type` 是稳定判别键),`clientAuthErrorNotice` 用
|
||||
`switch (error.type)` 给出可展示文案;`default → null` 表示"认不出"。
|
||||
- 认不出、系统类、非结构化拒绝 → `throw new ClientActionError(message, context, cause)`;
|
||||
`ClientActionError` 只承载 `source/action/page` 上下文与 `cause`,由全局 handler 用
|
||||
`instanceof` 解包后入池(指纹/展示字段与今天一致)。
|
||||
- 删除 `shouldCaptureClientError`:不再存在"叶子自己判定要不要报"的口径。
|
||||
|
||||
### 4. 报告面板与通知行为不变
|
||||
|
||||
默认选中快照中的全部事件、只由通知中的「查看并报告」打开、poisoned 快照用 fallback 等承诺保持不变;
|
||||
本次只保证"不该进池的东西不再进池"。
|
||||
|
||||
## 后果与边界
|
||||
|
||||
- auth 三命令(`login_client_with_password`、`login_client_with_phone_code`、`send_client_phone_login_code`)
|
||||
及其共用链路(`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 同包发布,"认不出"意味着有人加了变体忘了接界面,属于缺陷。
|
||||
- 仍保留的显式采集点(`captureAgentRuntimeError`、`ResourceReferenceInput` 偏好写盘、`invokeDiagnostic`)
|
||||
在后续变更里按同一口径重抛/删除,本 ADR 不改它们的行为。
|
||||
|
||||
## 验收
|
||||
|
||||
```text
|
||||
npm run ai-game-creator-shell:typecheck
|
||||
npx vitest run apps/ai-game-creator-shell/tests/errorReporting.test.ts apps/ai-game-creator-shell/tests/ErrorReportDialog.test.tsx apps/ai-game-creator-shell/tests/ErrorReportNotice.test.tsx
|
||||
cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings
|
||||
npm run ai-game-creator-shell:check:rust:shell
|
||||
npm run check:generated-bindings
|
||||
npm run check:doc-index
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
Reference in New Issue
Block a user