合并最新 master 到 M0 分支

冲突解决:

- provider_request_builders:M0A-2 的固定 owner 收束边界与 master 新增的
  动态目标验收图改在同一段 prompt 构建。保留 owner 分支(固定 owner 仍不
  广告 project.verify / command.run_limited),把 master 的动态目标分支提到
  owner 判断之外对两条路径生效(固定 owner 属 goal_contract_participant,
  本就该收到禁止调用 agent.goal_contract 的边界声明),git 提交协议改为仅
  非 owner 路径追加以维持 M0A-2 边界。工具裁剪三个块并列保留,双方新增
  测试全部保留。
- SupervisorChatOnlyView:保留 M0B-2 的谱系分支,采纳 master 的
  projectWorkspaceStatusForDisplay 包装,避免该 import 变成死引用。
- runtime_actions:两侧各加一个导出,取并集。
- main_loop_tests:保留 M0B-1 需要的项目预初始化与 runtime lane。
- decision-log / pitfalls:双方条目都保留,各自维持原序。

验证:cargo check --all-targets 通过;前端 typecheck 干净;
provider_request_builders / game_chat 委派 / goal-control 共 17 条 Rust
用例通过;agentRuntimeModel 与 appSurface 共 391 条前端用例通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 03:48:19 +00:00
187 changed files with 41477 additions and 4987 deletions
+137 -9
View File
@@ -48,6 +48,53 @@
- 并发恢复补充:自主根任务 journal 写入后建立或重建 completion contract 时,初始 manifest reset 与 continuation reconciliation reset 不能重新使用 fail-fast 项目锁。异步 child finalization 可以合法插入两次取锁之间,使已入 journal 的新根被误记为 `completion-contract-failed`。这两条 reset 必须使用现有有界等待项目锁,超时仍失败关闭;只验证 scheduler 合同的测试应预占 child Runtime lane,不能真实启动后台 worker 后再手工改 manifest。确定性回归要显式持锁,分别证明初始合同与 continuation 合同等待释放后成功落盘。
- 验证:夹具必须从 `init_local_game_project_at` 开始,先断言无 `package.json` 且占位入口 smoke 失败,再证明产物不齐阻断、齐全后内部验证通过、无 smoke trace、再次 mutation 失效;另覆盖四个 owner 路径矩阵、错误身份、恢复、跨 run 凭证、`art-director` 有/无 Key、动态美术借凭证拒绝、`code-prototype` project.verify-only 阻断和试玩 executor 身份。
- 关联:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。
## 正式保存不能等待 React state 才取得草稿 CAS 的新 revision
- 现象:用户刚完成编辑就点击“保存到项目”,自动草稿保存已经成功,但正式提交仍携带旧 `expectedDraftRevision`,于是单窗口也得到 draft revision conflict;快速连续保存时还可能使用不同 commitId 重复 staging。
- 原因:`setDraft(result.value)` 的 React state 提交晚于当前 Promise 链,正式保存若从闭包或下一次 render 读取 revision,会把 UI 调度时序误当成持久化顺序。相同问题也会出现在选择变化未标脏、父组件每次 render 新建 scope 对象而重复恢复、项目切换后旧 generation 回调继续写 notice。
- 处理:草稿保存队列在 CAS 成功后同步更新 `draftRef.current` 并直接返回权威 draft;正式提交继续使用该返回值的 revision。scope 按 project/draft/intent/source 原始字段稳定化,所有导入、保存、生成和事件回调捕获当前 epoch,选择变化属于草稿合同并必须标脏。首次正式保存冻结 commitId/idempotencyKey,未知结果只重放原请求。
- 验证:用 deferred Promise 证明草稿 CAS 完成后正式 commit 使用新 revision;相同 scope 值重渲染不重复 recover/load;锁定图层选择进入草稿更新;项目切换后迟到 generation/commit/event 均不改变新会话。
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx`、`apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx`。
## 共享画布不能用全局 DOM 查询或宿主整包 CSS 作为隐式依赖
- 现象:页面挂两个画布时,第二个画布点击小地图会移动第一个画布;反复挂载后 wheel 触发多次。Tauri 单独引入网站 `index.css` 时还会带入账号、项目页和历史业务样式,或因仓库外源码解析到第二份 React 而出现 Hook 错误。
- 原因:`document.querySelector`、body 级 portal、未清理的 listener/observer/animation frame 和不受作用域约束的 CSS 都把组件实例与网站宿主当成全局单例;Tauri Vite 默认根目录又不等于仓库根,React 解析路径可能分叉。
- 处理:共享小地图先从当前 viewport 查询,只有文档中唯一候选时才兼容旧单实例形态;wheel、ResizeObserver 和 animation frame 在 effect cleanup 中逐项释放。portal 支持实例 root,默认 body 只作兼容。共享 CSS 全部限定在 `.genarrative-image-canvas`,两宿主 alias 同一 `packages/` 源码并 dedupe React/ReactDOM,Tauri `fs.allow` 覆盖 repo root,禁止导入主站完整 `index.css`。
- 验证:共享 React 测试重复 mount/unmount 后 wheel add/remove 数量相等、ResizeObserver 精确 disconnect,并挂两个含各自小地图的 viewport,确认只更新目标实例;网站与 Tauri 壳分别 typecheck/build。
- 关联:`packages/image-canvas-react/src/useImageCanvasViewportControls.ts`、`packages/image-canvas-react/src/CanvasPortal.tsx`、根目录与 `apps/ai-game-creator-shell` 的 Vite 配置。
## 桌面工作台不要让 Supervisor 内容高度挤掉输入区和 Agent Dock
- 现象:`1280×800` 或更矮窗口中,Supervisor 的消息、Runtime 状态和错误正文共同按内容高度增长,聊天输入被推到栏外;底部 Dock 使用固定宽度卡片时还会在中等宽度造成页面级横向溢出。中央画布的生成卡和状态栏若同时固定高度并隐藏 overflow,进度、失败、重试、保存或取消动作会被裁掉。
- 原因:四区工作台没有把“主区内部滚动”和“页面级滚动”分开;Supervisor 的长状态没有独立上限,Dock 卡片不能收缩,宿主又用全局按钮/固定行高样式覆盖共享 chrome。
- 处理:桌面工作台使用 `100dvh` 两行网格,第一行 `minmax(0, 1fr)` 承载中央区与 Supervisor,第二行承载 Dock;消息和 Runtime 分别内部滚动,composer 保持最后一行并设置明确层级。Dock 卡片使用可收缩 flex 与文本省略。画布 dialog、进度/失败卡和状态栏设置 `min-height: 0`、受限最大高度与内部滚动;宿主不再覆盖所有按钮,只为主要动作和布局提供 token 化薄样式。
- 验证:AppSurface CSS 合同检查 `100dvh`、Supervisor composer、Runtime 内滚动、Dock 常驻/可收缩;素材画布测试检查生成 dialog、operation card、状态栏和窄屏保存动作不会被裁剪。真实入口在 `1280×800` 测量 document/body client 与 scroll 一致;登录门禁不可为视觉测试绕过。
- 关联:`apps/ai-game-creator-shell/src/styles.css`、`apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、`apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx`。
## 素材保存区不要把机器 subtype 当普通文本框(2026-08-06)
- 现象:保存区同时显示“画布素材”和裸 `asset` 文本框,普通用户无法判断两者用途;自由修改 kind 会形成无法稳定参与类型布局、Agent 合同和替换兼容性的 subtype。工具栏与保存设置挤在同一行时,主要保存按钮还会被压缩或裁切。
- 原因:把 Host Port 的 `name / assetKind / mediaType` DTO 直接映射成同层输入控件,没有区分用户命名、机器分类和编码格式,也没有为中央区域的真实容器宽度保留主操作列。
- 处理:名称保留编辑;create kind 使用 Runtime 权威四项目录和中文标签,refine 从源 manifest 继承并锁定,未知历史值只透传;格式继续使用有限枚举。工具动作和保存设置显式上下分行,保存列使用 `max-content + nowrap`,窄容器时按钮独占整行。普通工作区状态只显示项目名称,不把绝对路径作为默认辅助文案。
- 验证:Surface 测试断言四项用途、默认值、精修未知 kind 锁定、最终 commit 参数和保存按钮 CSS;AppSurface 断言普通界面找不到绝对路径,内部 Tauri 调用仍使用原完整路径。
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx`、`apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css`、`apps/ai-game-creator-shell/src/features/project-workspace/`。
## 正式素材提交不能把多文件写入或 Tauri 事件误当成一次原子动作
- 现象:图片已经落到 `assets/` 但 manifest 没有资产,或 manifest 已追加而 project revision/草稿仍是旧值;进程在 emit 前后退出后,用户重试又得到第二份图片、第二个 asset 或重复选中。
- 原因:文件系统只保证单文件原子替换,不能让最终图片、manifest、`.agent/runtime/project-revision.json`、commit ledger 和草稿跨文件物理原子;Tauri event 也没有跨崩溃 exactly-once。若先写副作用再临时生成幂等身份,或只凭目标文件存在推断成功,就无法区分未提交、已提交未回包和部分提交。
- 处理:第一次保存前冻结 `commitId + idempotencyKey + eventId + requestFingerprint`,在项目 write lock 内先写 prepared journal 和 before/after 摘要,再按最终图片、manifest/revision 逻辑原子更新、回读、ledger/草稿提交推进,释放锁后最后 emit。恢复只按 journal stage、精确字节摘要和 ledger 前向完成/安全回滚;矛盾状态进入 reconciliation-required。事件采用至少一次,监听方按 eventId 和 project revision 去重。
- 验证:分别在 prepared、图片安装、manifest 安装、revision 安装、ledger 提交、emit 和投递标记后强杀;确认只有唯一 `canvas-<commitId>`、revision 最多推进一次、源资产与血缘正确,响应丢失后返回 already-committed,矛盾 fixture 不自动重试。
- 关联:`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`。
## 素材保存成功不等于迟到结果仍有权抢占当前焦点
- 现象:用户等待生成/保存时切到另一个项目、run、另一份素材草稿,或主动选择其它资源、修改搜索条件;旧请求完成后界面却切回旧画布、清空筛选并自动选中新资源。
- 原因:异步回调只检查“请求成功”或捕获的旧 `isMounted/projectId`,没有绑定中央状态 session、draft/intent、selection epoch 和 query epoch;manifest 投影这一数据事实又被错误地与“当前应自动聚焦”的用户意图合并处理。
- 处理:保存开始捕获 `projectPath + projectId + centerKind + sessionId + draftId + intent + selectionEpoch + queryEpoch`,响应时从当前 ref/store 完整复核。manifest 可以按精确项目身份更新当前上下文或后台缓存,但自动切状态、选择、滚动和聚焦必须等当前 mode 布局 ready 且全部焦点守卫仍相等。新资源被搜索/筛选隐藏时保留条件与选择,提示“新资源已保存,当前筛选条件下不可见”,只提供显式清除/定位动作。
- 验证:使用 deferred commit/layout Promise,依次在请求后切项目、切 run/overview、新开 session、改选择和改筛选;断言 manifest 只更新对应项目,新资源仍进入投影/布局,但所有失效守卫都不切中央状态、不改选择、不清查询。条件未变化且资源可见时才自动定位。
- 关联:`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`。
## 派生 Debug 会让完整配置经应用状态递归进入日志
@@ -4237,15 +4284,13 @@
- 处理:先以 CAS 单独 commit `queued -> executing`,成功后才调 ToolHost;调用返回后再 commit observation。恢复见到 executing 或 ToolHost 返回 Unknown 时只能进入 reconciliation,不得自动重执行。重复 resume 不得继续增 revision 或重复 event。
- 验证:在“ToolHost 已调用、observation commit 失败”处注入故障,序列化快照并用新 engine 重载;断言重复 resume 后 ToolHost 计数仍为 1,且只有显式 reconcile observation 才恢复 running。
## Runtime pending 恢复不能让大型 async frame 共用默认 worker 栈(2026-08-03)
## Runtime 后台执行不能让大型 async frame 共用默认 worker 栈(2026-08-03)
- 现象:Supervisor collaboration durable isolated spawn 恢复测试在默认 Tokio worker 栈下稳定 `stack overflow`;单独运行同样失败,提高 `RUST_MIN_STACK` 后通过。
- 原因:不是业务递归。debug 构建中 pending action continuation、后台 task queue 和 Agent 主循环各自形成大型 async poll frame;恢复路径在同一次 poll 调用链直接进入下一层状态机,累计超过 worker 默认栈。
- 处理:整个 pending continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,使上层 poll 先退栈后再轮询下一层状态机。传入边界的 future 必须先装箱;若泛型 helper 直接持有大型 future,即使随后 `spawn`,调用方 async frame 仍会把它保留在默认 worker 栈上。边界必须保留结构化取消语义;当前使用 boxed future 与 `JoinSet`,父 continuation 被丢弃时同步 abort 子任务。不得只增大 CI 的 `RUST_MIN_STACK`,否则生产默认栈仍可能崩溃。
- 验证:失败用例必须在未设置 `RUST_MIN_STACK` 时通过;同时覆盖 policy batch 全组、拒绝 pending 后重规划并 drain 下一任务,以及 pending/cancellation 回归,证明恢复不重复生成 isolated spawn、队列继续推进且父任务取消不遗留后台子任务。
- 2026-08-10 补充:Provider、Codex CLI 与 Codex app-server 合并到同一个模式分发后,即使本轮实际选择普通 Provider,未装箱的组合 future 仍携带最大分支状态;委派子任务完成后回流父 Agent 的既有回归会在默认 Tokio worker 栈稳定溢出,单独运行同样失败,扩大 `RUST_MIN_STACK` 才通过。持久重试 helper 与非持久压缩路径都必须在构造完整物理 Provider request 后、进入下层泛型 control/lifecycle helper 前装箱;不要逐个扩大 queue worker 栈,也不要等到底层 helper 内部再装箱已经进入调用方 frame 的泛型 future。
- 2026-08-10 验证:未设置 `RUST_MIN_STACK` 时运行 `background_agent_runtime_can_delegate_task_to_other_agent`,并追加 `provider_retry_`、`provider_handoff_`、`response_stream_` 与 Native shell 完整门禁;测试只能以默认 worker 栈通过,不能把 CI 环境变量当修复。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs`。
- 现象:Supervisor collaboration durable isolated spawn 恢复测试或普通 `agent.delegate` 后台委派测试在默认 Tokio worker 栈下稳定 `stack overflow`;单独运行同样失败,提高 `RUST_MIN_STACK` 后通过。
- 原因:不是业务递归。debug 构建中 pending action continuation、后台 task queue、Agent 主循环,以及 Provider、Codex CLI、Codex app-server 组合模式分发的最大分支状态都会形成大型 async poll frame;恢复路径直接进入下一层状态机、普通后台任务把完整主循环放回默认 worker,或组合 future 进入泛型 helper,都会超过默认栈。
- 处理:整个 pending continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,使上层 poll 先退栈后再轮询下一层状态机。传入边界的 future 必须先装箱;若泛型 helper 直接持有大型 future,即使随后 `spawn`,调用方 async frame 仍会把它保留在默认 worker 栈上。普通后台任务、静态委派子任务和 manifest ready-task 的首次执行统一复用 16 MiB 专用 Runtime worker,并在 worker 已启动后交接 Agent 任务锁;worker 创建或交接失败要持久化当前 run 失败。Provider 物理请求必须在持久重试 helper 与非持久压缩路径构造完整请求后、进入下层泛型 control/lifecycle helper 前装箱,不能等到底层 helper 才装箱。pending 边界继续保留结构化取消语义,父 continuation 被丢弃时同步 abort 子任务。不得逐个扩大 queue worker 栈,也不得增大 CI 的 `RUST_MIN_STACK` 掩盖问题,否则生产路径仍可能崩溃。
- 验证:失败用例必须在未设置 `RUST_MIN_STACK` 时通过;同时覆盖普通后台委派、policy batch 全组、拒绝 pending 后重规划并 drain 下一任务,以及 pending/cancellation 回归,证明任务锁只交接一次、恢复不重复生成 isolated spawn、队列继续推进且父任务取消不遗留后台子任务。另需运行 `background_agent_runtime_can_delegate_task_to_other_agent`、`provider_retry_`、`provider_handoff_`、`response_stream_` 与 Native shell 完整门禁,全部以默认 worker 栈通过。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_queue.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs`。
## Provider 可扩展不能用一个全局 protocol 枚举代替实例隔离
@@ -4315,7 +4360,7 @@
- 原因:把“客户端没有收到结果”误判为“服务端没有受理”,又没有持久保留逻辑请求的幂等键和服务端返回的 `operationId`。托管 MCP 若绕过 External REST router 直接调用 worker 或 SpacetimeDB,也会形成第二套去重与状态语义。
- 处理:一次逻辑生成只分配一个稳定幂等键。桌面 Runtime 在 POST 前先把 endpoint、精确请求体字节、SHA-256 和幂等键原子写入私有生成账本并回读一致;收到 `202 + operationId` 后先把账本升级为 `accepted` 再轮询。`accepted` 只恢复 GET;`prepared` 或提交响应丢失时,只允许校验账本身份、配置指纹和请求 SHA 后,以账本保存的原 endpoint、原始正文与同一键恢复同一逻辑 POST,不得重建画布上下文、重组正文或换键。恢复 `202` 后继续 GET,恢复再次 transport 失败仍保留原账本;轮询超时只保留既有 operation 并恢复 GET。game-chat 的 4500 秒硬截止可以结束本轮、关闭预览和客户端,但 executing 的 `canvas.asset_generate` 必须保留 pending action、provider batch 与生成账本;旧 `200` 图集的 `spritesheetResource` 允许为空,此时只在顶层 `spritesheetImageSrc` 是有效下载引用时优先使用,否则回退可用 `objectKey`。`202` 缺 operationId、状态损坏与 `postprocess-failed-source-preserved` 仍进入对账边界;其它 non-blocking warning 继续消费成功结果并单独展示。旧 `200` 兼容不改变权威 External v1 的异步契约。MCP 生成工具必须把 `idempotencyKey` 映射到同一 REST header,并复用同一 External router、owner 和任务账本。这是 External v1 的专用幂等恢复,不是通用副作用自动重放。
- 补充:不能把“accepted 分支里没有生成 POST”误当成 GET-only 恢复。若读取账本前仍重做项目/素材目录准备、输出路径预检或请求正文构造,恢复仍可能创建远端资源或在查询 operation 前失败。恢复必须直接使用 durable snapshot;清理必须最后删除 pending 身份锚点,活动 orphan 不得自动删除。完整恢复 future 还要在默认 Tokio worker 栈下验证,不能靠测试环境调大 `RUST_MIN_STACK` 掩盖栈溢出。
- 加固:durable snapshot 必须绑定不含明文凭据的 base URL/API Key 配置指纹,配置漂移时恢复 POST 和 GET 都必须阻断。accepted operation 明确 failed 也不能在 observation 持久化前删账本。旧 `200` durable result 只保留允许字段与安全 objectKey/相对路径,签名 URL、query/fragment 和未知字段不落盘。只有首次提交直接返回契约明确的 `400 / 401 / 403` 才可证明未入队并清理 prepared 账本;首次结果已经未知后,恢复请求的临时鉴权错误、超时、冲突、限流、网关错误及其它意外状态均保留同一账本。账本根目录、扫描和删除必须通过受控路径解析逐级拒绝符号链接,不能让项目内链接把清理目标指向项目外。
- 加固:durable snapshot 必须绑定不含明文凭据的规范 base URL 服务身份指纹;服务地址漂移时恢复 POST 和 GET 都必须阻断,Developer API Key 轮换则必须继续原 operation。accepted operation 明确 failed 也不能在 observation 持久化前删账本。旧 `200` durable result 只保留允许字段与安全 objectKey/相对路径,签名 URL、query/fragment 和未知字段不落盘。只有首次提交直接返回契约明确的 `400 / 401 / 403` 才可证明未入队并清理 prepared 账本;首次结果已经未知后,恢复请求的临时鉴权错误、超时、冲突、限流、网关错误及其它意外状态均保留同一账本。账本根目录、扫描和删除必须通过受控路径解析逐级拒绝符号链接,不能让项目内链接把清理目标指向项目外。
- 代理 DNS:Clash 等透明代理可能把公网对象存储域名解析到 RFC 2544 的 `198.18.0.0/15` fake-IP。下载器只对已通过鉴权 `objectKey` 或受控 legacy path 换签得到的 URL 接受“全部地址均位于该 benchmark 段”的窄例外;直接 URL、其它本机/私网地址、公私混合解析和重定向仍必须失败关闭,不能为了兼容代理整体移除 SSRF 校验。
- 验证:覆盖“服务端已入队但提交响应丢失”后两次 POST 的 endpoint、正文 bytes 与 `Idempotency-Key` 完全相同,原键重试仍返回同一 operation,最终只出现一份 completed result 和一次计费 / 写回;恢复再次 transport 失败或临时鉴权失败仍保留同一账本;换 owner 不可见;MCP 与 REST 对同一 owner、同一请求和同一键必须命中同一 operation。
- 关联:`server-rs/crates/api-server/src/external_generation.rs`、`server-rs/crates/api-server/src/external_mcp.rs`、`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`。
@@ -4388,6 +4433,20 @@
- 处理:焦点状态机只比较稳定 `resourceId`:`null -> id` 与 `idA -> idB` 聚焦详情,`idA -> idA` 保持当前 active element。显式收起 / Escape 才恢复原卡片与滚动;后台删除清理 focused / matching selected ID 并聚焦搜索框;项目或运行视图切换清空 trigger / restore。媒体预览副作用依赖稳定 ID、路径和类别,不因同 ID 对象重建先卸载控件。
- 验证:媒体控件获得焦点后用同 ID 新 manifest 重渲染并断言 active element 不变;删除资源后断言详情关闭、选中清理且搜索框获得焦点;既有收起、Escape、项目切换和运行切换测试继续通过。
## manifest 与 revision 必须作为同一一致快照发布(2026-08-05)
- 现象:旧 manifest 的 React effect 在正式素材提交后才读取项目 revision,可能把“旧内容 + 新 revision”发给父级;若它先到,真正的 commit manifest 会被误判为同 revision 分叉并失败关闭。
- 原因:manifest 和 mutation revision 分开读取,却把其中任意时刻的两个值拼成一个权威快照;单独比较 callback 到达顺序无法修复这种身份错配。
- 处理:普通 Supervisor 投影固定执行“revision 前读 -> manifest -> revision 后读”,两次 revision 相同才发布,漂移时有界重试。素材 command/event 直接使用事务返回的完整 manifest 与对应 revision。父级按 `projectPath + projectId` 单调接受更高 revision,同 revision 只允许内容一致的重复,低 revision 和分叉都不覆盖。
- 验证:分别覆盖 command/event 两种先后、成功后旧轮询和同 revision 不同 manifest;不能只用 eventId 去重而跳过 revision 防倒灌。
## 新资源自动聚焦不能把投影、布局和 DOM 当成同一时刻(2026-08-05)
- 现象:保存回调已经带回 manifest,但新卡片可能尚无 dependency/type 坐标或尚未提交 DOM;立即选择会得到空画布、错误滚动,迟到回调还会抢走用户后来选择的资源。
- 原因:把 durable commit、资源投影、关系图 ready、两份布局协调和 React DOM commit 压成一个“保存成功”布尔值,缺少保存尝试身份和用户意图 generation。
- 处理:保存开始记录 `saveAttemptId + sessionId + draftId + commitId + focusGeneration`。自动定位依次等待资源投影存在、dependency/type 两份布局 settled 且都有位置、搜索条件可见和稳定 `data-resource-id` DOM 存在;按 commitId 只执行一次。切项目、切 mode、改选择/搜索、取消或开始新 flow 都推进 generation;迟到结果仍可合并权威 manifest,但不能改变选择。隐藏时保留搜索,只由显式“清除搜索并定位”建立新 generation。
- 验证:覆盖 manifest 已更新但布局未完成、DOM 后只聚焦一次、搜索隐藏、保存中切项目/改选择和连续保存;测试不得用 reload 或重开项目绕过阶段边界。
## 不要用自然语言精确 `.replace()` 维护 Runtime Prompt
- 现象:Prompt 文案稍作改写、增删空格或调整段落后,替换静默失效,代码中出现难以审阅的链式 `.replace()`。
@@ -4478,6 +4537,52 @@
- 处理:全部全局 sink 测试共用一把 test-only 串行锁,并由 RAII guard 在 `Drop` 中无条件清空;测试统一使用 `manifest_invalidation_sink_isolation_` 前缀。relay fixture 对 accept 和 payload 分别使用非阻塞轮询与总 deadline,不使用固定 sleep;生产 loopback、token、连接 / 写入超时和 payload 大小校验保持不变。
- 验证:用 `--test-threads=2` 重复运行统一 filter,覆盖正常 relay、无事件 accept 超时、不完整 payload 超时、panic 展开清理,以及 GUI owner attach 配置与 guard 清理。
## 远端图片 completed 不能冒充本地资源创建成功(2026-08-05)
- 现象:External operation 已返回 completed,但稳定引用缺失、下载失败、正式资产事务中断或 manifest 已提交而 UI 事件丢失时,界面仍可能提前显示“资源创建成功”,重复回调还可能再次下载、写文件或登记资源。
- 原因:把远端生成、媒体传输、本地 durability、manifest 投影、布局和选择压成一个 completed 布尔值;同时把 External idempotencyKey、operationId 或 taskId 暴露到公开草稿,导致恢复逻辑从非权威状态重建请求或误绑本地 task graph。
- 处理:使用私有 generation ledger 保存原请求、External 身份、稳定远端引用、固定 staging token 与本地 commit 身份;公开面只投影不可逆阶段。启动时先恢复阶段三事务,再恢复原 generation;重复 completed 先检查远端引用、staging 和 committed ledger,只有 `committed | already-committed` 才进入 manifest 投影。用户取消等待只推进 focus generation,不删除账本或伪装远端取消。
- 精修补充:`sourceImageSrc` 是可下载的稳定媒体引用,`sourceResourceId` 是资源身份,二者不能因为都可表现为字符串就填同一个 objectKey。本地 `local-asset:*` 只保留在本地 manifest 血缘;没有真实 External resourceId 时省略 `sourceResourceId`。
- 验证:覆盖确认前零调用、同 key 连点、accepted 重启 GET-only、重复 completed、取消后迟到、下载后本地事务恢复、事件丢失、切项目/改选择、旧轮询隔离、实时布局与选择、精修血缘及敏感字段零泄漏。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs`、`apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx`、`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`。
## 可恢复生成账本不能持久化 direct-upload ticket(2026-08-05)
- 现象:为支持参考图上传中断恢复,把完整 upload ticket 放进 generation ledger;账本随之包含 Provider host、formFields、policy、signature 或临时 Authorization,项目目录泄露即可复用临时凭证。
- 原因:把“恢复所需的稳定远端身份”和“仅供一次上传的临时授权材料”当成同一种持久状态。原子 sidecar 只能保证写入完整,不能让敏感字段变安全。
- 处理:ticket 结构不实现 Serialize/Deserialize,host/formFields 只在本次内存调用中使用。账本在上传前只保存稳定 bucket/objectKey;重启先用这组身份调用 object confirm,确认成功后只保留 objectKey/assetObjectId 并清掉上传中间态。账本测试必须直接序列化完整 ledger,扫描 Provider URL、Authorization、policy、signature、API Key 和 ticket 字段名。
- 验证:运行 `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml private_generation_ledger_never_serializes_upload_credentials_or_provider_url`,并继续检查公开草稿、manifest、事件和普通错误不含 prompt、operationId、Key、绝对路径或媒体正文。
## 客户端内部用途目录不能直接作为 legacyPrefix(2026-08-10)
- 现象:本地图片精修或视频、音频等全类型资源编辑点击生成后立即提示参考资源上传失败;私有账本的 `uploadBucket/uploadObjectKey/operationId/requestBodyJson` 全为空,服务端也没有 OSS、confirm、生成或扣费记录。
- 原因:direct-upload ticket 的 `legacyPrefix` 不是任意业务目录,而是 `platform-oss::LegacyAssetPrefix` 的权威白名单值。把 `asset-canvas-references` 或 `resource-editor-references` 直接放在该字段会被 api-server 在签名之前以 `400` 拒绝;客户端若把票据、OSS 和 confirm 全折叠成一个错误码,还会掩盖真正失败阶段。
- 处理:客户端编辑器统一使用合法私有 `legacyPrefix=generated-character-drafts`,把业务用途放入 `pathSegments`:图片画布为 `editor/asset-canvas-references/<projectId>/<draftId>/<generationId>`,全类型资源编辑为 `editor/resource-editor-references/<projectId>/<operationId>`。仍严格执行 ticket → OSS form POST → object confirm,只有 confirm 返回自洽稳定 `objectKey/assetObjectId` 后才允许提交生成;不要为内部目录扩白名单或新建上传接口。图片路径按本地校验、票据、对象上传、对象确认分别使用安全错误码,票据材料继续只驻留内存。
- 验证:客户端端到端测试必须断言 confirm 早于生成 POST、请求使用精确前缀与 pathSegments、账本只持久化稳定对象身份;票据失败时断言 `operationId/requestBodyJson` 为空且 manifest 只有源资产。api-server 测试应断言生成的 key 位于 `generated-character-drafts/editor/...` 且 access 为 private。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs`、`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`server-rs/crates/api-server/src/assets.rs`。
## Tauri 有平台登录 Token 不代表应调用主站画布 API(2026-08-10)
- 现象:客户端素材画布和全类型资源编辑从 WebView 读取平台 Access Token,把它传给 Tauri command,再调用 `/api/editor/*`、`/api/assets/*` 和 `/api/runtime/external-generation/jobs/*`;代码同时保留 External 分支,导致真实 UI、Runtime 和测试使用不同路径,发布客户端还错误依赖网页画布登录态。
- 原因:把“客户端壳有账号登录能力”误当成“客户端画布属于主站网页宿主”。主站和 Tauri 虽复用相同请求 DTO 与后端生成服务,但对外边界不同:主站使用站内认证路由,Tauri 远端媒体能力使用 Developer API Key 和 External v1 路由。
- 处理:Tauri 前端不读取、透传或持久化站内 Access Token;Rust 只从发布 AppData 私有 `editorApi.baseUrl/apiKey` 解析 External 凭据。图片、视频、音效、BGM 的项目/素材库、上传、确认、生成、轮询与换签全部留在 `/api/external/v1`,不访问内部 job 查询或账号/profile 接口。账本只绑定 External 配置身份指纹,升级前遗留的站内 endpoint 必须进入待对账状态,不能拿 External Key 自动重放。Key 缺失或无权限只返回安全配置错误,不打印 Key、Authorization、Provider 正文或私有路径。
- 验证:前端测试断言 command input 不含 `accessToken/apiKey`;Rust mock 服务器拒绝任何 `/api/editor/*`、`/api/assets/*` 和 `/api/runtime/external-generation/jobs/*` 请求,并覆盖 External `202`、原 operation 轮询、换签、非破坏性本地提交和账本零凭据。主站路由与 OpenAPI 未发生契约变化时不得为了客户端切换修改后端接口。
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/tauriImageCanvasHostAdapter.ts`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs`、`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`。
## prepared journal 之前同样存在正式事务崩溃窗口(2026-08-05)
- 现象:事务依次安装 before/after 快照后才写 journal;若进程在首个快照、全部快照或 journal 已写但 ledger 未写时退出,重启扫描看到 transaction 目录却无法进入原先只覆盖 prepared 之后的恢复状态机,可能留下孤儿目录或阻塞项目后续提交。
- 原因:把 `prepared` 当成事务的第一个可观察持久阶段,忽略了构造 prepared 证据本身也由多次原子文件安装组成。
- 处理:把首个快照、全部快照和 journal 后/ledger 前加入故障矩阵。无 ledger 时只允许清理受控快照与本模块临时文件;若 journal 已存在,还必须证明正式目标不存在、manifest 和 project revision 精确等于 before。未知文件、正式文件存在或权威状态漂移全部失败关闭,不能递归猜测清理。清理后同步 transaction 父目录,并允许同 commit/idempotency 身份安全重放。
- 验证:故障矩阵逐阶段恢复;额外用同一幂等身份在快照残留清理后提交两次,必须得到一次 committed、一次 already-committed,manifest 仍只有一个 canvas asset。
## 宿主事件接线不能顺手复制共享 history 栈(2026-08-05)
- 现象:Tauri Surface 已复用共享 viewport/transform/renderer 数学,却另外维护 undo/redo refs、快照克隆和恢复逻辑;网站共享 hook 后续增加内容安全或字段恢复时,两端会静默分叉。
- 原因:把 Pointer 事件接线、宿主生命周期胶水和可复用 history 算法放在同一组件中,误以为没有复制整个画布目录就已经满足共享源码边界。
- 处理:两宿主直接消费共享 `useCanvasHistory`;共享 snapshot 统一覆盖 viewport、selection、图层位置和 width/height,宿主只声明本地媒体是否允许安全移除/重做。Tauri 仍可保留 Pointer capture/epoch/host callback 接线,但选择、平移、缩放、变换、renderer 和 history 状态机不得在宿主重写。
- 验证:主站 history 定向测试覆盖 resize undo/redo;Tauri 新建、导入、编辑、撤销重做和 durable commit 用例必须在同一共享 hook 下通过。
## 编辑器生成不能把传输重试、参考图截断和客户端 provenance 当成独立小问题(2026-08-05)
- 现象:生成 POST 首次已经入队但响应丢失时,客户端自动重试产生第二个任务;第 6 张或更多参考图仍显示在 UI / 元数据里,却没有送给 provider;直接构造请求还能把任意资源 ID 写成最终素材引用。
@@ -4569,3 +4674,26 @@
- 原因:pool 只按 LLM 凭据和路由复用进程,节点身份只用于进程内 thread map。任一 stdout framing、子进程退出或连接故障都会 drain 整个进程的 pending/turn router,使所有共享节点同时失去可信终态。
- 处理:pool key 必须包含 `projectId + agentId + sessionId + runId`,每个权威节点直接持有独立 app-server 子进程;同节点 turn 还要串行,不能向同一 thread 并发 `turn/start`。只发送当前 CLI schema 定义的字段;stderr 使用有界内存尾部并先脱敏再进入 Runner 诊断。
- 验证:至少两个节点并发各跑多轮,确认存在两个 app-server PID;终止其中一个后只有对应节点进入 reconciliation,另一个仍能收到 `turn/completed`。旧 AppData 缺 `agentMode` 且含非 Responses 路由时必须保留 `provider`,不能在项目自动恢复时批量失败。
## 模拟 Provider 的隔离 AppData 测试必须显式固定执行模式(2026-08-11)
- 现象:测试已经写入本地 mock `baseUrl / apiKey / model`,却收不到任何 HTTP 请求,日志反而显示 Codex app-server 启动或退出;Native shell 全量中多个后台 Agent 用例一起超时。
- 原因:新安装和没有迁移上下文的隔离 AppData 默认使用 `codex_app_server`。只写 `agentLlm` 不能表达测试要走 HTTP Provider;直接切换 runtime config dir 的 fixture 也不会经过会自动补 `agentMode` 的测试 helper。
- 处理:任何要断言模拟 HTTP Provider 请求的配置都必须显式写 `agentMode=provider`。测试 helper 可以统一补齐,但直接写隔离 AppData 的 fixture 仍须在自身 JSON 中声明,不能依赖仓库 `.env`、用户 AppData 或历史迁移。
- 验证:先单跑失败用例确认请求命中 mock server,再执行完整 `npm run check:native-shells`;日志中不得出现该用例启动 Codex CLI/app-server,所有 Provider/MCP 请求数量和顺序按 fixture 闭合。
## Tauri 生成与资源编辑恢复不能依赖 UI 快照、旧 Key 指纹或队列首项(2026-08-11)
- 现象:应用重启后,任务视频、项目版本或 refine 草稿无法恢复;轮换 Developer API Key 后已有 operation 被误判为配置变化,已受理任务一次 401/403 还可能永久进入对账;文本 Provider 已成功但尚未 staging 时崩溃会重复调用。目录中放入大量无关文件还能绕过 pending 扫描上限。一条远端已明确失败的老 operation 会持续占据队列首项,挡住后续已受理或已下载任务;manifest 已写而 project revision 未写时,又可能被误标为 committed,或者 journal 已证明提交后因项目继续合法修改而无法补 ledger。durable committed 后遗留 staging 可能因一次删除失败而被误报为提交失败,也可能在正式媒体或 manifest 身份已经漂移时被直接删除;manifest 已有派生子版本而 journal 缺失,或旧 journal 没有 revision 身份时,也可能被猜成已经提交。派生视频再次编辑时若把 `assetObjectId` 当远端引用,生成会失败或指向错误身份。
- 原因:早期账本只保存显示层资源 ID,恢复时又依赖当前页面资源对象;refine `draftId` 只在组件 Map;配置指纹混入 Key 并把认证错误写成状态机终态;Provider 正文从内存直接进入解析/staging;扫描计数只在识别出 pending JSON 后递增;本地登记 ID 与 External generation 接受的稳定 `objectKey` 没有分层。恢复 UI 只选排序后第一项,而账本又没有远端终态失败/归档阶段;资产提交恢复把整个历史 after manifest 当作永久相等条件,没有区分目标事务事实与后续合法提交;旧 version journal 只保存 base/target 数值,不能证明完整 project revision before/after 身份。
- 处理:新账本冻结完整源快照,旧账本从权威 manifest、完成任务和版本记录有界恢复;refine 从正式 sidecar 按项目、意图、源素材和 active 状态唯一发现。服务身份用 `service-origin-v1` 哈希规范化 External base URL,确认 UI 只展示去除路径与凭据的服务 origin;旧 Key-bound 指纹由快照绑定的显式挑战迁移,确认前零网络动作,已受理任务换 Key 后只 GET 原 operation。Provider 调用前先持久化 request-issued,成功正文再写 durable handoff 后解析/staging;issued 无 handoff 只能对账。扫描在读取每个目录条目时先计数,任何文件都消耗预算。提交前复验源摘要,远端请求只使用账本已确认的稳定 `objectKey`,恢复始终复用原 operation 和请求字节。
- 队列与事务:独立恢复面板必须展示后端权威队列的所有 operation,读取失败不能伪装为空。`remote-failed` 不再重放,只能显式标为 `archived` 并保留账本;`reconciliation-required` 不能归档。派生 asset 使用 `prepared -> media-installed -> manifest-written -> revision-written -> committed` journal,只对可证明状态前向恢复;尚未证明目标写入时严格核对 before/after,已证明目标 asset/media 与 target revision 后允许 manifest/revision 被后续合法提交继续推进,并补齐同一 ledger。committed 后只有 staging 与正式媒体摘要一致、manifest 按 ID 或路径唯一精确匹配 journal asset 时才尽力清理;删除 I/O 失败保持 durable committed,身份或媒体漂移保留 staging 并进入对账。version journal 同样冻结 project revision before/after 身份;manifest 已有子版本但 journal 缺失,或旧 journal 面对已推进 revision 无法补证时都失败关闭。
- 验证:覆盖跨进程唯一 refine 草稿发现和多候选失败关闭、文本 Provider 成功到 staging 崩溃后零重复调用、任务视频/版本旧账本恢复、所有目录条目上限、Key 轮换与旧 Key 无法验证时的显式确认、Accepted 后 401/403 再换 Key 只 GET 原 operation、远端明确失败只归档且零新网络/扣费、三条乱序恢复队列、项目切换迟到结果、asset transaction 各崩溃阶段、revision 后项目继续合法修改仍补齐 ledger、committed 后 staging 清理成功/删除 I/O 失败/媒体或 manifest 漂移保留、源摘要漂移拒绝、committed 视频二次派生,以及 manifest 子版本缺 journal、旧 version journal 无法证明 revision 推进与 version journal exactly-once。
## 无限画布延迟草稿与零位移不能制造新状态(2026-08-11)
- 现象:r5 的 `loadDraft` 比 r6 更晚回包时会把草稿回退;pointerdown 后没有任何移动,pointerup 仍增加一条空 undo 并触发 CAS 保存;Tauri 自行维护 Shift toggle 后,Shift 单击唯一选中图层会意外清空选择。同一 `canvas.failed` 视图还可能把草稿保存或提交故障显示成生成重试。
- 原因:延迟回包只与发起时 revision 比较,没有在落地时复核当前最高 revision;指针按下就 capture history,而不是等首次真实几何变化;宿主复制了共享 selection 规则;失败状态没有携带发生故障的 operation 类别。
- 处理:generation progress、保存队列、生成/提交回包和延迟 `loadDraft` 统一用当前 scope、触发最低 revision 与回包当下草稿的单调门禁;同 revision 只允许完整相等回包。Tauri 指针和键盘选择复用 `resolveLayerPointerSelection`。pointerdown 只冻结快照,首次真实 move/resize/pan 才 capture 一次;零位移、未变选择和锁定图层不增加 undo、documentVersion 或草稿保存。
- 失败边界:`canvas.failed` 必须携带 `generation / draft-save / asset-commit / recovery / cancellation`,只有 `generation` 失败显示“返回修改/重新确认”。保存/CAS 只重试或重载,提交/恢复只安全恢复或对账,取消故障只保留草稿继续编辑;初始恢复失败也不得进入生成重试。
- 验证:用 deferred Promise 覆盖 r5/r6 逆序、保存与 progress 交错和 scope 切换;同时覆盖 Shift 单选自身、多选拖动、指针完整序列、零位移、首次有效移动只一条 history,以及五类失败的可访问名称与按钮集。