diff --git a/docs/README.md b/docs/README.md index 4c296810a..a62df7401 100644 --- a/docs/README.md +++ b/docs/README.md @@ -52,7 +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` 把拒绝装进 `ClientAuthErrorWrapper`(`cause` 就是 ts-rs 生成的 `ClientAuthError` 判别联合),不新增手写错误类;判定只写在 catch 子句里,每个分支 `as` 具名载荷,系统变体原样抛出经 `unhandledrejection` 入池,`default: expectNever` 编译期挡住漏接变体。 +- [AGC 认证失败的 JS 侧载体与抛出时机](./adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md):认证命令统一经 `invokeClientAuth` 把拒绝装进 `ClientAuthErrorWrapper`(`error` 字段就是 ts-rs 生成的 `ClientAuthError` 判别联合),不新增手写错误类;判定只写在 catch 子句里,无字段变体用固定文案、带载荷分支先 `as` 取自己的具名载荷类型,系统变体原样抛出经 `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 拆分、命名、启用和启动时注入边界。 @@ -128,7 +128,7 @@ - [UI 编辑器图片素材选择器](./technical/【前端设计】UI编辑器图片素材选择器-2026-09-03.md) - [后台 Dashboard 运营看板方案](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md) - [后台多账号与 Tab 访问权限方案](./technical/【后台管理】多账号与Tab访问权限方案-2026-07-14.md) -- [Pingora 独立网关试点](<./technical/【开发运维】Pingora独立网关试点-2026-06-11.md>) +- [Pingora 独立网关试点](./technical/【开发运维】Pingora独立网关试点-2026-06-11.md) - [AGC 后台模型别名与对话选择](./technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md):官方目录、本地自定义 LLM 开关、端点模型勾选与预览。 - [UI 编辑器工作流完成通知弹窗](./technical/【设计】UI编辑器工作流完成通知弹窗-2026-09-04.md) - [官网 SEO 地基实施约定](./technical/【SEO】官网SEO地基实施约定-2026-07-10.md) diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index d7adfcdc2..718d0e9cd 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -33,18 +33,23 @@ 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`)。 +- **变体拍平在同一层,不嵌套子枚举**:可枚举的事实各自成一个变体,只有类型本身说不出来的事实才进载荷 + (服务端原文、HTTP 状态码、本机 IO 明细)。internally tagged 下无字段变体就是 `{ type: 'x' }`;带 + 载荷变体是 `{ type: 'x', ... }`,ts-rs 为它生成 `{ type: 'x' } & X` 与 `generated/X.ts`。例如服务地址 + 校验的 7 种失败各自成一个无字段变体,前端只按 `type` 选提示。 +- 前端 `switch (error.type)` 必须列全变体:无字段变体直接取本 catch 的固定文案;带载荷变体先 `as` + 取自己的具名载荷类型,再读它自己的字段(等价于 Java 的 `catch (X e)`)。`default` 用 `expectNever` + 让漏接变体变成**编译错误**。**不假设所有变体都有同一个字段**,也不做任何文案匹配。 +- Rust **不预拼用户可见文案**:载荷只装原始事实(服务端 400 的原文 `serverMessage`、HTTP 状态码、 + 本机 IO 的 `detail`),服务端没给原文就是 `None`;前缀与句式由前端调用方在自己的 catch 分支按当前 + 操作拼接。 - `#[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`), + `BAD_REQUEST`),所以 400 变体按"哪条请求的输入被拒"命名(如 `passwordLoginRejected`), 不假装能区分密码长度/手机号格式;**任何地方都不允许对错误文案做判断**。 -- 每个变体带一份可展示 `message`,文案仍只在 Rust 生成一次;前端不拼文案。 +- 本地前置校验只做布尔判定、拿不到更细事实时不编字段:手机号校验 `phone_is_valid` 只回 true/false, + 所以 `phoneNumberInvalid` 保持无字段,提示由调用方给固定文案;编不出事实的"细分原因"不硬造。 ### 2. 报告池只收"没有任何调用方处理"的错误 @@ -63,9 +68,9 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - 本节原先的"调用方用 `isClientAuthError` 读形状、`switch (error.type)` 分流、`default` 交池"已被 [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](./【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md) - 取代:`invokeClientAuth` 把结构化拒绝装进 `ClientAuthErrorWrapper`(`cause` 是判别联合),判定只写在 - catch 子句里,每个 `case` 用 `as` 取具名载荷类型。 -- "未识别变体上调是**故意**的"不再成立:`default` 改为 `expectNever(error.payload)`,漏接变体是 + 取代:`invokeClientAuth` 把结构化拒绝装进 `ClientAuthErrorWrapper`(`error` 字段是判别联合),判定只写在 + catch 子句里,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as` 取具名载荷类型。 +- "未识别变体上调是**故意**的"不再成立:`default` 改为 `expectNever(failure)`,漏接变体是 **编译错误**,不再是运行时报进池。 - 系统变体与非结构化拒绝仍由调用方原样 `throw`,经全局 `unhandledrejection` 入池; `captureClientError` 的 `instanceof ClientAuthErrorWrapper` 解包保持不变。 diff --git a/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md b/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md index 97b2180f0..2cacb44db 100644 --- a/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md +++ b/docs/adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md @@ -12,7 +12,7 @@ Rust 已经返回结构化错误,但 Tauri 的 `invoke` 拒绝值是**普通对象**,不是 `Error`: -- 调用方 `catch (error)` 拿到的是 `{ type, message, ... }`,没有栈。原样 `throw` 它, +- 调用方 `catch (error)` 拿到的是 `{ type, reason, ... }`,没有栈。原样 `throw` 它, 上报链路的 `error instanceof Error` 判断会把它降级成 `new Error(String(error))` (`[object Object]`),文案与类型一起丢掉。 - 前一版在渲染层加了 `isClientAuthError` / `getClientAuthErrorMessage` / @@ -29,14 +29,13 @@ ts-rs 已经把 `ClientAuthError` 生成成判别联合(`src/services/generate 原始拒绝值是普通对象,直接 `throw` 会被上报链路降级成 `String(obj)`;所以包装层把它装进 **已有**的 `ClientAuthErrorWrapper`,载体只持有一个 `ClientAuthError` 类型的 `error` 字段,值就是原始拒绝值(也就是那个 -判别联合)。它**不读、不产任何派生值**:不读变体上的 `message`(变体不保证都有这个字段), +判别联合)。它**不读、不产任何派生值**:不读变体字段, 不塞 `context`,`Error.message` 留空。上报的 `source` / `action` 由调用 `captureClientError` 时的显式入参决定;catch 里 `error.error as ClientAuthError` 直接分流。 -- 19 个具名载荷类型是有意保留的:它们是 §3 每个 `case` 里 `as X` 的目标,也正是"不要假设 - 所有变体字段相同"的落点。改成内联 struct 变体确实能让 ts-rs 把字段内联进联合成员、少掉 - 19 个生成文件,但每个分支就再也拿不到可 `as` 的具名类型(只能手写内联对象类型,等于放弃 - ts-rs)。本 ADR 选择保留具名载荷。 +- 只有带载荷的变体才有具名载荷类型:无字段变体在 ts-rs 里就是 `{ type: 'x' }`,不生成文件; + 有字段的变体才生成 `X.ts`。这些具名类型是 §3 每个带载荷 `case` 里 `as X` 的目标,也正是 + "不要假设所有变体字段相同"的落点——没有字段可读的变体不需要、也不允许硬造一个空载荷类型。 ### 2. 一个包装函数:`invokeClientAuth` @@ -59,7 +58,7 @@ async function invokeClientAuth(command, args): Promise { 静默吞掉——只是不再在包装层替 Tauri 兜底。 - `requireInvoke()` 放在 `try` 之外:认证桥未安装是我们自己的失败关闭错误,不是命令拒绝,保持 原样抛出(`需要在 Tauri App 内登录`)。 -- **不读、不产任何派生值**:不读变体上的 `message`(变体不保证都有这个字段),不注入 +- **不读、不产任何派生值**:不读变体字段,不注入 `source` / `action`,`Error.message` 留空;载体只把原始拒绝值原样放进 `error`。展示文案与 上报上下文都由 catch 子句里拿到具名载荷的调用方决定。 - 该包装是"Rust 结构化错误 → JS 错误对象"的唯一转换点:不做分类、不读文案判断、不兜底文案。 @@ -72,12 +71,19 @@ catch (error) { const failure = error.error as ClientAuthError; switch (failure.type) { case 'phoneNumberInvalid': { - const payload = failure as PhoneNumberInvalid; - setLoginStatus(payload.message); + // 无字段变体:文案由本 catch 给,不读任何字段。 + setLoginStatus('手机号无效: 需为纯数字且不超过 32 位'); + break; + } + case 'passwordLoginRejected': { + const payload = failure as PasswordLoginRejected; + // 前缀由本 catch 按当前操作提供;Rust 只给服务端原文(可能为 null)。 + setLoginStatus(`登录失败: ${payload.serverMessage ?? '服务端拒绝了本次登录'}`); break; } // ... 每个业务 / 会话变体一个分支 - case 'authNetworkUnavailable': + case 'authNetworkTimeout': + case 'authNetworkUnreachable': // ... 系统变体逐个列出后原样抛出 throw error; default: @@ -86,11 +92,13 @@ catch (error) { } ``` -- **每个业务 / 会话 `case` 用 `as` 取自己的具名载荷类型**,不写 `failure.message` 这种 - 跨变体的通用读取;等价于 Java 的 `catch (PhoneNumberInvalid e)`。系统变体不读载荷(调用方 - 只负责原样抛出),但变体名必须逐个列出,`default` 的 `expectNever` 才成立。 -- 业务 / 会话变体:把载荷自带的 `message` 原样交给用户,**不加兜底文案**(那就是 Rust 生成的 - 那一份)。 +- **每个带载荷的业务 / 会话 `case` 用 `as` 取自己的具名载荷类型**,再读它自己的字段;无字段的 + `case` 直接用本 catch 的固定文案。不写跨变体的通用读取,也不让 Rust 预拼上下文。前缀取自 + 当前 catch 的操作语义(登录、发码、启动检查各自可以不同),等价于 Java 的 `catch (PasswordLoginRejected e)`。 + 系统变体不读载荷(调用方只负责原样抛出),但变体名必须逐个列出,`default` 的 `expectNever` 才成立。 +- 业务 / 会话变体:Rust 只给可判定事实(无字段变体连字段都没有;带载荷变体给 `serverMessage` / + `status` / `detail`),调用方在自己的 catch 里补上本次操作的上下文前缀(例如「服务器地址非法: + 远程地址必须使用 https」、「登录失败: 密码长度需要在 6 到 128 位之间」)。 - 系统变体:调用方处理不了,**原样 `throw`**。`onSubmit` / `onClick` 这类 `void` 掉的 handler 抛出的拒绝最终以 `unhandledrejection` 结算,由全局 handler 交给错误池。 - `default: expectNever(failure)`(`expectNever(value: never)`)让"Rust 加了变体而这里 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index abb1c792b..a0cfb2889 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4,12 +4,12 @@ - 决策: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)由调用方消化并给反馈,永不进池;真故障由调用方带上下文交给错误池(`ClientAuthErrorWrapper` 承载 `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,clientAuthErrorWrapper.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` 已删除)。 +- 边界:变体按**可判定的事实**命名——服务端 400 只给 `status + message`(`AppError.code` 仍是通用 `BAD_REQUEST`),所以 400 变体按"哪条请求的输入被拒"命名(如 `passwordLoginRejected`),不假装能区分密码长度/手机号格式。报告面板默认全选、只由通知打开的既有承诺不变。`captureAgentRuntimeError`、`ResourceReferenceInput` 偏好写盘、`invokeDiagnostic` 三处显式采集点保持原行为,按同一口径改造或删除留在后续变更。 +- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{auth_error.rs,auth_session.rs}`、`apps/ai-game-creator-shell/src/services/{clientAuthErrorWrapper.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 形状**只有带载荷的变体才有具名载荷类型**——无字段变体在 ts-rs 里就是 `{ type: 'x' }`,有字段的变体是 newtype 变体持有同名 `#[ts(export)]` 结构体,生成 `{ type: 'x' } & X` 与 `src/services/generated/X.ts`;前端 `switch (error.type)` 的无字段分支用固定文案,带载荷分支先 `as X` 再读它自己的字段。不允许在前端手写这层类型,也不再包派生分类 / 提示文案函数(`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)` 调用;拒绝值原样装进已有的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体 `message`、不塞 `context`、`Error.message` 留空),**不新增手写错误类**(`ClientAuthFailure` 已删除),形状完全信任 tauri + ts-rs 映射、不做运行时嗅探。形状读取 / 文案回落 / 提示分类三层(`isClientAuthError`、`getClientAuthErrorMessage`、`presentAuthFailure`)全部删除。 -- 决策(2026-10-01,判定位置与出口):要不要上报只由 catch 子句里的 `switch (failure.type)` 判,`failure = error.error as ClientAuthError`,每个 `case` 用 `as` 取具名载荷类型;业务 / 会话变体把载荷自带 `message` 原样给用户(无兜底文案),系统变体原样 `throw` 经全局 `unhandledrejection` 入池(`captureClientError` 用 `instanceof` 解包 `error` 字段取原始错误),`default: expectNever(failure)` 让漏接变体编译失败。取代"未识别变体上调是故意的"。 +- 决策(2026-10-01,JS 侧载体与抛出时机):认证命令统一经 `invokeClientAuth(command, args)` 调用;拒绝值原样装进已有的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体字段、不塞 `context`、`Error.message` 留空),**不新增手写错误类**(`ClientAuthFailure` 已删除),形状完全信任 tauri + ts-rs 映射、不做运行时嗅探。形状读取 / 文案回落 / 提示分类三层(`isClientAuthError`、`getClientAuthErrorMessage`、`presentAuthFailure`)全部删除。 +- 决策(2026-10-01,判定位置与出口):要不要上报只由 catch 子句里的 `switch (failure.type)` 判,`failure = error.error`;无字段业务 / 会话变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名类型、再用它自己的 `serverMessage` / `status` / `detail` 拼上本次操作的上下文前缀;Rust 不预拼用户可见文案、服务端原文缺失就是 `null`(无兜底文案)。系统变体原样 `throw` 经全局 `unhandledrejection` 入池(`captureClientError` 用 `instanceof` 解包 `error` 字段取原始错误),`default: expectNever(failure)` 让漏接变体编译失败。取代"未识别变体上调是故意的"。 - 决策(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/{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 用例。 @@ -9138,6 +9138,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 影响面:`apps/ai-game-creator-shell/src-tauri/build_support/{package-layout.json,package-layout.generated.rs,package_layout.rs,godot_bundle.rs}`、`src-tauri/build.rs`、`scripts/{prepare-bundled-resources.mjs,prepare-bundled-resources.test.mjs,check-package-layout.mjs,build-release.mjs}`、两份 `.taurignore`、技术方案 §4.9/§8、M3 里程碑、运维文档、决策日志与排障经验。 - 验证:准备步骤 13 条用例通过(含三类准备步骤调度、指纹跳过、缺产物失败关闭、幂等与失败关闭);`npm run agc:bundled-resources:check` 通过;`cargo check --no-default-features` 通过(构建脚本仅剩只读校验,且不再出现在随包资源的写入路径上)。 - 边界(未验证):Windows 真机未验证——powershell/cargo 两条命令路径、Unity/Godot/Cocos 产物归位、包内容一致性与客户端加载,需按 M3 里程碑的验收清单在 Windows 上确认。 + ## 2026-09-24 命令入队化与待发消息队列归宿主:放行归 Thread Manager,CLI 直连入口退役 - 决策(词表):「接单 / 拒单」退役,命令边界的成功与失败改叫「入队 / 入队失败」;旧「接单」的语义角色 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 0189a4f72..8e6521e58 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -8,7 +8,7 @@ - **原因**:① 登录已下沉 Rust,`login_client_with_password` 等命令失败返回 `Err(String)`,Tauri 以**裸字符串**拒绝 `invoke`,前端拿不到任何类型信息;② `shouldCaptureClientError` 对非 object 值走默认 `return true`,`handleLoginSubmit` 的 catch 把预期业务拒绝报进了错误池。技术方案里"预期 4xx 登录/鉴权失败不进池"的口径早就成立,是错误通道的实现方式违背了它。 - **处理(现行口径)**:命令错误一律按具体变体结构化(`Result<_, ClientAuthError>` + ts-rs 导出),认证命令统一经 `invokeClientAuth` 调用:结构化拒绝原样装进已有的 `ClientAuthErrorWrapper`(只有一个 `error` 字段,值是判别联合),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` 取全局 `unhandledrejection` handler 的显式入参;载体不再携带 `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 子句里:`const failure = error.error as ClientAuthError; switch (failure.type)`,每个 `case` 用 `as` 取具名载荷类型,`default: expectNever(failure)` 保证漏接变体编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物随后交给 prettier。 +- **形状约定**:`ClientAuthError` 的变体拍平不嵌套——无字段变体在 TS 里就是 `{ type: 'x' }`;带载荷变体是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体)。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律写在 catch 子句里:`const failure = error.error; switch (failure.type)`,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as X` 再读自己的 `serverMessage` / `status` / `detail`,`default: expectNever(failure)` 保证漏接变体编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物随后交给 prettier;ts-rs 只写文件、不删文件,变体从有载荷改成无字段时要手动清掉孤立的 `X.ts`。 - **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/{clientAuth.ts,errorReporting.ts,platformSession.ts}`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`。 @@ -147,6 +147,7 @@ - **现象**:直接编辑 `apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs`,或另写一份组件白名单,`npm run agc:typecheck`(链内含 `npm run agc:bundled-resources:check`)会立刻失败并报「随包资源声明与 Rust 常量不一致」。 - **正确做法**:改 `build_support/package-layout.json`,运行 `npm run agc:bundled-resources:sync` 重新生成;改布局同时递增 `layoutVersion`(参与准备步骤的缓存 key)。声明里的 `codex.version` 必须与应用锁定的 `@openai/codex` 一致,门禁会对照 `apps/ai-game-creator-shell/package.json` 校验。 - **边界(M1 完成时)**:准备步骤 `scripts/prepare-bundled-resources.mjs` 尚未接入 dev / 发布入口,`npm run agc` 仍由构建脚本 staging;构建脚本当前既写资源又做只读校验,`AGC_SKIP_RESOURCE_STAGING=1` 可只跑校验。构建脚本重建 `resources/plugins` 时会整体删除该目录,所以插件侧的准备步骤清单要等 M2 接管写入后才成立,插件目录现在只校验必需组件与符号链接。 + ## 2026-09-24 模型输出的围栏会粘在正文行里:聊天 Markdown 必须先归一化再解析 - **现象**:AGC 对话里代码块解析错位——引言行被当成代码渲染(`…实现细节(game.js):```js`),或者代码块收不住、把后面的正文一起吞进去(`… return centerOn(projection); }````)。文本本身「看起来没问题」,容易被当成渲染器坏了。 diff --git a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md index fbb21b0f0..c3995cf25 100644 --- a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md +++ b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md @@ -11,7 +11,7 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事 - 报告池只收**没有任何调用方处理**的错误:React render error、`window.onerror`、`unhandledrejection`,以及调用方判定为真故障后带上下文交给错误池(`ClientAuthErrorWrapper`)的错误。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)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。认证命令的封装形态:`invokeClientAuth` 只是薄包装,把结构化拒绝原样装进**已有**的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体 `message`、不塞 `context`、`Error.message` 留空),不新增手写错误类;判定只写在 catch 子句里,每个 `case` 用 `as` 取具名载荷类型,`default` 用 `expectNever` 在编译期挡住漏接变体。 +- 命令失败按具体变体建模(Rust 枚举 + ts-rs 导出的 `type` 判别联合;变体拍平不嵌套,无字段变体生成 `{ type }`,带载荷变体生成 `{ type } & 载荷类型`),前端按变体分流,**任何地方都不对错误文案做判断**:认得的业务变体(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。认证命令的封装形态:`invokeClientAuth` 只是薄包装,把结构化拒绝原样装进**已有**的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体字段、不塞 `context`、`Error.message` 留空),不新增手写错误类;判定只写在 catch 子句里,无字段变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名载荷类型、再用它自己的 `serverMessage` / `status` / `detail` 拼上本次操作的上下文前缀,`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` 命中凭据标记时替换的是**整行**,自由文本因此只放详情行:详情行被吃掉也不影响身份行定位事件。