清掉错误类型相关的过时文档
- 删两份 ADR:`AGC命令错误结构化与错误报告口径`、`AGC认证失败的JS侧载体与抛出时机`;错误通道的现行口径只留在代码与该专题技术方案里; - 删导出面板的【实施计划】与【里程碑】两份计划,落地情况回到技术方案里一句话; - `docs/README.md` 去掉对上述四份的链接,技术方案里的失败通道段落跟着删; - `decision-log` 去掉 2026-10-01 那一段,2026-10-05 那段的标题与四条决策改成「同步细节与文案归属」;`pitfalls` 去掉两份 ADR 的引用。
This commit is contained in:
@@ -9,21 +9,19 @@
|
||||
- 决策(工作目录):`EXPORT_WORK_RELATIVE_DIR = ".export"` 锚在项目根并保持 flat;共享 icon 放 `.export/` 根,注册表存项目内相对路径,多目标复用同一文件;agent 拷贝的脚本按 `<target>` 前缀命名避免第二目标撞名。`.export/` **只**排除在项目快照同步之外(`PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS`),恢复或换机后需重新适配(已知代价);**不得**并进通用排除口径——`agent/direct_patch.rs` 直接拿它拒绝路径,`agc_apply_patch` 会以「不得修改受保护或排除路径」拒绝 `.export/`,首次适配就落不了地;同机 checkpoint 仍含 `.export/`,可作恢复适配脚本的兜底。
|
||||
- 决策(skill 落点):`pack.mjs` 新增 `--zip-out <path>`(相对 cwd 解析、父目录自动创建),让「产物必须落在项目根 `.export/xhs-minitool.zip`」有确定写法;`--out-dir` 只允许指向 vite 构建输出目录(它会就地删掉该目录内非白名单扩展名的文件)。宿主只认结果:脚本名 + 非空产物路径,产物结构与平台规范仍归 skill 与 agent。
|
||||
- 决策(权限与副作用):跑脚本复用 `command.exec` 的权限口径(只查 deny,UI 按钮即用户确认),不新增 `GAME_CREATION_APP_COMMANDS` 条目;导出链路不拿项目写锁、不推进全局 revision,`.export/` 不进 manifest、素材、UI State 或客户端投影;发布包白名单收集,`.export/` 不会进入。
|
||||
- 决策(prompt 归属):首次适配与失败修复的指令正文、契约常量与组装都放前端(`view/project-development/export/state/xhsMinitoolInstruction.ts` 纯函数;失败说法在 `xhsMinitoolFailure.ts`),宿主不提供 `enqueue_*` 命令;前端经 `useDirectProjectChatController` 的 `chat.submit` 入队,复用它既有的会话写权限门与 clientTurnId。Rust 侧因此只有 per-target 的四个命令(读内容 / 读指纹 / 存表单 / 跑构建),typed error 首个错误即返回,不带问题数组;内容与指纹为什么分成两条见下面 2026-10-05 的同步细节决策。
|
||||
- 决策(prompt 归属):首次适配与失败修复的指令正文、契约常量与组装都放前端(`view/project-development/export/state/xhsMinitoolInstruction.ts` 纯函数;失败说法在 `xhsMinitoolFailure.ts`),宿主不提供 `enqueue_*` 命令;前端经 `useDirectProjectChatController` 的 `chat.submit` 入队,复用它既有的会话写权限门与 clientTurnId。Rust 侧因此只有 per-target 的四个命令(读内容 / 读指纹 / 存表单 / 跑构建);内容与指纹为什么分成两条见下面 2026-10-05 的同步细节决策。
|
||||
- 影响范围:新增 `apps/ai-game-creator-shell/src-tauri/src/export/{mod,draft/mod,draft/xhs_minitool/{mod,commands}}`(`main.rs` 仅加 `pub mod export;`)、前端 `view/project-development/export/{tabs/xiaohongshu,state,generated}`、`resources/agc-skills/vite-export-xhs-minitool/scripts/*` 与其 `manifest.json` 指纹、`src-tauri/src/agent/skill_pack.rs` 的 `include_bytes!` 内容(文件数不变,仍 36 条)。不触碰 `GAME_CREATION_APP_COMMANDS`、`shared-contracts`、`server-rs`、SpacetimeDB、OpenAPI、`api-server` 与现有 `project/export.rs` 发布链路。
|
||||
- 验证方式:`cargo test ... export::` 与 `export_bindings` + `npm run check:generated-bindings`、前端 vitest(复制 / 下载 / 冲突逐字段选择 / 适配指令组装与入队 mock)、`npm run ai-game-creator-shell:typecheck`、`npm run agc:skill-pack:sync` + `agc:skill-pack:check`、`npm run check:doc-index`、`npm run check:encoding`、`git diff --check`;真实 vite 项目上验证首轮适配产出可上传 zip 与二次导出零 agent 调用。
|
||||
- 边界:本决策只覆盖小红书小工具这一个目标;第二个目标出现时再抽 target 描述符,不预留动态注册或通用表单引擎。
|
||||
|
||||
## 2026-10-05 导出面板的同步细节与失败通道:指纹不进对外状态,真故障原样抛出
|
||||
## 2026-10-05 导出面板的同步细节与文案归属:指纹不进对外状态
|
||||
|
||||
- 背景:导出面板(同上一条)落地后回头核 ADR([`【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)、[`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](../../adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md)),发现四件事与既有口径不一致:读状态把内容与指纹混在一条命令里、失败只有「显示一句话」一条出口、策略拒绝与宿主故障共用一个变体、宿主自己拼了「省略前 N 个字符」这句用户可见文案。四条都在这里收口。
|
||||
- 背景:导出面板(同上一条)落地后回头核 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](../../adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md),发现两件事与既有口径不一致:读状态把内容与指纹混在一条命令里、宿主自己拼了「省略前 N 个字符」这句用户可见文案。两条都在这里收口。
|
||||
- 决策(同步细节不进对外状态):命令拆成 `read_xhs_minitool_export`(`form` + `hasScript`,自动刷新反复调的高频路径)与 `read_xhs_minitool_export_hash`(写回基线),后者只在进编辑会话、或新内容真的被采纳时取一次。`contentHash` 不进前端对外状态(hook 里只在 `baselineRef`),界面不显示任何同步状态。理由:刷新这条高频路径在**类型上**就动不了写回基线,否则「顺手刷新」会把两次刷新之间的外部改动认成自己的基线、把冲突吞掉。
|
||||
- 决策(自动重读,取代「刷新按钮」,作废提交 `90f651ddf` 的「刷新取消未保存输入」):取消刷新按钮,按 2 秒固定间隔自动重读(窗口在后台时跳过)。`hasScript` 是宿主现算事实,任何时刻都采纳;表单只在用户手上没有未保存输入(也不在冲突/字段错误里)时才覆盖,否则绝不覆盖正在打的字。代价是「刷新会取消未保存输入」这条旧行为被有意作废,对应的回归用例同步替换。
|
||||
- 决策(失败通道):预期拒绝留在面板(一句话按变体写死 + 宿主现场),**宿主侧事实故障与认不出形状的拒绝先给现场再原样抛出**,由全局 `unhandledrejection` 进错误池——面板不再把裸 message 当成业务失败的解释糊给用户,也不再静默吞掉真故障。抛出按指纹去重:面板每 2 秒重读,同一个故障只抛一次、变了再抛,否则报告池的 `count` 统计的是「面板开了多久」。策略拒绝(`commandDenied`)与宿主故障(`exportUnavailable`)必须是两个变体:`enforce_project_permission_policy` 那版把「策略拒绝」与「策略读不出来」拼成同一句人话,调用方无从判该不该上报,导出链路因此改用 typed 的 `enforce_project_permission_policy_rejection`。
|
||||
- 决策(文案归属的延伸):宿主不预拼用户可见文案这条也管载荷里的说明——构建输出尾部只回 `outputTail` + `omittedCharacters` 两个事实,「已省略前 N 个字符」由前端拼(`export/state/xhsMinitoolOutputTail.ts`,失败卡片与成功提示共用)。与上面那条 prompt 归属同源:机器事实在 Rust,句子在前端。
|
||||
- 刻意保留的差异:`xhsMinitoolFailure.ts` 这个「变体 → 一句话 + 下一步按钮」的纯映射没有按 ADR「判定只写在 catch 里、不抽 presenter」内联进 catch。那条规则的落点是只有一个消费方的认证 catch;这里有两个消费方(面板卡片与交给 agent 的修复指令),必须说同一句话,否则用户在面板里看到的和 agent 拿到的会分叉。它不判「要不要上报」——上报判据在 hook 的 catch 里。
|
||||
- 契约常量的单一来源:Rust `export/mod.rs` 与 `draft/xhs_minitool/layout.rs` 是契约值(`.export`、脚本名、注册表名、产物名)的唯一权威;前端在 `export/state/xhsMinitoolInstruction.ts` 保留一份用于拼指令,由 `tests/xhsMinitoolContract.test.ts` 直接读 Rust 源码把两份钉住(ts-rs 不能导出 `const`,所以钉法是测试而不是生成物)。改 Rust 常量而前端没跟,测试红。
|
||||
- 影响范围:`export/draft/xhs_minitool/{commands,error,build,dto}.rs`、`export/generated/{CommandDenied,CommandFailed,XHSMiniToolExportRunResult,XHSMiniToolExportError}.ts`、`export/state/{useXhsMinitoolExport,xhsMinitoolFailure,xhsMinitoolOutputTail,xhsMinitoolApi}.ts`、`export/tabs/xiaohongshu/*`。不新增命令、不碰权限位、不动发布链路。
|
||||
- 影响范围:`export/draft/xhs_minitool/{commands,error,build,dto}.rs`、`export/generated/*.ts`、`export/state/{useXhsMinitoolExport,xhsMinitoolFailure,xhsMinitoolOutputTail,xhsMinitoolApi}.ts`、`export/tabs/xiaohongshu/*`。不新增命令、不碰权限位、不动发布链路。
|
||||
- 验证方式:`cargo test ... export::`(含 `export_bindings`)、`npm run check:generated-bindings`、`npx vitest run apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx apps/ai-game-creator-shell/tests/xhsMinitoolContract.test.ts`、`npm run ai-game-creator-shell:typecheck`、`npm run ai-game-creator-shell:check:rust:shell`、`npm run check:doc-index`、`npm run check:encoding`、`git diff --check`;真实 vite 项目上的首轮适配与二次导出仍是未验证项。
|
||||
- 边界:只覆盖小红书小工具这一个目标;第二个目标出现时再抽 target 描述符,不预留动态注册或通用表单引擎。
|
||||
|
||||
@@ -164,23 +162,6 @@
|
||||
- 影响范围:根开发脚本、AGC 开发启动编排、本地开发运维文档;不改变 API、schema、生产部署和独立 `npm run agc` 行为。
|
||||
- 验证方式:参数/状态单测、开发栈健康端点 smoke、`.app/dev-stack.json` 身份复用检查、进程树收束检查。
|
||||
|
||||
## 2026-10-01 AGC 命令错误结构化与错误报告口径
|
||||
|
||||
- 决策:AGC 命令失败按**具体变体**建模(Rust `#[derive(Serialize, TS)]` 枚举 + `#[serde(tag = "type", rename_all = "camelCase")]` + ts-rs 导出,生成物不手改),`#[tauri::command]` 的 `Err` 直接携带结构化枚举;前端先按 `type` 选类别、可枚举细分再按类型化 `reason` 分流,**任何地方都不对错误文案做判断**。做法沿用 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 变体按"哪条请求的输入被拒"命名(如 `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`;可枚举的细分原因是类型化枚举字段(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`),不是字符串、也不各拆一个顶层变体。前端 `switch (error.type)` 的无字段分支用固定文案,带载荷分支先 `as X` 再读它自己的字段,`reason` 是枚举时再 `switch (payload.reason)`(`default` 同样用 `expectNever`)。不允许在前端手写这层类型,也不再包派生分类 / 提示文案函数(`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 生成的判别联合;不读变体字段、不塞 `context`,构造时把整份载荷 `JSON.stringify` 进 `Error.message`,上报事件因此拿到机器事实),**不新增手写错误类**(`ClientAuthFailure` 已删除),形状完全信任 tauri + ts-rs 映射、不做运行时嗅探。形状读取 / 文案回落 / 提示分类三层(`isClientAuthError`、`getClientAuthErrorMessage`、`presentAuthFailure`)全部删除。
|
||||
- 决策(2026-10-01,判定位置与出口):要不要上报只由 catch 子句里的 `switch (failure.type)` 判,`failure = error.error`;无字段业务 / 会话变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名类型、再用它自己的 `reason` / `serverMessage` / `status` 拼上本次操作的上下文前缀(`reason` 是枚举时再 `switch (payload.reason)`);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-02,429 按路由判定):`/api/auth/phone/login` 验证码错误次数耗尽返回的 429 是用户可修正的输入问题,映射为 `phoneCodeLoginRejected`(复用现有业务变体、不进错误池);发码路由仍是 `smsCodeThrottled`,其余路由的 429 仍是 `unexpectedRejection`。
|
||||
- 追加(2026-10-02,401 归 phoneCodeLoginRejected):`/api/auth/phone/login` 的 401 只来自「用户不存在」(验证码错误/失效/过期在服务端都是 400,已由 `phoneCodeLoginRejected { serverMessage }` 带原文);顶层变体 `smsCodeRejected` 退役删除,前端三个 catch 去掉了它那个「验证码错误或已过期」的固定分支,`phoneCodeLoginRejected` 的文案统一为「验证码登录失败:<服务端原文>」。
|
||||
- 追加(2026-10-02,读 body 失败按已确认状态码归类):AGC 认证请求拿到 `status` 后 `response.text()` 失败,不再一律压成 `authNetworkFailure { unreachable }`;非 2xx 走既有分类(`serverMessage` 为 `None`,如 503 → `authServiceUnavailable { 503 }`),只有 2xx 响应没收完才算传输层故障。分类收敛在 `classify_unreadable_body`。
|
||||
- 追加(2026-10-02,系统类失败保留原始错误载荷):`clientSessionPersistFailed` / `runtimeSessionInstallFailed` / `authClientInitFailed` 都带 `detail: string`(原始 error),既让调用方有机会分流处理,也让报告包带够诊断信息;原始 error 同时经 `app_log!`(落盘前过 `sanitize_diagnostic_message`)记一行本地日志。`detail` 不贴到界面上:三个 catch 用本操作的固定文案(登录检查 / 发码 / 登录各自不同)。取代上一版"本机 IO 失败不进载荷、原始 error 只进日志"。
|
||||
- 影响范围(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 用例。
|
||||
|
||||
## 2026-10-02 launcher 页面高度契约:外壳分高度,页面不再自己算窗口高度
|
||||
|
||||
- 背景:PR #228(`6d2c275d3`)只给项目页补了「外壳纵向 flex + 页面 `flex: 1 1 auto`」的高度修复;其余页面仍各自算高度——帮助页没写高度也没有内层滚动容器,内容一长就被外壳 `overflow: hidden` 裁掉且无法滚动;首页用 `h-screen` / `h-[calc(100vh-32px)]`,模板库用 JS 量父级高度写内联 `height`。
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
- **现象**:登录页密码输错(或密码长度不合规)后弹出「发现问题」,报告面板「错误事件(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 导出),认证命令统一经 `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)。
|
||||
- **处理(现行口径)**:命令错误一律按具体变体结构化(`Result<_, ClientAuthError>` + ts-rs 导出),认证命令统一经 `invokeClientAuth` 调用:结构化拒绝原样装进已有的 `ClientAuthErrorWrapper`(只有一个 `error` 字段,值是判别联合),UI 在 catch 里按具体变体分流——认得的业务 / 会话变体只给用户反馈,系统变体原样 `throw` 经 `unhandledrejection` 入池,非结构化拒绝原样抛出;删除 `shouldCaptureClientError`。
|
||||
- **判据/取证**:`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` 顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段——无字段变体在 TS 里就是 `{ type: 'x' }`;带载荷变体是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体),细分原因枚举(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`)同样由 ts-rs 生成。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律写在 catch 子句里:`const failure = error.error; switch (failure.type)`,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as X` 再读自己的字段,`reason` 是枚举时再 `switch (payload.reason)`,两处 `default` 都用 `expectNever` 保证漏接编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物保持 ts-rs 原始输出(不再经 prettier / eslint 二次改写,见本节「生成绑定」口径);ts-rs 只写文件、不删文件,变体从有载荷改成无字段时要手动清掉孤立的 `X.ts`(本次 `AuthResponseServerRejected.ts` 就是这样删的)。
|
||||
- **Rust 侧不得把结构化错误降级成字符串**:`refresh_session_inner` 的非权威失败直接返回 `Err(ClientAuthError)`,视图不带 `errorMessage`;一旦折成 `String`,前端就只能拿文案判断,变体信息永久丢失。
|
||||
|
||||
Reference in New Issue
Block a user