Merge remote-tracking branch 'origin/master' into feat/pricing-plan
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m28s
Project CI / Backend tests (pull_request) Failing after 53s
Project CI / Native shell tests (pull_request) Failing after 1m33s
Project CI / Frontend tests (pull_request) Failing after 1m2s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m43s
Project CI / AI game creator shell web tests (pull_request) Failing after 45s
Project CI / Repository checks (pull_request) Failing after 1m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m24s

This commit is contained in:
2026-10-03 13:36:04 +08:00
438 changed files with 18965 additions and 242444 deletions
@@ -1,4 +1,75 @@
# 决策记录
## 2026-10-02 资源编辑远端失败的原始原因穿出到工具错误,资源编辑错误通道补一层 typed
- 背景:轮询到 `status=failed` 时客户端只读 `status`,丢掉平台在同一个响应里给的 `error`(契约 `ExternalEditorGenerationJobResponse.error`),统一写 `terminal_failure_code = remote-generation-failed` 并返回「remote-terminal-failed: 资源编辑生成失败」。平台的可行动原因就此消失:模型与用户卡片只看到一句「失败了」,重试路径(`ensure_resource_edit_phase_resumable`)也只有分类码。这违反 `pitfalls.md`「远端资源编辑终态必须指出唯一出口」里已写下的口径——「首次失败的原始拒绝说明继续由当次错误文案承担」;提交期 HTTP 400 分支(`editor_api_rejection_reason`)兑现了,轮询分支没有。另外 `remote-terminal-failed:` 只是文案前缀(全仓没有 `starts_with` 解析它),在第一句失败文案里与「失败」重复。
- 决策(typed 承载):新增 `ResourceEditError`(`project/resource_editor/error.rs`),只两个变体:`RemoteGenerationFailed { serverMessage }` 承载平台 `error` 原文,`Other(String)` 收尚未分类的失败(`// TODO refactor string-typed`)。两个入口 `derive_local_project_resource`、`resume_local_project_resource_edit` 返回 typed;旧名 `derive_local_project_resource_at` / `resume_local_project_resource_edit_at` 保留为 `Result<_, String>` 外观(映射 `to_user_msg()`),因此 33 个既有测试调用点与两个 Tauri 命令零改动。
- 决策(不用 blanket From):不提供 `impl From<String>`;每处 String 错误显式 `.map_err(ResourceEditError::Other)`,让「还没 typed 化」的边界处处可见,而不是被一次隐式转换吞掉。
- 决策(不按 code 分支):不按 `terminal_failure_code` 分支。它两个写入点最终落到同一个 `phase`、唯一读者只做插值不比较,值域撑不起 policy;字段上加 `// TODO clean unnecessary`。清理前置条件已核实:该字段无 `skip_serializing_if`,`.agent/resource-edits/operations/*.json` 每个文件都带这个 key,而 `ResourceEditLedger` 是 `deny_unknown_fields`、扫描循环里一个文件解析失败会让整个「待恢复资源编辑」列表报错返回。
- 决策(前缀去留):删掉第一句失败文案里的 `remote-terminal-failed:`(轮询与提交期 400 两处)。`ensure_resource_edit_phase_resumable` 里那三个 token 保留:它们与三个 phase 一一对应,是那句重试文案里区分「确定失败 / 已归档 / 待对账」的唯一手段。
- 决策(原文边界):平台原文只进当次错误文案,仍不进账本(`terminal_failure_code` 的写入边界与既有断言不变)。平台 `user_visible_external_generation_error` 已对四种 kind 做 sanitize,图片/视频两种原样透出——与同 wire 的 `canvas_generation.rs` 口径一致,要收边界应改服务端。
- 决策(命名与落点):尚未 typed 化的变体叫 `Other`,不叫 `Message`(后者分不清是「已渲染文案」还是「原始消息」);新错误单独放 `project/resource_editor/error.rs`,不再往主文件里塞类型定义。
- 决策(工具层承载):`RemoteGenerationFailed` 不能到工具层又被压回一句字符串。共用载体放 `agent/tool/error.rs` 的 `RemoteResourceEditFailure { serverMessage }`(两个工具共用的文案只写一份),`CreateOrDeriveResourceError` / `RemoveBackgroundError` 各加 `RemoteGenerationFailed(RemoteResourceEditFailure)` 变体;翻译用显式 `from_resource_edit_error`,不用 `impl From<ResourceEditError>`,远端终态进 `RemoteGenerationFailed`、其余 `Other` 仍落回各工具原有的「失败:<文案>」变体。这样诊断 sidecar 的 `error` 字段(typed enum 整体序列化)天然带上平台原文,LLM 侧拿到的 `message` 也带上。
- 决策(前缀归属,2026-10-02 追加):叶子错误只给事实,不给「谁失败了」的总结前缀。`RemoteResourceEditFailure::to_user_msg` 与 `ResourceEditError::to_user_msg` 都只返回平台 `error` 原文;平台没给就回「服务器未返回错误信息」,不再说「资源编辑生成失败」这种没有信息量的总结。typed 错误新增 `phaseDetail` 字段,但它只作为结构化字段进诊断 sidecar(开发者/LLM 侧看原始值),**不参与用户文案**。前缀由使用者自己加:`agc_create_or_derive_resource` 用「生成或派生资源失败:」、`agc_remove_background` 用「抠图失败:」、桌面命令面用「资源编辑生成失败:」。同一份 typed 错误因此可以同时服务工具面(前缀各随其工具)与桌面面(保留原有文案)。
- 决策(HTTP 兜底与叶子前缀,2026-10-02 追加):叶子只给「服务端 message / code / 原始传输事实」这类事实,不把操作名写进叶子。`game_package_upload/runtime.rs` 三处 `let (_, message)` 把服务端 `code` 丢掉、再拼「读取上传状态失败(HTTP 503)」这类前缀,改成 `message` → `code` → `HTTP {status}`(操作名交给调用方的话术)。`game_distribution_publish.rs` 的 `response_data`(2xx + `ok:false`)同样用上被丢掉的 `error.code`,`account_api.rs` 的 envelope 分支补 `error.code`;服务端没给任何原因时统一回「服务器未返回错误信息」。错误类型自身的单测不再断言 `to_user_msg()` 的字面量(文案是给用户的话术,不是契约),只保留「typed 字段原样序列化进诊断」的结构断言。
- 决策(凭据作用域的 error 类型):`with_direct_editor_api_credentials` 原本把操作限定成 `Result<_, String>`,会把 typed 错误提前压掉。新增 `with_direct_editor_api_credentials_as(operation, credentials_error)` 保留调用方 error 类型,凭据解析失败由调用方显式翻译(这里传 `ResourceEditError::Other`),旧名保持 `String` 语义、零改动。
- 决策(future 装箱):`handle_direct_tool_bridge` 的状态机在调试测试线程的默认栈上已经贴着上限,资源编辑 arm 直接内联会顶穿(`bridge_write_file_waits_on_the_blocking_pool_instead_of_a_runtime_worker` 栈溢出)。桥里四个资源编辑 await 点用 `Box::pin` 只留指针进外层状态机;这是体积问题,不是错误用 `Box`。
- 决策(同类兜底,2026-10-02 追加):`agent/generation/canvas_generation.rs` 的远端 `failed` 分支原来在平台没给 `error` 时兜底成「生成任务失败」,与句首的「平台图片生成任务失败:」重复,改成「服务器未返回错误信息」;`phaseDetail` 不再参与用户文案(只作结构化字段)。
- 改动范围:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`project/resource_editor/error.rs`(新)、`agent/tool/error.rs`、`agent/tool/create_or_derive_resource/error.rs`、`agent/tool/remove_background/error.rs`、`agent/direct_tool_bridge.rs`、`assets.rs`、`docs/project-memory/shared-memory/pitfalls.md`。
- 验证:`cargo check --bin genarrative-ai-game-creator-shell --tests` 通过;`cargo test --bin genarrative-ai-game-creator-shell -- project::resource_editor --test-threads=1` 66 passed(并行跑会有一批 TCP fixture 用例因争用超时,串行全绿,与本次改动无关);`-- agent::tool:: agent::direct_tool_bridge` 47 passed;`npm run check:encoding` 5111 files;`git diff --check` 干净;`cargo fmt` 已跑。
- 关联:`pitfalls.md`「远端资源编辑终态必须指出唯一出口」。
## 2026-10-02 DirectProject 对话:更早历史自动加载、前插锚定与回到底部胶囊
- 背景:右侧对话更早历史只靠常驻按钮「显示更早的对话」拉,且没有任何加载反馈(`historyLoadingRef` 是 ref,渲染不出来);用户滚上去之后没有「回到最新」的入口;展开「执行过程」/工具组/思考块时浏览器保持 `scrollTop`,新展开的正文长在视口下方,在底部展开更是直接顶出可视区。
- 决策:删按钮改自动加载——触顶 24px 与「首帧后内容填不满视口」共用一道门(有更早历史 ∧ 不在加载中 ∧ 无失败记录),失败挂起、只留内联「加载更早对话失败 · 重试」且不自动重试;加载行挂载在列表最上方、延迟 150ms 才显示。前插按「回合 key(`data-turn-key`)+ 块内序号 + 相对列表顶边偏移」冻结锚点,列表显式 `overflow-anchor: none`;加载期间所有补偿还原同一个冻结锚点,加载结束后的下一次补偿再刷新。底部居中 `sticky` 胶囊「回到底部」(不跟随时来了新终态内容改「有新回复 · 回到底部」):距底 48px 阈值、点击平滑滚动并恢复跟随。`ResizeObserver` 观察列表直接子元素(`MutationObserver` 负责子元素变化时重订阅):跟随时任何高度变化贴底,否则冻结刚展开的折叠头,头部锚不住再对齐展开正文顶边;新一回合开始(`turnInFlight` 假转真)强制恢复跟随。滚动所有权(列表 ref、跟随最新、补偿、胶囊显隐)从 `DirectProjectChatView` 搬进 `DirectProjectConversation` 及其同目录 hook,控制器新增可渲染的 `historyLoading` / `historyError` 与 `retryEarlierHistory`。
- 边界:只做 DirectProject;`PlanningChatView` 与 `App.tsx` 里的 `message-history-more` 遗留路径不动,因此该样式保留(后续项已写进 ADR)。新增元素全部用内联 Tailwind,`styles.css` 未改;阈值、文案与判据集中在同目录 `conversationScrollPolicy.ts`,契约见 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](../../adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md),旧「单一事实源」ADR 的分页条目已改为引用它。
- 验证:新增 37 条与实现同目录的用例(阈值与加载门、锚点读取/还原、折叠头冻结与正文对齐、hook 的触顶/填充/胶囊/未读/延迟加载行、组件层的按钮移除与内联重试行)由根 `vitest.config.ts` 的 include 收进门禁;`npm run ai-game-creator-shell:typecheck`、`eslint`(含新文件)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过;滚动观感(顶部加载圈、胶囊显隐、底部展开回贴、历史前插不跳)留真机手动验收。
- 追加(2026-10-02,两个真机缺陷的根因与修正):①「点回到底部只下去一屏、到不了底」——平滑动画期间滚动事件把「跟随最新」翻成假,布局补偿与锚点还原接着写 `scrollTop`,而真实浏览器里任何一次写都会取消正在跑的平滑动画;现在滚动位置由这次程序化滚动独占(滚动事件只在贴底时才结算并交还,补偿整段跳过),滚轮 / 触摸 / 键盘接手立刻交还,避免标记永远挂着。②「展开折叠块没有自动滚动到位」——旧规则只在展开正文比视口还高时才动,正文矮的折叠块(工具组、思考块)展开后正文仍在视口外;现在冻结折叠头之后统一补「刚好露出新展开正文」的最小位移(底边超出就补超出量,比视口还高则对齐正文顶边),两段位移合成一次写。落在 `conversationToggleReveal.ts`(新增纯函数 `toggleRevealDelta`)与 `useConversationScroll.ts`(新增 `programmaticScrollRef`)。
- 验证(2026-10-02 追加):同目录用例补齐到 48 条(新增 `toggleRevealDelta` 六例、hook 的四例程序化滚动用例,其中两例在修正前确实红)全部通过;Chromium 真机脚本复验三处——点回到底部收敛到 `scrollHeight - clientHeight`、底部展开贴到新底、视口外展开补 269px 后正文底边正好贴视口下缘。
- 追加(2026-10-02,换会话复位滚动所有权):`useConversationScroll` 的跟随最新 / `atBottom` / `hasNewReply` / 前插锚点 / 折叠头 / 程序化滚动标记只在挂载时初始化一次,而 `DirectProjectChatView` 切项目时不重挂载——在项目 A 往上滚过再切 B,B 首屏不贴底且胶囊直接显示「有新回复 · 回到底部」。现在把会话身份(`conversationKey`,DirectProject 传项目路径)作为显式信号传进 hook,身份变化即复位这批状态、同步终态指纹并重新贴底;不改成 `key` 重建列表,避免消息列表重建与加载行 150ms 延迟计时重来,同一个项目重开也不算换会话。
- 验证(2026-10-02 追加,换会话复位):`useConversationScroll` 新增两例换会话用例(新会话首屏贴底且不出现胶囊、切会话后在 B 里往上滚只显示「回到底部」而不是「有新回复」),修正前确实红、修正后绿;chat 范围用例全绿。
- 追加(2026-10-02,review 两处内部缺陷的修正):①更早历史读取的守卫从「项目路径相等」改成「世代号相等」(`historyLoadTokenRef`,切项目与每次读取各推进一格)——A→B→A 之后在飞的旧读取又落回同一路径,原守卫会放行,把新一代的加载态、并发闸门与游标一起改掉;现在非最新一次读取落地时整段丢弃。②锚点的块集合按列表缓存(`WeakMap`),由已有的子元素 `MutationObserver` 在同一处 `invalidateTurnBlocks` 失效——原来一个滚动帧里读锚点、还原锚点各查一遍整份列表,长会话下是 O(块数) 的 DOM 查询。
- 验证(2026-10-02 追加,review 修正):新增一例控制器用例(驱动 A→B→A 且同路径新读取在飞,修正前在「旧读取落地」处红)与两例锚点缓存用例(连续收集只查一次 DOM、子元素变化并失效后重新收集);chat 范围用例全绿,`npm run ai-game-creator-shell:typecheck`、`eslint --max-warnings 0`、`npm run check:encoding`、`git diff --check` 通过。
- 追加(2026-10-02,锚点改用稳定块身份):review 第 3 条(`findTurnBlock` 按块序号定位)成立,采纳「换一套块身份」。锚点从 `{回合 key, 块序号, 偏移}` 改为 `{回合 key, 块身份, 偏移}`,块身份即 `DirectChatBlock.key`(`${回合 key}:${条目 itemId}` / 本地说明 `messageId`),只要求同一回合内唯一;展示层给每个可锚定块加 `data-block-key`(`DirectProjectTurn` 的正文块与终态文案、折进 `<details>` 的过程包装块、`ToolCallGroup`、`AgentReasoning` 各自透传),`conversationScrollAnchor.ts` 的收集选择器因此收敛为 `[data-turn-key][data-block-key]`。原因:`renderTurnProcess` 对运行中的回合平铺过程块、对已结束的回合折进一个 `<details>`,收口时整个回合的块序号后移一格,序号锚点会解析到隔壁块并按错误基准写 `scrollTop`。同时补上 review 指出、原方案漏掉的一半:锚点块没有布局盒(被折进收起的 `<details>`)时读锚点跳过它、`restoreTurnAnchor` 判为失败,调用方放弃这次补偿并重新起锚——不回跳,也不按别的块硬对齐。
- 验证(2026-10-02 追加,稳定块身份):锚点纯函数用例改为按块身份构造(`buildList` 传 `[回合 key, 块身份]`),新增「回合收口后块序号整体后移,块身份仍指向同一块且还原成功」「收起的 `<details>` 里的块没有布局盒:读锚点跳过、还原返回 false 且不写 `scrollTop`」;组件层新增用例断言每个带 `data-turn-key` 的块都有 `data-block-key`、同回合内块身份唯一、回合 running→finished 后同一块身份仍在。修正前锚点用例在「收口后按身份定位」处红。
- 追加(2026-10-02,review:动画期间内容变高会把程序化滚动标记卡住):`scrollToBottom` 把点击那一刻的 `scrollHeight` 当动画目标,而流式正文 / 图片撑开会让它在动画期间继续变高;`programmaticScrollRef` 只在「贴底」那次滚动事件里交还,于是动画停在旧目标后标记永远为真——布局补偿整段被跳过(列表不再跟随新内容),胶囊又已按「已贴底」隐掉,用户停在底部之上却没有任何指示和自动跟随。修正:程序化滚动期间布局补偿不写 `scrollTop`,但内容变高时把动画目标重新对准新的底部(`scrollListToBottom(list, 'smooth')`),动画继续跑到真正的底,标记照常在贴底时交还。
- 验证(2026-10-02 追加,动画目标重对准):`useConversationScroll.test.tsx` 新增一例——点击回到底部后内容变高(`scrollHeight` 1200→1500)触发一次布局变化,断言动画目标从 `[1200]` 变为 `[1200, 1500]`、落到新底部后仍能交出控制权;修正前在目标数组处红。
- 追加(2026-10-02,review:错误行的重试按钮在断言式 live region 内):`DirectProjectHistoryErrorRow` 原来把重试按钮渲染在 `<p role="alert">` 里面,而 `role="alert"` 隐含 `aria-live="assertive"` + `aria-atomic="true"`,交互控件会被卷进整段断言性播报、读屏也不一定把它当可聚焦按钮。修正:容器改为普通 `<div>`,`role="alert"` 只包住「加载更早对话失败」文案本身,重试按钮是 live region 之外的兄弟。
- 验证(2026-10-02 追加,错误行结构):`DirectProjectConversation.test.tsx` 新增一例断言 `role="alert"` 节点不包含重试按钮、且文案仍在 alert 内并仍可点击;修正前红。
- 追加(2026-10-02,review:世代号推进时机):`historyLoadTokenRef` 的换项目推进原来只在复位 effect 里,而 `projectPathRef.current` 是渲染期赋值的——交接窗口里守卫不再即时生效:React 的 passive effect 走宏任务、promise 续体走微任务,切换提交之后、复位 effect 之前落地的旧读取拿到的仍是旧世代号,会照常合并条目、写游标、关加载态(复位 effect 随后清掉状态,故终态没坏,但白跑一次跨项目读取且留下瞬时脏状态)。修正:推进移到渲染期,与 `projectPathRef` 同一处、只在路径真的变化时推进;复位 effect 不再推进。安全性依据:同一提交里子组件「填充视口」effect 的判据(`turns` / `historyHasMore` / `historyLoading` / `historyError` / 稳定的 `loadEarlier`)都不变,订阅状态复位本身也是 effect,因此这个窗口里不可能新起一次带旧游标的读取,去掉 effect 里的那一次推进不会放过它。
- 验证(2026-10-02 追加,世代号推进时机):该窗口依赖 React 调度(act 会把 effect 与断言放在同一个作用域里冲掉),jsdom 下无法构造出「旧读取先于复位 effect 落地」的确定性用例,因此没有新增红灯用例;既有的控制器世代用例(切项目、A→B→A)保持全绿,行为等价性由「本窗口内不会有新读取启动」的依赖分析支撑。真机若要硬证据,需在慢读取期间切项目并观察是否多打一次跨项目读取。
## 2026-10-01 Web、后台与 AGC 一键联调
- 背景:Web、管理后台和 AGC 同时开发时,分别启动入口容易产生两套 API/worker/SpacetimeDB,以及重复后台 Vite。
- 决策:新增 `npm run dev:all`,保持 `npm run dev` 现有主站完整栈语义不变;一键入口固定使用 AGC 的 database/data dir,先启动根完整栈,待五个服务就绪后由 AGC 复用该后端,再启动 AGC Vite 与 Tauri,并关闭 AGC 自带后台。
- 影响范围:根开发脚本、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`。
- 决策:把 #228 的修复提升为 `apps/ai-game-creator-shell/src/styles.css` 里的通用契约——`.launcher-main:has(<页面钩子>)` 改纵向 flex 列(`height: 100dvh`,窗口外壳 `height: 100%` 命中时贴合真实舞台;`min-height: 0`、`padding-bottom: 0`),`.launcher-main > <页面根节点>` 统一 `flex: 1 1 auto; height: auto; min-height: 0`;帮助页额外 `overflow-y: auto; padding-bottom: 42px`(它没有内层滚动容器)。契约钩子:`launcher-home-page`、`launcher-help-page`、`launcher-template-library`、`launcher-projects-page`。
- 边界:页面根节点不再写 `100vh` / `100dvh` / `calc(100vh - Npx)`;横幅(`.launcher-promo`)是外壳里的真实行,有横幅就靠 flex 自动少一份,不手算偏移。工作台页(`.game-project-workbench`)在窗口外壳里已有等价契约(`.window-chrome__content ...` 那几条),移动端媒体查询里遗留的 `100dvh` / `calc(100dvh - 32px)` 由外层 `flex: 1 1 auto` 收缩兜住,本次不动。
- 影响范围:`apps/ai-game-creator-shell/src/styles.css`、`src/view/home/index.tsx`、`src/view/template-library/index.tsx`。
- 验证:Chromium 真机量测(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅)——首页 / 模板库 / 项目页改动前后高度一致,帮助页从「被裁 112~150px 且无可滚动祖先」变为正好等于 `.window-chrome__content` 高度并可滚动到底;`npm run typecheck`、`npm run check:encoding`、`git diff --check`、`eslint` 通过。
## 2026-10-02 泥点双余额与会员补差升级方案(拷问定稿)
@@ -68,6 +139,16 @@
- 未修(另开):`redact_absolute_path_tokens` 的无语境绝对路径扫描仍会把 HTML 结束标签、嵌套 JSON 转义里的 `/` 误判成路径,是 issue #553 的根因;本次只删死路径,未改扫描器。
- 验证:`cargo test --bin genarrative-ai-game-creator-shell agent:: -- --test-threads=1`(929 passed / 0 failed / 5 ignored)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
## 2026-10-02 退役AGC独立Agent Runtime与CLI执行面
- 决策:AGC 自建 Agent Runtime 执行面整体退役,按「从未存在」处理——`src-tauri/src` 的 `agent/runtime_driver`、`runtime_protocol`、`runtime_tools`、`runtime_actions`、`runtime_state`、`runtime_adapter`、`agent/prompt.rs`、`agent_native_tools.rs`、`collaboration.rs`、`delegation.rs`、`goal.rs`、`context_compaction.rs`、`isolated_agent.rs`、`provider_handoff.rs`、`provider_retry.rs`、`tool_plan_handoff/`、`user_input.rs` 及专属测试/fixture 删除;`generate_local_game_draft`、`control_agent_run`、`chat_with_game_creator_agent`、`*_game_creator_agent_goal`、`*_game_creator_agent_runtime_*`、`schedule_game_creator_agent_ready_tasks`、`start_game_creator_supervisor_runtime_task` 移出 Tauri `generate_handler!` 与 `check-config.mjs` 白名单;`--agent-run` / `--agent-task` / `--agent-goal-*` / `--agent-resume` / `--agent-context-compact` / runner 状态类 CLI 一并删除,`CliCommand` 收敛为 `LlmStatus | EnvironmentCheck | PreviewServe`。
- 决策(Runner 收缩):外部 Runner 的项目 execution-owner / known-roots / `runner.status` / read-only configure 删除,只保留编辑器桥 RPC(`*.editor.rpc` / `*.editor.ack` / `*.editor.mark_uncertain`)与 `runner.attach_gui_owner` + GUI owner 参与锁 / watchdog;`--agent-runner` 模式保留,`runner.rs` 用 `TODO(retire-runner)` 记录后续整体退役条件。
- 决策(前端与 harness):删除前端 `read/resume/confirm_resume_game_creator_agent_runtimes*` 调用链、`agentRuntimeById` 状态与 `onAgentRuntimeSummariesChange` 透传、`AgentRuntime*` / `AgentGoal*` 类型、`AgentStatusCard` 的 `runtime*` 字段与 `projectAgentRuntimeSummaries` / `formatAgentCardRuntimeStatus`;删除 `agent-runtime-real-e2e*`、`agent-runtime-steer-real-e2e`、`smoke-agent-run-local-provider.mjs`、`llm-transient-fault-proxy.mjs` 及其 CI job 与缓存预热条目。
- 边界:AGC 会话命令、项目权限策略、DirectProject 的 `enqueue/cancel_direct_codex_turn` 链路与编辑器桥不受影响;保留项与备选方案见 ADR。
- 影响范围:`apps/ai-game-creator-shell/src/**`、`src-tauri/src/**`、`scripts/**`、`tests/**`、root / App `package.json`、`.gitea/workflows/project-ci.yml`、`deploy/container/README.md`、AGC 实施计划与 Runtime V1.1 文档。
- 验证:`cargo check --tests --bin genarrative-ai-game-creator-shell`、`cargo test --bin genarrative-ai-game-creator-shell runner::`、`node apps/ai-game-creator-shell/scripts/check-config.mjs`(App 目录内执行)、`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`npx vitest run scripts/project-ci-workflow.test.ts`、`git diff --check`。
- 关联:[【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02](../../adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md)。
## 2026-09-30 release 渠道移除产品名与包名后缀
- 决策:`release` 渠道的正式产品名统一为 `陶泥儿`,Windows NSIS、macOS DMG / updater 归档等由 Tauri `productName` 派生的包名不再包含 `Release` 文本;`identifier=world.genarrative.ai-game-creator.release` 与 `release-win` 更新分区保持不变。
@@ -429,7 +510,7 @@
- 背景:AGC 项目对话曾把大量能力挂在「聊天输入 `/<cmd>`」上(`/history`、`/read`、`/help`、`/status`、`/trace`、`/export`、`/preview`、`/remember`、`/brief` 等),无 GUI 的终端 swarm chat 入口 `--swarm-chat` 又自带一套控制命令(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit`)。两套入口都没有现役调用方,撤回成本却持续存在:命令字面量散落在前端命令分支、润色绕过、摘要模块、`swarm_cli` 终端输入解析、构建期门禁条目和文档承诺里,任何新对话形态都要额外维护这套死词汇表。
- 决策:斜杠命令语义与终端 swarm chat 入口整体退役,按「从未存在」处理。应用侧删除 Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`、`chatPromptPolish` 的 `/` 前缀绕过、`chatCommandMetadata` / `chatCommandHelp` / `memoryCommands` 的命令清单与参数解析、只服务退役 Supervisor 摘要面板的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`、草稿回填死链(前端 `agentPresentation.ts` + Rust `suggested_canvas_tool_call`)、无人调用的 Tauri 命令 `get_game_creation_agent_capabilities` / `get_limited_local_commands`,以及钉住这些字符串的构建期门禁条目与专属测试。终端侧连同入口一并删除:`--swarm-chat`、`src-tauri/src/swarm_cli.rs` 与整个 `swarm_cli/` 目录(命令解析与帮助输出、turn 派发、观察器、报告、专属测试)、`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`、只服务这些命令的 `agent.compact` / `agent.resume` / `agent.run_status` 权限门禁与 `print_runtime_response_stream_status` 打印器,以及只服务终端交互内核的 `agent/interaction.rs` 整层(`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`);该文件只保留自然语言 steer 决策路径 `decide_game_creator_agent_runtime_steer_at`。真实 E2E 的交互式 CLI 管道、`scripts/agent-swarm-test-chat.mjs`、`agentSwarmTestEntry.test.ts` 与 `agc:test:chat` / `agc:test:chat:manual` / `agc:chat` / `agc:swarm` 等 npm 脚本同步删除。
- 保留项:命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计粒度,数组本体由 Rust 策略路径消费)、`needsInitializedChatProject`,以及 `--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` 等非聊天 CLI 控制命令与 Tauri IPC 注册。
- 保留项:命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计粒度,数组本体由 Rust 策略路径消费)、`needsInitializedChatProject`,以及 `--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` 等非聊天 CLI 控制命令与 Tauri IPC 注册。(2026-10-02:这些 CLI 控制命令与对应 Tauri IPC 已随自建 Agent Runtime 整体退役,见同日前条与 ADR。)
- 影响范围:`apps/ai-game-creator-shell/src/**`(Direct 聊天控制器、润色、`project-summary`、`project-workspace`)、`src-tauri/src/**`(`cli.rs`、`main.rs`、`swarm_cli` 整目录删除、`agent/interaction.rs` 收敛、命令注册、canvas 生成、provider / project 测试)、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/**`、`scripts/check-config.mjs`、root 与 App 的 `package.json` 脚本、`tests/**`,以及 AGC 主实施计划文档、Runtime V1.1 文档与 `CONTEXT.md` 术语。
- 验证方式:`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npm run --workspace apps/ai-game-creator-shell typecheck`(含 `check-config.mjs` 的脚本与门禁一致性)、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`cargo check --tests`(告警消息集与基线一致)、`npm run check:encoding`、`git diff --check`;保留的 e2e 套件为 `supervisor-swarm`、`-transient-retry`、`-final-reply-transient-retry`、`-tool-plan-handoff-runner-kill`、`goal-runtime`、`response-stream`、`web-search`、`context-compaction`、`scoped-agents`、`project-skill`、`parallel-read`、`steer-runner-kill`、`process-session`。
- 关联文档:[【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22](../../adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md)、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。(2026-09-24:对应里程碑已完成并删除,结论以 ADR 为准。)
@@ -6532,7 +6613,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
## 2026-07-15 AI 游戏创作 Agent Runtime V1.21 token-aware 持久上下文压缩
- 顺序:MCP 动态工具目录与输出会进一步放大上下文,因此先补 Codex 风格 token-aware compaction,再进入 MCP。当前固定 12 条 conversation/observation 截断不再作为“已具备压缩”的完成证据。
- 配置:`llm` 增加 `contextWindowTokens=128000 / autoCompactTokenLimit=64000 / toolOutputTokenLimit=12000`,`agentLlm` 可逐 Agent 覆盖。预算估算必须包含 function schema;Provider usage 单独标记为真实值,不能与估算混用。
- 配置:`llm` 增加 `contextWindowTokens=128000 / autoCompactTokenLimit=64000`,`agentLlm` 可逐 Agent 覆盖。预算估算必须包含 function schema;Provider usage 单独标记为真实值,不能与估算混用。
- 边界:只压缩旧 Agent/legacy conversation 和当前 run 的旧 observation,保留最近精确 tail;Goal、任务、结构化计划、steer、pending action、project/repository revision、verification、process/join/delegate、receipt 和 finalization 身份保持规范事实,不进入摘要改写。
- 持久化:私有 `game-creator-runtime-context-compaction.v1` sidecar 绑定 Agent/Session、source prefix 指纹、可选 run、summary 指纹、预算与 usage;同源幂等,追加后 revision 单调,前缀漂移失败关闭。context bundle 只绑定压缩元数据,不复制 summary 正文。
- 请求安全:compaction 使用独立 Provider lifecycle、稳定 request slot、零工具和零 web search。未知 started 或 completed 后 sidecar 未提交均按 orphan barrier 进入 reconciliation,禁止自动重发;sidecar 已提交后恢复直接复用。
@@ -8157,7 +8238,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
## 2026-08-22 AGC Tauri 命令调用可达性失败关闭
- 决策:`check-config.mjs` 的 App 调用扫描必须识别现役精确形态:裸 `invoke`、`directInvoke`、素材画布的 `invokeInput` / `invokeAuthenticatedInput` wrapper,以及对象字段 `.invoke`;不以包含 `invoke` 的任意名称、动态命令变量、注释、字符串、模板或正则文本作为可达证据。
- 决策:`check-config.mjs` 的 App 调用扫描必须识别现役精确形态:裸 `invoke`、`directInvoke`、素材画布的 `invokeInput` / `invokeAuthenticatedInput`、认证命令的 `invokeClientAuth` wrapper,以及对象字段 `.invoke`;不以包含 `invoke` 的任意名称、动态命令变量、注释、字符串、模板或正则文本作为可达证据。
- allowlist 边界:前端源码已调用的命令不得继续保留在 explicit native-only allowlist。allowlist 只承载确实由原生窗口或原生侧流程触发、App 源码不直接调用的 handler;源码调用与 allowlist 必须互斥。
- 门禁:逐文件使用仓库锁定的 TypeScript AST 解析,设置文件数量、单文件 / 总源码长度、命令长度和调用数量上限。回归测试同时锁定直接、wrapper、对象字段的正例与诱饵 / 动态 / 畸形输入的反例,并证明删除真实 wrapper 调用后 handler 可达性检查失败,不能由错误 allowlist 继续误绿。
@@ -9196,6 +9277,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 直连入口退役
- 决策(词表):「接单 / 拒单」退役,命令边界的成功与失败改叫「入队 / 入队失败」;旧「接单」的语义角色
@@ -9394,6 +9476,38 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 客户端把后台 `codex` 映射到现有 Codex app-server,把 `cc` 映射到独立 Claude Code CLI adapter;不通过替换 Codex JSON-RPC 可执行文件实现。
- Claude Code 只使用隔离环境和 AGC loopback MCP,禁用原生工具;取消通过独立 Direct 回合进程树回收处理。Codex、provider 和自定义 Responses 链路保持原路径。
## 2026-09-30 内置工具错误改成每工具一个 typed enum,统一错误事件去掉 retryable
- 背景:`agc-tools` 的失败诊断把预算相关三类之外的所有工具失败都写成 `retryable=true`,`code` 由错误文案 `contains("validation-budget-exhausted")` 之类反推;余额不足、非法参数这类条件不变就不会恢复的失败也被呈现为「可重试」,并发排障时只能靠 `metadata.tool` 认是哪个工具出的错。
- 决策(工具侧):每个内置工具在自己的 `agent/tool/<tool>/error.rs` 里定义错误 enum,一个 case 一个变体,用户(以及转述给用户的模型)可见文案写在变体的 `to_user_msg()` 上;捕获处只调 `to_user_msg()`,不解析文案、不分类、不算重试标志。跨工具重复的 case(入参不是对象 / 不认识的字段、项目权限门禁、分页、清单读取、资源登记完成投影、客户端 Direct 回合门禁、宿主执行门禁、未登记工具名)只在 `agent/tool/error.rs` 定义一次,各工具用包装变体 + `From` 复用,不复制文案。
- 决策(宿主侧):工具失败诊断的 `code` 改成稳定的工具名(不再从错误文案反推),开发者信息进 `metadata`:`tool`、脱敏后的 `arguments`、`directTurn`、`dispatchDenied`;`clientTurnId` 仍按回合归属记录,桥未被回合授权时如实记 `null`。
- 决策(schema):统一错误事件(`.agent/runtime/errors/<eventId>.json` 与应用日志身份行)去掉 `retryable` 字段,`AGENT_RUNTIME_ERROR_SCHEMA_VERSION` 升到 `agent-runtime-error.v2`。Direct Codex 用户可见文案里的 `direct-codex-failure:v2 … retryable=…` 由 typed `DirectTurnError::is_retryable()` 判定,不依赖这个字段,前端解析不受影响。
- 决策(MCP 预检收口,同日续做):独立客户端 MCP 的 `validate_*` 不再自带一套文案,改成调用工具桥同一份入参规则,再用该工具错误 enum 的 `to_user_msg()` 渲染 MCP 结果:`write_file_input`、`list_registered_assets_input`、`list_project_files_input`、`list_account_assets_input`、`account_asset_import_inputs`、`resource_generation_input`、`remove_background_input`、`generate_image_input`、`edit_image_arguments`、`prepare_game_art_input`、`web_search_input`、`editor_execute_code_input`、`cocos_execute_code_input` 都是「工具桥校验 + MCP 预检」共用入口;`ToolArgumentsRejection`(入参不是对象 / 不认识的字段)同时供 MCP 与 Runtime 侧虚拟工具观测(`agent/runtime_tools/context.rs`)复用。
- 收口时显式对齐的两处语义差异:① `agc_remove_background` 的 `backgroundMode` / `screenColor` 传显式 `null` 与省略等价(与工具桥其他可选字段同一口径),MCP 不再单独拒绝 `null`;② `.hermes` 并入 `bridge_project_file_is_hidden_control_path` 的保护目录集合,工具桥与 MCP、`preview.rs` 的受控目录口径一致。
- 改动范围:`agent/tool/**`(`error.rs` + 每个工具的错误模块;工具实现仍留在 `direct_tool_bridge.rs`,不搬家)、`agent/direct_tool_bridge.rs`、`agent/direct_tools_mcp.rs`、`agent/runtime_tools/context.rs`、`agent/runtime_error.rs`、`agent/runtime_state.rs`、`agent/direct_runtime/mod.rs`;同步去掉 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 与 `docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md` 身份行字段表里的 retryable。
- 验证:`cargo test --bin genarrative-ai-game-creator-shell agent::` 951 passed / 0 failed(5 ignored);`cargo check --tests` 通过;`npm run check:encoding`、`git diff --check` 通过;前端 `resourceCanvasAssetGenerationQueue`、`resourceCanvasGenerationHostLifecycle`、`appSurface` 套件通过(「项目权限策略拒绝执行:{id}」对外文案保持原字面)。
- 边界(未完成):Windows 专属的 Cocos / Unity / Godot 执行工具(含 MCP 预检)已按同一口径实现,但只做了交叉配置编译校验(Linux 上临时放开 `windows` cfg 后 `cargo check --features cocos-editor-execute,unity-editor-execute,godot-editor-execute --tests`),没有 Windows 真机构建;`agent/direct_validation.rs` 的 Runtime 动作(`run_command` / `run_browser`)与 `direct_tools_mcp.rs` 里 MCP 专有记录协议(Codex 返回记录、skill 资源读取)仍用各自的字符串错误,它们是 Runtime 动作 / MCP 协议而非内置工具。
## 2026-10-01 统一错误事件收成一个 message(去掉 public_text / recovery_hint / detail)
- 背景:`AgentRuntimeErrorEvent` 的三个文本字段在工具桥这条路径上完全退化——`publicText` 与 `detail` 逐字相同,`recoveryHint` 是常量「查看项目错误诊断后处理」,而且三个字段同时进 sidecar 与日志,读者分不清哪个才是「要给人看的话」。核对确认这个事件没有任何读取方:三处调用都是 `let _ = persist_agent_runtime_error(...)`;前端读的 `publicText` 属于 `AgentRuntimeEventRecord`(`runtime_state.rs` 的 runtime event,`app/types.ts` + `features/agent-runtime/model.ts` 的 `formatAgentRuntimeEvent`),是另一个结构;`.agent/runtime/errors/*.json` 只有测试在读,`read_agent_runtime_error_detail` 命令早已退役。这条链路的用途就是写日志与诊断包。
- 决策(字段):事件只保留一个 `message`——由产生失败的 typed 错误在失败现场写好的人类可读文案(工具侧就是 `ToolFailure::to_user_msg()` 的那一句);删掉 `recovery_hint` 与 `detail` 两个字段及其入参。开发者信息不另开字段,全部进 `metadata`:`tool` + 脱敏 `arguments` + `directTurn` / `dispatchDenied`,即「工具 + 参数(脱敏)+ 上下文」。`persist_agent_runtime_error` 由 10 个入参减到 8 个,`agent_runtime_error_app_log_lines` 同步收口。
- 决策(schema 与日志):`AGENT_RUNTIME_ERROR_SCHEMA_VERSION` 是 `agent-runtime-error.v2`,sidecar 由 `publicText / recoveryHint / detail` 收成 `message`(无读取方,不需要兼容层)。应用日志详情行由 `hint=… summary=… detail=… metadata=…` 收成 `message=… metadata=…`,身份行不变(本来就只放程序生成与调用方常量字段)。`message` 在应用日志里取原 `detail` 的 1200 字符预算、在 sidecar 里按 8 KiB 上限:用户只上传 AppData 应用日志、拿不到 sidecar,日志这一份必须是信息量最大的那份。
- 决策(保留项):`direct-codex-failure:v2 … retryable=… summary=…;建议:…` 是前端(`features/agent-runtime/model.ts` 的 v2 正则)要解析的用户可见文案,`retryable` / `recovery_hint` 由 typed `DirectTurnError::is_retryable()` / `recovery_hint()` 判定,原样保留,只是不再进统一事件;`direct-codex` 路径把信息量最大的 `failure.to_string()`(typed Display)作为 `message`。`runtime_state.rs` 那条改为只记原始 `error`(投影后的用户文案已写进 `project.jsonl`,不必再存一遍)。
- 改动范围:`agent/runtime_error.rs`、`agent/runtime_state.rs`、`agent/direct_tool_bridge.rs`、`agent/direct_runtime/mod.rs`;同步修正 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 里 2026-09-15 的「统一事件至少包含 …」字段表与 2026-09-21 的日志两行口径。
## 2026-10-01 工具失败改成 typed 错误穿出 dispatch 边界,统一事件携带原始 error
- 背景:工具桥的派发边界把失败吞成 `Value`——`bridge_outcome(root, result)` 内部就把 typed 错误压成一句 `bridge_tool_failure` 文案,`isError` 由工具与桥手传;诊断只能从结果值里回读 `/content/0/text` 与(上一阶段临时挂在结果上的)`error` 键。结果是「哪一轮、哪个工具、什么结构化事实」在派发期间被降级成字符串,composer 只能靠 `isError=true` 反推分支。
- 决策(dispatch 边界):`handle_direct_tool_bridge` 里的 `dispatch` 现在返回 `Result<Value, ToolCallError>`,失败原样穿出,不再在派发期压成 `Value`。`ToolCallError { message, redact_limit, error }` 是唯一载体:泛型 `impl<T: ToolFailure + ?Sized> From<&T>` 把具体错误 enum 的 `to_user_msg()`、`redact_limit()` 和原样序列化一起带出来——不做跨工具大 enum,不 Box(`?Sized` 让 trait 对象也能转)。`bridge_outcome` 随之删除。
- 决策(composer):工具桥只有一个出口 `compose_direct_tool_outcome(state, tool, arguments, dispatchDenied, outcome)`:`Ok` 补 `isError=false`,`Err` 补 `isError=true` 并落一次诊断。`isError` 由分支决定,工具与桥都不再手传;MCP 完成包仍必须带布尔 `isError`(`direct_tools_mcp.rs::call_client_tool_bridge` 会校验),所以两条分支都写。五条早退(付费 / 写入 / 执行许可取不到、许可任务丢失、结算未落盘 `ReceiptNotPersisted`)也走同一出口:它们以前直接 `return Json(bridge_tool_failure(…))`,不写诊断。
- 决策(诊断字段):`AgentRuntimeErrorEvent` 新增 `error`——产生失败的 typed 错误 enum 的原样序列化,`Value::Null` 表示该调用方(`agent/runtime_state.rs` 的终态公开消息)没有 typed 错误;`message` 仍是给人(以及转述给用户的模型)的那句话。应用日志详情行由 `message=… metadata=…` 扩成 `message=… error=… metadata=…`,`error` 取 400 字符预算(`AGENT_RUNTIME_ERROR_APP_LOG_ERROR_CHARS`)。`AGENT_RUNTIME_ERROR_SCHEMA_VERSION` 升到 `agent-runtime-error.v3`(sidecar 无读取方,不需要兼容层)。sidecar 里的 `error` 与既有 `metadata` 一样按原文落盘、只在应用日志里脱敏:sidecar 留在项目内不上传,上传的日志那份已经过 `redact_agent_runtime_error`。
- 决策(周边收口):`bridge_tool_result(text, images)` 去掉 `is_error` 入参,模板只剩 `{ content }`;`record_completed_regeneration` 不再用 `isError` 判「能否缓存这次美术重生成」——走到那里的只有 `Ok`(失败会 `?` 上抛),`ArtRegenerationAuthorizationRejection::CompletedResultNotSuccessful` 因此退役。
- 决策(软失败清零,同日续做):不接受「工具调用本身成功、载荷里写着失败」的软失败,十处全部改成各工具自己的 typed 变体,载荷原样进变体、由 `to_user_msg()` 拼成给模型的文案:`ImportAccountAssetsError::ImportFailed / ImportPartial`、`RunValidationError::ValidationNotPassed`、`BrowserPlaytestError::PlaytestNotPassed`、`EnvironmentCheckError::EnvironmentNotReady`、`ApplyPatchError::PatchNotApplied`、`EditorExecuteError::ExecutionNotCompleted / ExecutionUnconfirmed`、`CocosExecuteError::ExecutionBusy / ReconciliationPending / ExecutionNotCompleted`。于是 `isError` 与执行租约的 `passed`(`result.is_ok()`)重新对齐:execute 类失败仍落 `ExecutionPhase::Draining`。
- 决策(证据图):`ToolFailure` 增加 `attached_images() -> Vec<String>`(默认空),`ToolCallError` 增加 `images`;composer 的 `Err` 分支在 `bridge_tool_failure` 的成文结果上追加 MCP image block。带图的两个变体(`ValidationNotPassed` / `PlaytestNotPassed` / `ExecutionNotCompleted`)把截图正文放在 `#[serde(skip)]` 字段里:图要回到结果里,但 base64 不能进 sidecar 的 `error` 正文。`bridge_validation_result` 由泛型 `?` 改成接受一个 `fn(Value, Vec<String>) -> E` 构造器,验证与试玩共用同一份 `passed` / 截图投影。
- 改动范围:`agent/direct_tool_bridge.rs`、`agent/runtime_error.rs`、`agent/tool/error.rs` 与 23 个 `agent/tool/<tool>/error.rs`(补 `serde::Serialize`,`EditorKind` 同样补)、`agent/tool/run_validation/error.rs`、`agent/tool/browser_playtest/error.rs`、`agent/tool/environment_check/error.rs`、`agent/tool/apply_patch/error.rs`、`agent/tool/import_account_assets/error.rs`、`agent/tool/editor_execute/error.rs`、`agent/tool/cocos_execute/error.rs`、`agent/tool/prepare_game_art/error.rs`、`agent/generation/canvas_generation.rs`(并发测试改用 `Result`)、`agent/runtime_state.rs`、`agent/direct_runtime/mod.rs`;同步修正 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的统一事件字段表与应用日志两行口径。
- 验证:`cargo check --bin genarrative-ai-game-creator-shell --tests` 通过(无新增警告);Windows 专属的 Cocos / Unity / Godot 执行路径在 Linux 上临时去掉 `#[cfg(all(windows, …))]` 的 `windows` 条件后,用 `cargo check --features cocos-editor-execute,unity-editor-execute,godot-editor-execute --tests` 交叉编译校验通过,随后原样还原(没有 Windows 真机构建);`cargo test --bin genarrative-ai-game-creator-shell -- agent::` 951 passed / 0 failed(5 ignored);`-- agent::direct_tool_bridge:: agent::direct_tools_mcp:: agent::runtime_error::` 73 passed;`-- agent:: tests::project::` 1076 passed / 2 failed,两条都是并行负载下的已知 flake(`agent::runtime_actions::provider_request_builders::tests::art_director_request_exposes_canvas_only_for_the_keyed_owner_route` 与 `tests::project::background_agent_runtime_can_generate_platform_art_asset`),单跑各自通过;`npm run check:encoding`、`git diff --check` 通过。
## 2026-09-30 合入 master 时把 Claude Agent SDK sidecar 归位到随包资源准备步骤
- 背景:master `fb130d184` 新增 `cc` 执行模式与 Claude Agent SDK sidecar,sidecar 的 staging 写在 `build.rs`(构建期 `remove_dir_all` + 从 `node_modules/@anthropic-ai/**` 复制 `resources/claude-agent`),同时把 `resources/claude-agent` 映射进**基线** `tauri.conf.json`。本分支的 M1–M3(issue #519)已把「构建期写随包资源」定性为结构问题,合入时必须按同一套架构落地,不能把写入分支带回来。
+102 -69
View File
@@ -2,6 +2,32 @@
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
## 2026-10-01 用户输错一次密码被当成"客户端出问题了"引导上报
- **现象**:登录页密码输错(或密码长度不合规)后弹出「发现问题」,报告面板「错误事件(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)。
- **判据/取证**:`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` 重新导出,生成物随后交给 prettier;ts-rs 只写文件、不删文件,变体从有载荷改成无字段时要手动清掉孤立的 `X.ts`(本次 `AuthResponseServerRejected.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`。
## 2026-10-01 Windows `dev:all` 启动 npm 子进程报 `spawn EINVAL`
- **现象**:`npm run dev:all` 能完成 AGC Vite 端口预留,但在启动根开发栈前输出 `spawn EINVAL`。
- **原因**:Windows 的 `npm.cmd` 是批处理入口;Node `child_process.spawn('npm.cmd', args, { shell: false })` 会直接返回 `EINVAL`,还未执行根 `npm run dev`。
- **处理**:`scripts/dev-all.mjs` 在 Windows 使用 `shell: true`、`windowsHide: true` 启动 npm 子进程;POSIX 仍使用独立进程组,退出时按进程组收束。
- **验证**:Windows 实测根开发栈已启动并完成端口漂移(Web `3001`、API `8084`、worker `8085`、SpacetimeDB `3104`、后台 `3105`),之后 AGC 因当前工作区缺少 `@anthropic-ai/claude-agent-sdk` 退出;dev:all 已收束根栈进程。
## 2026-10-02 AGC 页面在自绘标题栏外壳里自己算 `100vh`:底部被裁而且没得滚
- **现象**:帮助页(使用指南 / 联系客服 / 更新日志)在矮窗口里底部卡片看不到,把窗口拉高才出现;外壳 `.launcher-main { overflow: hidden }` 之下没有任何可滚动祖先,页面既滚不动也裁得干净。首页在通知横幅出现时用 `h-[calc(100vh-32px)]`,同样把窗口高度当成了舞台高度。
- **原因**:AGC 桌面外壳是自绘标题栏(`--window-chrome-height`;窗口 100vh=800 时舞台只有 750),页面根节点写 `100vh` / `100dvh` / `calc(100vh - Npx)` 就比真实舞台高一整个标题栏,差额被外壳裁掉;横幅是 `.launcher-main` 里的真实行,再写 `-32px` 等于重复扣一次。帮助页还没有内层滚动容器,连「内容超高就在内部滚动」这条兜底也不存在。
- **处理(现行口径)**:页面高度只由外壳分配——`apps/ai-game-creator-shell/src/styles.css` 里 `.launcher-main:has(<页面钩子>)` 是纵向 flex 列(`height: 100dvh`,窗口外壳命中 `height: 100%` 时贴合真实舞台),`.launcher-main > <页面根节点>` 统一 `flex: 1 1 auto; height: auto; min-height: 0`,帮助页这类没有内层滚动容器的再加 `overflow-y: auto`。页面根节点一律不再写 `100vh` / `100dvh` / `calc(100vh - Npx)`;有横幅就靠 flex 自动少一份,不要手算偏移。
- **验证**:真机判据是 Vite + Chromium 量页面根节点是否正好等于 `.window-chrome__content` 的高度(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅),帮助页应可滚动到底。
## 2026-10-02 固定试玩误判祖先的指针穿透样式
- `pointer-events:none` 不会强制禁用整棵子树;后代显式 `auto` 可以恢复命中。控件探针只检查目标的计算样式,继承未覆盖的 `none` 仍拒绝;可见性、遮挡、disabled 与 inert 保留各自检查。
- 修复和回归必须经过生产输入入口及可信事件驱动的状态变化,不能用程序化点击证明真实可玩。双视口 generic 回归与真实触摸验收需要区分,详见 AGC 实施计划“固定试玩控件的指针命中边界”。
## 2026-10-01 Rust 分片编译失败只剩汇总错误
- **原因**:`--message-format=json` 把编译诊断写到 stdout;只读取 `compiler-artifact` 的运行器会丢弃 `compiler-message`,CI 只能看到「due to 1 previous error」。
@@ -164,6 +190,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); }````)。文本本身「看起来没问题」,容易被当成渲染器坏了。
@@ -313,8 +340,8 @@ Copy Artifact 插件在**非 SYSTEM 认证**下按「认证用户」判权:只
- 现象:「生成背景音乐」再次提交 0.1 秒就失败,卡片只有 `remote-terminal-failed: 远端资源编辑已明确失败,不允许再次请求`,既没有原因也没有下一步。
- 原因:上一次同 `operationId` 的请求被平台确定性拒绝(HTTP 400 或任务 `failed`)后,账本落到 `remote-failed`,之后所有重试都在 `ensure_resource_edit_phase_resumable` 失败关闭;唯一出口是「待恢复资源编辑」里的移出恢复队列,但终态文案没有指向它。
- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担。
- 验证:`remote_failed_status_is_terminal_and_can_only_be_archived`、`submission_bad_request_is_terminal_while_gateway_failure_requires_reconciliation` 等资源编辑用例继续通过,账本序列化不含上游失败原文。
- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担——轮询终态这条路由此改成把平台 `error` 原文装进 `ResourceEditError::RemoteGenerationFailed` 原样带出(2026-10-02),工具层经 `RemoteResourceEditFailure` 转发时保留 `error` 与 `phaseDetail` 两个原始字段(`phaseDetail` 只进诊断,不当用户文案);`to_user_msg` 只给 `error` 原文,平台没给就说「服务器未返回错误信息」。前缀由使用它的工具/命令自己加(不再统一压成「资源编辑生成失败」一句,也不再多一层无信息前缀);第一句失败文案不再带 `remote-terminal-failed:` 前缀。
- 验证:`remote_failed_status_is_terminal_and_can_only_be_archived` 断言失败文案带出平台 `error` 原文、同时账本序列化不含原文;`background_removal_remote_failure_keeps_manifest_without_result` 覆盖平台没给 `error` 时的兜底文案;`submission_bad_request_is_terminal_while_gateway_failure_requires_reconciliation` 等资源编辑用例继续通过。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`。
## Tauri `--no-sign` 会连带跳过 updater 签名
@@ -496,7 +523,7 @@ Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\`
- **现象**:把 `Native shell tests` 拆成客户端三个 job 后,如果只跑 `npm run check:native-shells:release`,静态契约和壳运行时门禁都不会执行;如果只跑 `--groups=contract`,`desktop-release-binary-artifact` 又会因为缺少 `build/native/desktop/` 产物而失败。
- **原因**:分组是执行范围,不是"额外检查"。`desktop-release-binary-artifact` 断言依赖同 job 内的 `desktop-shell-stage-release-binary` 步骤,所以它归 `release` 组,不能放进 `contract`;反过来,任何"只跑一组"的命令都不能被当成完整门禁。
- **处理**:分组与 job 的对应关系固定为 `contract`+`shells`+`release` → `Native shell tests`,`agc-web` → `AI game creator shell web tests`,`agc-rust-shard-1..2` → `AI game creator shell Rust lane 1/2`,`agc-rust-shard-3..4` → `AI game creator shell Rust lane 2/2`,`agc-rust-smoke` → `AI game creator shell Rust smoke`,`agc-rust-crates` → `AI game creator shell Rust crates`;`scripts/project-ci-workflow.test.ts` 校验"每个分组恰好被一个 lane/job 调用一次"和"CI 不再调用全量 `npm run check:native-shells`",新增分组必须同步门禁脚本、根脚本与 workflow 三处。
- **易错点**:① 拆 job / 改 job 名后要确认分支保护里没有残留已不再上报的旧 job 名(本仓库现在不配 required context,只需人工确认 CI 结果,见置顶条目的「分支保护口径」);② 每个 job 只预热自己会构建的 Cargo 依赖,`agent-run:smoke` 因为会 spawn `cargo` 必须与 AGC 壳的依赖预热同 job;③ 本地全量 `npm run check:native-shells` 仍会串行跑完所有分组,用它作为本地完整门禁,不要用单组脚本冒充。
- **易错点**:① 拆 job / 改 job 名后要确认分支保护里没有残留已不再上报的旧 job 名(本仓库现在不配 required context,只需人工确认 CI 结果,见置顶条目的「分支保护口径」);② 每个 job 只预热自己会构建的 Cargo 依赖,需要 spawn `cargo` 的 smoke 必须与 AGC 壳的依赖预热同 job(原 `agent-run:smoke` 已随自建 Agent Runtime 退役,2026-10-02);③ 本地全量 `npm run check:native-shells` 仍会串行跑完所有分组,用它作为本地完整门禁,不要用单组脚本冒充。
- **关联**:`.gitea/workflows/project-ci.yml`、`scripts/check-native-shells.mjs`、`scripts/project-ci-workflow.test.ts`、`.gitea` 分支保护设置。
## 2026-09-24 重复 `#[test]` 属性会让 Rust 分片门禁报「同一用例被分到两片」
@@ -981,16 +1008,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:AppSurface 先用全新但内容相同的 Agent 结果数组 rerender,断言图读取仍只有一次且原卡片 DOM 保持连接;再增加真实资源并延迟第二次图响应,断言旧卡片在刷新窗口持续挂载,新图返回后新增卡片正常出现。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`。
## Supervisor steer 不能只有内部排队事件(2026-08-10)
- 现象:自主制作期间继续向项目总控发消息,用户消息已进入同一 Run,当前 Provider 也被中断并重新规划,但普通工作台短暂的提交状态消失后一直没有回复,直到整轮制作最终收束。
- 原因:steer 只持久化用户消息与内部 `steer.queued` 事件;公开事件投影又明确排除 `steer.*`。普通工作台提交后会用后端 conversation 覆盖本地消息,因此仅追加临时前端气泡也无法稳定跨刷新显示。
- 处理:根 Project Supervisor 的 steer 进入 durable `queued` 后,先写“正在判断、当前任务继续”的公开确认,再由独立 `steer-decision` LLM turn 返回自然语言回复和 `interruptCurrentProvider`。状态询问、解释和不冲突补充默认不中断;明确停止、改向或会使在途方案过期时才允许请求中断。判定和回复按 `run + steer` 持久幂等,刷新后仍可见;判定失败时继续当前任务,并在下一安全边界应用 steer。
- 并发边界:steer 入队、Runner `runtime.steer` 通知都不得直接触发 Provider interrupt。Codex app-server 的判定使用独立节点,不能等待主节点 turn 锁;判定为 true 后也只能中断 `appliedSteerCursor < steer.sequence` 的旧 Provider 请求,已经消费该 steer 后启动的新请求不可被误杀。已经开始的工具和外部动作不强杀,完成 observation 后再消费 steer。
- 验证:真实 mock LLM 回归必须覆盖状态询问回复且 `interruptCurrentProvider=false`;持久重放只保留一条语义回复;steer 入队后旧 Provider 继续运行,判定为 true 后才中断;新规划 Provider 的 cursor 已包含该 steer 时即使旧判定为 true 也不能中断。前端同秒多条消息保持“用户补充 → 判断提示/语义回复”的关联顺序。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs`、`apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs`、`apps/ai-game-creator-shell/src/features/agent-runtime/model.ts`。
- 2026-09-23 更新:`agent/interaction.rs` 与 `steer-decision` LLM 判定链已整体删除,本条中「判定 LLM / `interruptCurrentProvider` / 只中断旧 cursor」的实现细节仅作历史记录;现役语义是 steer durable 入队后由 `runtime.steer` 唤醒,并在下一安全边界应用。
## Jenkins 异步备份不能用 nohup 脱离作业
- 现象:Stdb Publish 成功,上传日志只留下“已获取进程锁 / 上传已有备份 / 目标对象”,没有成功或可捕获错误;本地 tar.gz 和 `uploadStatus=deferred` manifest 每次发布后继续增长。
@@ -1023,30 +1040,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:锁定 `generate-ui-design.image_size = ["1K", "2K"]`,三个可切换图片模型的工具都接入共享 `gpt-image-2 -> image_size = ["1K", "2K"]` 条件,以及视频 fast 条件没有内层 `required`、其 `then.resolution = ["480p", "720p"]`;同时保留运行时拒绝 `gpt-image-2 + 0.5K` 与 `seedance2.0-fast + 1080p` 的测试。
- 关联:`server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs`、`docs/【编辑器】画布Agent对话面板-2026-07-03.md`。
## 重复成功的 agent.message 不能被当成新的 Runtime 进展
- 现象:专业 Agent 已把一条定向消息写入目标 Session,却在后续 planning 中反复发送相同正文;目标会话看起来没有重复消息,但 Provider 请求持续增长,run 可能长期不返回自身终态回执。
- 原因:conversation 层的 messageId 幂等只能阻止重复落盘。若每个新 Runtime action 的 `status=ok` 都进入上下文进展指纹,相同 durable no-op 会不断刷新 6 轮停滞窗口;只检查目标会话条数无法证明 action loop 已有界收束。
- 处理:消息语义键必须包含来源 Agent/run、目标 Agent/已解析 Session 和清洗截断后正文 SHA-256;conversation message、`conversation.message` 和 `agent.runtime.agent.message` 各自 exactly-once。重复调用继续完整记录自己的 action/observation/receipt,但私有 observation 固定返回 `messageAppended=false`,ContextWindowTracker 只忽略这一精确 no-op,不能忽略不同正文的新消息。专业 Agent prompt 同时明确中途消息不能替代自身 final response。
- 验证:`background_agent_runtime_bounds_duplicate_agent_message_livelock` 必须真实驱动 6 个相同指纹、不同 actionId 的消息动作,证明 action/observation/receipt 各 6 条,目标消息和两类消息审计各 1 条,后 5 次不算进展,第 6 轮保留 `in_progress` 计划并进入 `budget-exhausted`,没有第 7 次 Provider 请求、context compaction 或 completed。另保留 `agent_runtime_context_window_counts_distinct_agent_message_bodies`,防止把真正不同的新消息误压成 no-op。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## Swarm E2E 的隔离 AppData 不能建在正式 AppData 里面
- 现象:真实 suite 自称使用隔离配置,但一次性 AppData 出现在正式 AppData 子目录;源目录 watcher、配置副本计数和清理归属变得含糊,Runner 还可能把临时 endpoint 或运行态写进正式目录树。
- 原因:把 `mkdtemp` 前缀拼在 source config dir 内,只隔离了文件名,没有隔离目录所有权;source-dir guard 无法区分 suite 自己的合法子目录写入与污染,失败清理也可能触碰正式目录边界。
- 处理:需要保护正式配置的 suite 一律在 `dirname(realConfigDir)` 下创建 sentinel 管理的 sibling AppData,并要求 realpath 后与源目录同父、互不包含。配置只使用私有副本或受控 hardlink/overlay,启动 CLI/Runner 全部指向 sibling;清理前核对 sentinel、源配置 inode/hash/link count、source-dir 前缀事件、正式 endpoint 身份和正式 CLI 调用计数,随后只删除拥有明确 token 的临时目录。
- 验证:真实报告必须同时满足 `isolatedAppDataUsed=true`、`sourceAppDataDirectoryUntouched=true`、`sourceRunnerEndpointUnchanged=true`、`formalConfigCliCallCount=0`、配置副本校验和 `AppDataCleanupPerformed=true`;项目选择 `--keep-project` 时也不能改变 AppData 自动清理。
- 关联:`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## 异步 Runtime 测试不能把 child idle 当成终态结果已发布
- 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表;或者 Runtime 已显示 completed / failed,Goal、conversation、Agent DB 审计和 per-Agent lock 仍未完成,完整 Rust suite 里出现低概率失败,单独重跑通常通过。
- 原因:Runtime state、Goal sidecar、终态 result、conversation、审计记录、handoff 清理和执行 lane 释放不是同一个原子观测点;测试只等待 idle / failed 会在同一后台 drain 的 durable 收尾前抢先断言。
- 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内等待业务目标终态;需要断言同一 drain 的后续副作用时,同时以 per-Agent runtime task lock 释放为 fence,命中后重新读取投影。join 场景继续重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。
- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒;Goal、loop-budget、finalization 与 Supervisor reconciliation 测试必须在目标 status / phase 与 Agent lane 同时收束后再读取最终副作用。`background_agent_runtime_marks_response_plan_step_failed_when_final_reply_fails` 和 `background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies` 同样必须经过该 fence 后再断言 `turn.failed` 或 `agent.runtime.completed` 审计。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`。
## CI root 环境不能用文件只读权限注入写失败
- 现象:本地测试把 conversation 文件设为 readonly 后能稳定得到写入失败,Gitea Actions 中同一断言却发现写入成功并继续执行任务。
@@ -1055,14 +1048,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:在普通本地用户和 root 容器中分别运行用户消息、assistant 最终回复持久化失败测试,均应进入相同 durable phase 并通过恢复断言。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`。
## Ubuntu 容器不能把 chromium-browser 的 Snap 占位包当成 CI 浏览器
- 现象:AI 游戏创作壳的 1132 条 Rust 测试全部通过,尾部 `agent-run:smoke` 却以 `spawn google-chrome ENOENT` 失败;直接给 Ubuntu 24.04 job 安装 `chromium-browser` 仍拿不到可执行浏览器。
- 原因:Ubuntu 24.04 仓库里的 `chromium-browser` 是 Snap 过渡包,普通 Docker job 没有 snapd 宿主能力;固定 job image 也不预装 Google Chrome。脚本回退到命令名 `google-chrome` 后只能在本机通过,在干净 Runner 中必然 ENOENT。
- 处理:Native job 通过 Google 官方签名 APT 源安装 `google-chrome-stable`,安装后先执行 `google-chrome --version`;smoke 继续真实启动 headless 浏览器验证 DOM / canvas,不允许因 CI 缺浏览器而跳过或降级为静态 HTTP 检查。
- 验证:固定 Ubuntu 24.04 job image 内先确认 `apt-cache policy chromium-browser` 仅为 Snap 占位,再安装官方签名包并运行 `google-chrome --version`;Gitea Native job 最终必须在 1132 passed / 5 ignored 后继续通过 `agent-run:smoke`。
- 关联:`.gitea/workflows/project-ci.yml`、`apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs`。
## PTY 测试不能假设输入回显与后续输出必然分行
- 现象:PTY 环境隔离用例偶发得到 `你好BRIDGE_ENV:`,而不是独立的 `你好` 与 `BRIDGE_ENV:` 两行;真实私有环境变量并未泄漏,但整行相等断言失败。
@@ -1071,30 +1056,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:`process_session_pty_uses_private_environment_and_redacts_public_records` 对 `BRIDGE_ENV:` 使用行尾匹配,并保留真实私有环境变量、stdin 正文与公共记录泄漏扫描。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session.rs`、`apps/ai-game-creator-shell/src-tauri/src/command_output.rs`。
## 自主 Swarm 验收不能把父 project.verify 当成意外确认动作
- 现象:两个专业 Agent 已完成初始交付,Supervisor 在语义 repair 前合法执行项目宿主验证,但 E2E harness 把所有父 run pending action 一律拒绝,导致真实协作链在业务逻辑正常时提前失败。
- 原因:验收器把“repair 前不允许父 Agent 绕过专业工作”错误实现成“父 run 不能出现任何确认动作”,混淆了 Supervisor 自己的 `project.verify` 与会改变专业交付/文件的意外动作。
- 处理:确认过滤器必须按 owning run 和 tool 精确判断。repair 前允许当前父 run 的 `project.verify`,仍拒绝其它未列入场景合同的父 pending action;专业 Agent 的修改和验证继续按各自 run、policy 和预期确认集合处理。允许确认不等于通过验收,最终仍由 host oracle、最新 revision verification、delivery/claim/repair 和唯一回复共同裁决。
- 验证:自主 suite 必须出现有效 `hostVerificationPassed=true`,同时保持恰好 2 个初始 + 1 个 repair delivery、父计划完成、意外 pending 为 0、Runner 强杀恢复和唯一 Supervisor assistant;若放宽后出现额外父写动作,场景必须失败而不是吞掉。
- 关联:`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## Provider 全成功的真实报告不能证明显式重试可用
- 现象:真实 Swarm 报告显示全部 Provider lifecycle completed,E2E 的 retry validator 也没有报错,于是文档把“支持瞬态重试”一并写成已真实验收。
- 原因:validator 只在实际出现 failed lifecycle 时校验 retry audit;`failed=0 / retry=0` 会自然通过。随机等待外部网络故障既不可重复,也无法在故障和重试之间证明副作用仍为 0。
- 处理:为重试单独建立 fail-first loopback proxy。在正式 AppData 同级创建 sentinel 管理的一次性目录,只覆盖其中一个目标 Agent 的 base URL 和重试配置;首个 POST 在正文进入 upstream 前断线,第二个请求由 forwarding gate 暂停。gate 内交叉检查 failed lifecycle、retry audit、request slot/identity、action、pending、receipt、delivery、claim、assistant、project revision 和目标产物,再显式放行真实 Provider。代理不能记录 URL、headers 或正文,不能跟随 redirect,必须可幂等清理;启动 CLI/Runner 时同时设置合并后的 `NO_PROXY / no_proxy` 并显式加入 loopback,不能假设开发机已正确配置代理绕过;source-dir guard 禁止本 suite 前缀进入源目录,配置和 endpoint 身份保持只读并逐字复核。不要用源目录 mtime/ctime 归因,正式 Runner heartbeat 会并发改变它。完整链在 checkpoint 后失败时,partial report 也要保留已取得的 identity、slot 和零副作用证据,不能退回模板默认值。
- 验证:`npm run test -- apps/ai-game-creator-shell/tests/llmTransientFaultProxy.test.ts` 覆盖故障、暂停、base path、流式转发、fallback、隐私和清理;`npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-real-e2e -- --config-dir <AppData>` 必须得到恰好 1 failed/1 retry、重试前副作用全 0,并继续通过完整 Swarm/Runner 恢复和零泄漏门禁。
- 关联:`apps/ai-game-creator-shell/scripts/llm-transient-fault-proxy.mjs`、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## Agent 真实验收的阶段等待必须同步观察 Runtime 终态
- 现象:真实 Provider 已因 transport、格式修复或其它不可恢复错误把 task/Runtime 写成 failed,专项验收仍在等待某个 pending action、observation 或 receipt,直到 30 分钟总超时才返回。
- 原因:阶段等待只轮询“想看到的成功证据”,没有同时读取 owning Agent/run 的最新 task 与 Runtime phase;外部错误发生在该证据之前时,目标条件永远不会出现。
- 处理:所有分钟级阶段等待都要在每轮先检查 owning task 的 failed/cancelled/budget-exhausted,以及 Runtime 的 needs-reconciliation;命中后立即抛出带阶段前缀的结构化错误。正常 pause 必须保留为可恢复状态,不能被 fail-fast 当失败;Runner 强杀后的 paused 稳定窗口继续按签名零推进单独验证。
- 验证:用正式 `goal-runtime` 观察 Provider repair transport failure,确认部分报告立即保留成功/repair 协议计数、生命周期闭合和零泄漏证据;随后完整复跑仍能通过 Goal edit/pause/Runner kill/resume/finalization,证明 fail-fast 未破坏正常恢复路径。
- 关联:`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## 图片生成的 K 档不能靠回图后缩放实现
- 现象:用户选择 2K 时占位框看起来是 2K,最终资源元数据也显示为 2K,但模型请求实际仍是固定 1K 或竖版回落尺寸;画面只是后端放大后的低分辨率结果。
@@ -6248,3 +6209,75 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **处理**:保留预检失败关闭语义,但提取 Rust 返回的安全错误码;对无错误码的 Tauri 调用故障显示独立 IPC 提示,并仅对这类瞬态调用失败重试一次。首页调用方必须透传该安全提示,不能重新覆盖为通用文案。
- **验证**:`apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx` 覆盖 ready、具体宿主错误码、IPC 失败、无错误码 blocked 和 planning 绕过;同一 Windows 安装包 CLI 实测 Node `v22.22.2`、npm `10.9.7`、Edge desktop/mobile PNG 均通过。
- **关联**:`apps/ai-game-creator-shell/src/features/app-shell/homeWebPreflight.ts`、`apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts`、`apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx`、`apps/ai-game-creator-shell/src-tauri/src/environment_check/web_creation.rs`。
## 2026-10-02 AGC 首页把创建失败覆盖成通用文案,「项目已建好」被说成「创建未完成」
- **现象**:首页自动创建在项目目录已经建好之后失败(策划运行时切换、附件导入、进入工作台抛错)时,浮层显示「创建未完成,请重试」,而同一页面同时列着「已创建的工作区:…」;用户会以为没建成功,再建一遍。
- **根因**:`useHomeProjectCreation.ts` 把底层文本拼进「工作区已创建;未能进入项目开发:<原因>」再抛出,`src/view/home/index.tsx` 的 `catch` 又无条件覆盖成写死的通用文案。注释声称“详细错误已由工作区创建控制器记录”,实际只有一次 `setStatus`,紧接着就被覆盖,没有任何落盘;同一处的包装文案还写死成「首条需求投递失败」,把运行时切换、附件导入等失败都指认成“投递”这一步。
- **处理**:新增 `homeCreationFailure.ts` 作为唯一出口:由宿主机器码(`project-revision-conflict` / `web-build-timeout` 这类连字符标识)、Tauri/WebView 传输形状(`Failed to fetch`、IPC 自定义协议失败)与专用兜底共同决定用户文案,并按“工作区是否已建好”分成两条;宿主原始文本不再上屏,改为 `appendWebviewLog` 写进 `application.log` 作为证据(脱敏沿用 `normalizeDiagnosticText`)。控制器抛出 `HomeCreationFailure`(带 `userMessage` 与 `diagnostic`),首页只透传它的 `userMessage`,对未知值一律落回专用兜底;包装文案改为中性的「未能进入项目开发」,不再指认某一步。
- **验证**:`apps/ai-game-creator-shell/tests/homeCreationFailure.test.ts` 5 条覆盖机器码、传输故障、无码兜底、原始文本只进诊断、首页出口;`npx vitest run tests/appSurface.test.ts tests/homeCreationFailure.test.ts tests/homeWebPreflight.test.tsx tests/appUpdate.test.ts` 215 passed / 9 skipped;`npx tsc --noEmit -p tsconfig.json` 与 `npm run typecheck` 通过。
- **关联**:`apps/ai-game-creator-shell/src/features/app-shell/homeCreationFailure.ts`、`apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts`、`apps/ai-game-creator-shell/src/view/home/index.tsx`、`apps/ai-game-creator-shell/tests/homeCreationFailure.test.ts`、`apps/ai-game-creator-shell/tests/appSurface/home.suite.ts`。
## 2026-10-02 AGC 宿主预检把浏览器验证的多种原因压成一个 browser-failed
- **现象**:预检失败一律提示浏览器相关问题,但同一安装包 `--environment-check` 可能返回 `status=ready`;宿主侧真实原因可能是证据目录不可写、报告写盘失败、宿主取消或浏览器收束未确认。
- **根因**:`environment_check/web_creation.rs` 把 `validate_local_preview_in_browser` 的所有 `Err` 统一映射成 `web-preflight-browser-failed`(验证层实际会返回“未发现可用浏览器”“启动浏览器超时”“启动浏览器失败:<OS 码>”“宿主已停止本轮浏览器验证”“browser-cleanup-unconfirmed”“evidenceRoot …”“页面导航失败”等)。同一文件的依赖准备收据又把任何失败都写成 `web-scaffold-preparation-failed`,而宿主返回的真实机器码就在手上,等于把唯一的诊断线索作废。
- **处理**:按真实原因分流成 `web-preflight-browser-missing` / `-launch-timeout` / `-launch-failed` / `web-preflight-cancelled` / `-browser-cleanup-failed` / `-browser-environment-failed` / `-evidence-failed` / `-input-invalid` / `-page-check-failed`,未识别原因落回专用兜底 `web-preflight-unclassified`;收据改为保留宿主返回的真实机器码,只有确实拿不到码时才用专用兜底;首页无码兜底文案不再写「请检查 Node/npm 或浏览器」这种未证实的候选原因。
- **验证**:`cargo test --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell environment_check::` 19 passed(含 `browser_validation_failures_keep_their_own_reason_codes`、`scaffold_receipt_keeps_the_real_failure_code`)。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/environment_check/web_creation.rs`、`apps/ai-game-creator-shell/src-tauri/src/browser/process.rs`、`apps/ai-game-creator-shell/src/features/app-shell/homeWebPreflight.ts`。
## 2026-10-02 AGC 直连回合失败分类器拿具体码当兜底
- **现象**:同一条 `code=runtime-failure` 同时承载「Claude Code 返回失败终态」与「Claude Agent SDK sidecar 回合超时」两种完全不同的原因;任何新失败形态都会静默并入该桶,从标签上看不出分类覆盖率。
- **根因**:`agent/runtime_error.rs` 的 `classify_direct_codex_error` 用具体码 `runtime-failure` 做 `else` 兜底,并且整条分类依赖中文子串嗅探,未识别原因既没有专用兜底槽,也不进入统计口径。
- **处理**:`else` 改为专用兜底码 `runtime-unclassified`;把已有真实证据的原因提升为具体码 `sidecar-turn-timeout` / `sidecar-launch-failed` / `app-server-launch-failed` / `claude-code-failed`。前端 `direct-codex-failure:v1|v2` 解析本就接受任意 `[a-z0-9-]+`,不需要兼容层。
- **验证**:`cargo test … agent::runtime_error::` 5 passed(含新增的 `known_sidecar_and_app_server_failures_keep_their_own_codes`、`unclassified_failures_get_a_dedicated_code`);`cargo test … direct_failure_diagnostic` 4 passed(断言已改为按实际原因分类)。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_error.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs`、`apps/ai-game-creator-shell/src/features/agent-runtime/model.ts`。
## 2026-10-02 AGC 子进程启动失败被报成「不支持子命令」/「Node runtime 不可用」
- **现象**:Codex CLI 可执行文件缺失、路径错误、权限不足或被安全软件拦截时,用户被提示「当前 Codex CLI 不支持 app-server 子命令,请升级 Codex CLI」,升级后仍是同一个错;Claude sidecar 的 Node 进程完全起不来时被笼统说成「Node runtime 不可用」,且丢掉 OS 错误码。
- **根因**:`config.rs::check_game_creator_codex_app_server_available` 与 `agent/claude_code_cli.rs::game_creator_claude_code_cli_version_identity` 都把 `Command::output()` 的 `map_err(|_| …)` 归因到“子命令不支持”或“运行时不可用”。`output()` 失败只可能是 spawn/exec 失败,与“不支持子命令”是两码事。
- **处理**:两处改为保留真实 OS 错误的「启动失败」文案;`output()` 成功但输出不符时仍保留原来的“不支持 app-server 子命令,请升级”判据,失败关闭语义不变。
- **验证**:`cargo test … tests::configuration::` 45 passed(覆盖 LLM 配置状态链路);同一 `--bin` 编译通过。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/config.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`。
## 2026-10-02 AGC 更新检查失败被显示成「当前已是最新版本」
- **现象**:「关于」页手动检查更新,在清单 404、渠道缺少当前平台条目、网络失败或签名校验失败时仍显示「当前已是最新版本」。
- **根因**:`services/appUpdate.ts` 把更新插件的所有异常统一 `return null`(按“无更新”收口),而这个 `null` 同时表示“没有更新”和“检查失败”;`RuntimeConfigDialog` 里对应的 `catch` 分支因此永远不可达,失败被兜成成功。
- **处理**:新增结果型入口 `checkForAppUpdateResult()`(`available` / `current` / `failed` / `disabled`),失败保留插件返回的原始原因;`checkForAppUpdate()` 退化为启动期投影(失败仍静默、不阻塞启动、不动更新提示);「关于」页按结果分别显示,失败显示具体原因,渠道未启用不再冒充“已是最新”。
- **验证**:`apps/ai-game-creator-shell/tests/appUpdate.test.ts` 9 passed(新增失败/已是最新/有更新/未启用四组用例,并保留启动期静默断言);`npx tsc --noEmit` 通过。
- **关联**:`apps/ai-game-creator-shell/src/services/appUpdate.ts`、`apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx`、`apps/ai-game-creator-shell/tests/appUpdate.test.ts`。
## 2026-10-02 AGC 测试夹具未跟随“执行器与协议正交”拆分,HEAD 上 Rust 测试无法编译
- **现象**:`cargo test --bin genarrative-ai-game-creator-shell` 在 HEAD 上直接编译失败,9 个错误全部集中在 `src/tests/configuration.rs`:`select_game_creator_model` 少了第 4 个协议参数、`GameCreatorAppConfig` 少了 `selected_model_protocol` 字段。
- **根因**:`6bf57c1e5` 把执行器与线协议拆成正交维度时只更新了生产代码,测试夹具没有同步,于是所有 Rust 定向验证(包括随包预检与运行时分类)都被挡住。
- **处理**:夹具补 `None` 协议参数与 `selected_model_protocol: None`,不改任何断言口径。
- **验证**:`cargo test --features=… --bin … tests::configuration::` 45 passed。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs`、`apps/ai-game-creator-shell/src-tauri/src/commands.rs`、`apps/ai-game-creator-shell/src-tauri/src/main.rs`。
## 2026-10-02 AGC 的 cc 路由被本机 `ANTHROPIC_*` 环境顶掉,官方模型全发到用户个人中转
- **现象**:用户终端里给 Claude Code 配了自己的中转(`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`)。AGC 选 `claude-opus-5-5` 发消息时好时坏:偶尔两分半回一句话,多数回合一路静默到 `requestTimeoutMs`(180s)超时,日志只有 `claude-sidecar-start` 加 `claude_sidecar_idle idleSeconds=30/60/…`。同一时间平台网关、模型目录、账号 Router 分组怎么改都没反应。
- **根因**:`claude_code_cli.rs` 先把 AGC 进程继承来的 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` 原样复制给 sidecar,再让它们优先于平台会话(`claude_base_url` 里 `std::env::var("ANTHROPIC_BASE_URL")` 排在 `.or_else` 最前,凭据有 `if … is_none()` 守卫因此永远不覆盖)。实测:直接跑 sidecar 探针,`claude.exe`(PID 43256)的两条 443 连接落在 `198.18.2.60` = 用户个人的 `yunyi.rdzhvip.com`,完全没碰 `dev.genarrative.world` / Router;同一个 token 对 `POST https://yunyi.rdzhvip.com/claude/v1/messages` 45 秒不返回(Bearer 与 `x-api-key` 都一样)。用户 `~/.claude/settings.json` 里的 env 反而无关——sidecar 用 `settingSources: []` 关掉了设置文件来源,事故来源是**进程环境**。
- **现行口径**:cc 的路由与凭据只由 AGC 决定,本机 `ANTHROPIC_*` **完全不参与**(`claude_code_route` 的入参里没有进程环境)。官方模型 + 平台会话 → `{apiBaseUrl}/api/llm/anthropic` + `Bearer <平台 access token>`;`customEnabled` 或配了 `llm.apiKey` → AGC 配置的 baseUrl(自定义端点用 `x-api-key`,其余保持 Bearer);没登录也没配 key 就报「cc 模式需要先登录陶泥儿账号」,不再退回本机环境。`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` 也不再进 `copy_env` 白名单,只由解析出的路由下发。个人中转要走 AGC 的 `customEnabled` + baseUrl + apiKey,不做隐式继承。
- **同时(配置隔离)**:sidecar 子进程把 `HOME`、`USERPROFILE`、`CLAUDE_CONFIG_DIR` 全部指向项目内隔离目录(`.agent/runtime/claude-code/home`)。只改 `HOME` 不够——Windows 上 Node 的 `os.homedir()` 只看 `USERPROFILE`,Claude Code 会照样读用户自己的 `~/.claude.json`(个人 `mcpServers`、插件市场、凭据)并把 `projects/`、`sessions/` 写回真实 profile。实测隔离前 CLI 先花 ~90 秒在个人配置上、真实 `~/.claude/sessions` 每次回合都被写;隔离后 `time_to_request_ms=41`,且只写隔离目录。
- **同时(超时语义)**:`requestTimeoutMs`(默认 180000)在 cc 执行器里只作**静默预算**:sidecar 每有一条事件就重置,连续静默超过预算才按 `sidecar-turn-timeout` 收口;另有 `CLAUDE_CODE_TURN_MAX_DURATION`(45 分钟)兜底事件流假活,正常情况更早到的是 DirectProject 的 `maxTurnSeconds`。原来「整回合 180 秒墙钟」会把「网关慢但仍在下发事件」的正常回合掐死——实测 19:46 那轮 sidecar 一直在出事件,第 180 秒被墙钟杀掉。
- **排查手段**:sidecar 每次启动都写一行 `agent.direct_codex.claude_route source=<Platform|Custom|Config> host=<…> auth=<key|session|none>`(只写主机名,不含路径与凭据)。这一行是判断"回合到底发到哪个网关"的唯一低成本证据。
- **注意**:日志脱敏标记包含 `credential`、`x-api-key`、`bearer `、`token=`、`api_key`,命中即整行替换成 `<sensitive diagnostic details redacted>`。诊断行只能写 `source=`/`host=`/`auth=` 这类自查过的字段(第一版写成 `credential=bearer`,整行被吃掉过一次)。
- **验证**:`cargo test --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell claude_code_cli::tests::` 11 passed,覆盖平台优先、自定义端点保留配置、未登录时不回落本机环境且报「先登录」、baseUrl `/v1` 归一化、主机名诊断、sidecar 环境隔离(`USERPROFILE`/`CLAUDE_CONFIG_DIR` 指向隔离目录且不带本机 `ANTHROPIC_*`);另用本地假 Anthropic 端点跑通真 sidecar:`REQ HEAD /api/hello` → `REQ POST /v1/messages?beta=true auth=bearer` → `result=PROBE_OK`。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`、`apps/ai-game-creator-shell/agent-sidecar/src/index.mjs`、[`【技术方案】AGC后台模型别名与对话选择-2026-09-05.md`](../../technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md)。
## 2026-10-03 cc 回合"模型已回复却报宿主任务提前结束":缺终态 + 缺落盘 + 工具被拒
- **现象**:Router 渠道恢复后,cc 回合能拿到真实回复(日志 `stage=claude-parse-done chars=176/208/289`),但紧接着就是 `runtime-unclassified` +「DirectProject 宿主任务提前结束(panic、future 被丢弃或被取消)」,用户看到的仍是失败;项目对话历史里只有用户消息、没有助手消息;模型还会回一句「读项目文件的工具没有权限」。
- **根因 1(缺终态)**:终态 `turn.completed` 的"深层出口"只在 codex app-server 那条路径里(`DirectTurnTerminalContext::write` → `complete_turn`)。cc 执行器(`direct_game_creator_claude_code_chat_at`)整轮成功返回后没有任何人写终态,于是 `thread_manager::dispatch` 里占用对象的兜底把一轮已经拿到回复的回合收成 `HostDropped`。诊断行 `agent.direct_turn.host_dropped … panicking=false` 且**没有** `agent.direct_turn.panic` 行,就是这条(不是 panic,是"没写终态就结束")。
- **根因 2(缺落盘)**:assistant 回复只存在于 SDK 事件流里。codex 路径由 `finish_direct_project_collect_history` 写进 `.agent/conversations/project.jsonl`,cc 没有对应步骤,所以即使回合收口成功,UI 也读不到回复。
- **根因 3(工具被拒)**:sidecar 用 `permissionMode: 'dontAsk'` 且没有 `allowedTools`,宿主 MCP 工具(`mcp__agc__*`)一律被直接拒绝,模型只能回"没有权限"。
- **根因 4(界面看不到回复)**:聊天区是按 `item.completed` 事件流投影的(codex 路径在 `rawResponseItem/completed` 时下发 `ThreadItem::Message`),只把回复落进 `project.jsonl` 不会让本轮出现在界面上——用户看到"用户气泡 + 本轮结束于 … · 耗时",回复只在重进项目时从历史读出来。
- **现行口径**:cc 成功出口由放行侧补写 `DirectTurnTerminal::completed()`(`finish_if_unfinished` 幂等,codex 已写过终态时是空操作);cc 解析成功后必须①把回复按 `{"type":"message","role":"assistant","id":"direct-codex:<clientTurnId>:assistant","content":[{"type":"output_text","text":…}]}` 落进项目历史,②用**同一个 id** 下发 `ThreadEvent::item_completed(ThreadItem::Message{role:"assistant"})`(落盘失败按回合失败收口);sidecar 按 `mcp__<server>` 前缀整体放行请求里声明的 MCP 服务器(权限策略在宿主侧执行)。
- **诊断口径**:`agent.direct_turn.host_dropped` / `agent.direct_turn.panic` 里的令牌字段必须写 `tt=`,写 `turnToken=` 会命中脱敏标记,整行变成 `<sensitive diagnostic details redacted>`,离线只剩"说不出原因"的 HostDropped。
- **验证**:dev 栈里用 CDP 注入真实回合(`node %TEMP%\agc-cdp.mjs <expr>`):①读文件轮 `claude-parse-done chars=108`,`.agent/conversations/project.jsonl` 出现 `direct-codex:cdp-…:assistant` 条目,回复内容与 `game/index.html` 前两行(`<!doctype html>` / `<html lang="zh-CN">`)逐字一致(证明宿主工具真的执行了);②聊天视图打开时注入 `只回三个字:收到了`,DOM 断言(`document.body.innerText`)同时出现用户气泡 `11:40:05`、助手回复 `收到了` 与 `本轮结束于 11:40:16 · 耗时 10.7秒`(证明 `item.completed` 实时投影生效,不必重进项目);同一日志不再出现新的 `host_dropped`。`cargo test … -- claude_code_cli::tests direct_turn_failure::tests` 19 passed。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`、`apps/ai-game-creator-shell/agent-sidecar/src/index.mjs`。
@@ -23,7 +23,7 @@ Genarrative / 陶泥儿当前主站聚焦图片画布创作、编辑器项目与
## 本地开发端口真相
`scripts/dev.mjs` 的默认值是 Web `3000`、API `8082`、BgFilter worker `8083`、SpacetimeDB `3101`、后台 Web `3102`。Linux 用户端口段会把这五个服务映射到 `start` 至 `start+4`,AGC Vite 使用 `start+5`;显式端口或端口段配置可覆盖默认值。
`scripts/dev.mjs` 的默认值是 Web `3000`、API `8082`、BgFilter worker `8083`、SpacetimeDB `3101`、后台 Web `3102`。Linux 用户端口段会把这五个服务映射到 `start` 至 `start+4`,AGC Vite 使用 `start+5`;显式端口或端口段配置可覆盖默认值。`npm run dev:all` 复用 AGC 的数据库和 data dir 启动一份共享后端,并在根 dev 栈就绪后启动 AGC Vite 与 Tauri;管理后台只由根 dev 栈启动。
端口的运行时权威始终是当前工作区的 `.app/dev-stack.json` 与启动日志(该文件可能在未启动时不存在),不得从文档默认值推断当前监听端口。前端代理、API URL、SpacetimeDB 地址和 AGC Vite 地址都必须读取同一份运行时状态。
@@ -18,6 +18,8 @@
- DirectProject 工具可并行调度,依赖由调用方等待,同资源事务与付费动作幂等不能放松。Web 创作先用客户端环境预检,分层验证共用持久的 `validation.maxRuns`,不改写 Provider 的 `llm.maxRetries`;成功证据按输入指纹复用,达标后交付。完整回合条目统一保存在 `project.jsonl`;旧平行审计和附属请求分段计时已退出生产入口,不能因残留实现或测试而恢复旧契约。界面生命周期耗时与独立模型使用记录继续有效,未知边界不补零。详见 AGC 主专题的“Direct 历史、审计与耗时的现行边界”。
- 用户可见失败必须与真实原因对齐:兜底只允许使用**专门的兜底码/兜底文案**(`*-unclassified` / `*-unknown` / `Other` / IPC 与未分类专用提示),任何一层都不得借用某个具体子系统的错误去兜住无关原因(把 Tauri IPC 断连说成「环境预检未通过」、把进程启动失败说成「不支持 app-server 子命令」、把更新检查失败说成「已是最新」都属此类)。下层已经算好的**用户文案**(宿主错误码、控制器整理过的提示)只能透传,不得被上层覆盖成通用文案;宿主原始文本(Provider 文本、IPC 报错、完整路径)不直接上屏,只作为证据写进 `application.log`、运行时错误事件或 analytics。分类器的 `else` 必须是专用未分类码,不能用已有具体码兼任。详见 pitfalls 的 2026-10-02 批次条目。
- DirectProject 源码修改走 `agc_apply_patch`、进度走 `agc_update_plan`:SDK 原生的 `apply_patch` / `update_plan` 注册会被按回合移除(全局串行单例),不要恢复它们或用伪造工具注解换取并发。补丁只在当前项目内、受当前回合 Write 许可和受控进程约束,失败可能已部分写入,未知结果不自动重放;计划完成不构成验收证据。
- 捆绑 Codex 版本只在 `build_support/codex_bundle.rs` 固定一次,不要在测试或脚本里另写字面量;升级 SDK 后必须重跑模型目录真实用例、宿主补丁往返、并发夹具与发行载荷 smoke。原生命令工具名随 SDK 版本变化(0.155 起为 `exec_command` / `write_stdin`),脚本与夹具应按真实目录取用,不要按旧名字硬编码。