修复无限画布恢复与幂等链路
Project CI / Repository checks (pull_request) Successful in 1m0s
Project CI / Frontend tests (pull_request) Successful in 2m57s
Project CI / Backend tests (pull_request) Successful in 3m48s
Project CI / Native shell tests (pull_request) Failing after 11m11s

补齐 refine 草稿跨重启唯一恢复与生命周期门禁

完善 Provider durable handoff、认证恢复和旧密钥服务确认

修复资源事务前向恢复与异步导入迟到写入

补充前后端回归测试并同步权威文档与项目记忆
This commit is contained in:
2026-08-12 10:21:18 +08:00
parent 912df59c2b
commit daaef0917f
13 changed files with 2115 additions and 230 deletions
@@ -86,6 +86,7 @@
- 影响范围:下一阶段的共享画布包、网站 adapter、`apps/ai-game-creator-shell` 前端与 Tauri Rust 本地持久化;不修改 SpacetimeDB schema,不把草稿或资源布局 sidecar 变成 manifest 业务真相。
- 验证方式:按权威专题的 28 项矩阵覆盖 Web/Tauri 共用源码、新增/精修、生成响应丢失、重复提交、两窗口并发、事务各崩溃点、草稿恢复、切项目/切状态/改选择/改筛选迟到结果和不刷新即时投影。
- 关联文档:`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md``docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md``docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-08-07 game-chat 使用单主路径按需补齐美术
- 背景:原 game-chat 把 Supervisor 的意图判断之后又硬接为 `design-director / code-director / art-* / code-prototype / preview-*` 固定图。`code-director` 代替程序主 Agent 判断素材缺口,会让“把现有美术资源应用到游戏中”被错误翻译成先生成美术,且美术回执无法天然回到同一个代码 Run 完成接入。
@@ -1546,6 +1547,7 @@
- 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。
> 2026-07-18 覆盖说明:本段后续关于微信 `navigation.openNativePage`、生成结果订阅页、`[subscribe-message]` 日志和订阅页路由门禁的 2026-06 决策均已由旧创作模板退役决策废止,只作为历史记录。Expo / Tauri 的同源 H5 受控导航及微信登录、支付、分享能力继续有效。
- 2026-06-20 H5 原生导航预校验:`navigateHostNativePage()``native_app` 下发送 `navigation.openNativePage` 前必须先拒绝空值、控制字符、协议相对 URL、外域绝对 URL 和非 `http:` / `https:` 协议目标;同源绝对 URL、`/path` 和保留给桌面壳兼容的相对 route 继续交给 Expo / Tauri 壳二次归一并补写宿主上下文。微信小程序分支仍按小程序页面 URL 语义走 `wx.miniProgram.navigateTo`,不套原生 App 同源 H5 预校验。根级 `npm run check:native-shells` 会反查 H5 facade 仍使用 `normalizeNativeAppPageUrl(...)` 且发送归一后的 URL,避免明显不安全目标触达原生壳。
- 2026-06-20 微信受控原生页能力声明:微信小程序壳真实 capability profile 声明 `navigation.openNativePage`,用于承接已经登记并测试的小程序原生页 flow;当前订阅生成结果通知页通过 H5 `requestGenerationResultSubscribePermission()` 调用 `navigateHostNativePage()` 打开 `/pages/subscribe-message/index`,小程序页再调用真实 `wx.requestSubscribeMessage` 并按既有结果协议回灌。根级 `npm run check:native-shells` 必须把该能力反查到共享 profile、微信 `WECHAT_HOST_CAPABILITIES` 镜像、订阅页协议常量、H5 入口、小程序 host-bridge / shell / page 文件和相关测试;该能力不代表开放任意小程序页面跳转。
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities``openHostShare``writeHostClipboardText``requestHostHapticsImpact``setHostAppTitle``exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示受控分享动作,并按 `hostShell` 区分 Expo 系统分享面板和 Tauri 剪贴板复制表达,避免旧壳或裁剪壳露出不可用入口。
@@ -6964,8 +6966,12 @@
- 资源编辑 operation 以私有账本中的原 endpoint、请求字节、幂等键和 `operationId` 为唯一恢复身份;账本同时冻结源 asset、媒体类型、资源 kind、生产任务和源版本快照。旧账本只从当前 manifest、已完成任务产物、版本数组及受控扩展名兼容恢复,不能信任 UI 路径或前端重新构造的来源。
- Developer API Key 允许轮换,当前 `service-origin-v1` 配置身份绑定规范化 External base URL 的服务指纹,确认 UI 只展示服务 origin。升级前包含 Key 的生成指纹在当前 Key 可精确验证原服务时自动迁移;无法验证时保留原 operation 进入对账,不自动接管或重放。素材画布生成恢复另提供受挑战值保护的显式 origin 确认。operation 已建立后遇到 401/403/404、超时或临时网关错误都保留原 operation;恢复命令仅接收项目与 operation 身份,不接收 Key 或新请求正文。
- pending 扫描上限覆盖目录全部实际条目。媒体提交前重新校验源摘要;视频后续派生使用 committed ledger 的稳定 `objectKey`,不把 `assetObjectId` 当远端输入;版本 journal 重放返回原子版本,禁止重复追加。
- refine 草稿恢复以正式 sidecar 为权威,按项目、意图、源素材和 active 状态有界唯一发现;唯一命中保留 `draftId` 并换新 `sessionId`,多候选失败关闭。文本/SVG/Agent 回执在 Provider 调用前持久化 request-issued,成功正文先写 durable handoff 再解析和 stagingissued 后无可信 handoff 只对账,有 handoff 只消费原响应。
- 旧 Key-bound 资源编辑账本采用快照绑定的显式服务 origin 确认,确认前零网络动作;已受理 operation 遇到 401/403 保留原 remote phase,换 Key 后只 GET。asset journal 已证明目标 asset/media 与 target revision 后,后续合法 manifest/revision 前进不阻止补齐 ledger。
- 画布生成中冻结 wheel、指针、缩放和方向键状态改变并聚焦“停止等待”;普通 wheel 二维平移,Shift 纵向滚轮横移,Ctrl/Command wheel 缩放。Shift 指针序列只执行一次选择切换,草稿保存失败后退出仍必须提供保留/放弃确认。
- 只有 `editing` 生命周期允许新生成;其它状态与 modal 同时锁闭背景交互,生成 handler 必须再次读取当前 lifecycle 失败关闭。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs``apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx``packages/image-canvas-core/src/interaction.ts`
## 2026-08-06 编辑器生成结果使用 durable receipt 与统一原子提交
- 背景:图片、改图、去背景、图集 / UI 多产物、角色动作、视频、音效和背景音乐在 OSS 结果可用后,仍分段 confirm object、创建 project resource / account asset、保存 canvas 和 complete job。任一中间失败都会留下部分业务事实;只把 `external_generation_job` 当 operation journal 又无法覆盖无 job 的 inline,也无法独立证明某批 resource/asset/canvas 已作为一笔提交完成。
@@ -4548,6 +4548,7 @@
- 原因:把 Pointer 事件接线、宿主生命周期胶水和可复用 history 算法放在同一组件中,误以为没有复制整个画布目录就已经满足共享源码边界。
- 处理:两宿主直接消费共享 `useCanvasHistory`;共享 snapshot 统一覆盖 viewport、selection、图层位置和 width/height,宿主只声明本地媒体是否允许安全移除/重做。Tauri 仍可保留 Pointer capture/epoch/host callback 接线,但选择、平移、缩放、变换、renderer 和 history 状态机不得在宿主重写。
- 验证:主站 history 定向测试覆盖 resize undo/redoTauri 新建、导入、编辑、撤销重做和 durable commit 用例必须在同一共享 hook 下通过。
## 编辑器生成不能把传输重试、参考图截断和客户端 provenance 当成独立小问题(2026-08-05
- 现象:生成 POST 首次已经入队但响应丢失时,客户端自动重试产生第二个任务;第 6 张或更多参考图仍显示在 UI / 元数据里,却没有送给 provider;直接构造请求还能把任意资源 ID 写成最终素材引用。
@@ -4649,11 +4650,11 @@
## Tauri 生成与资源编辑恢复不能依赖 UI 快照、旧 Key 指纹或队列首项(2026-08-11
- 现象:应用重启后,任务视频项目版本的编辑 operation 无法恢复;轮换 Developer API Key 后已有 operation 被误判为配置变化目录中放入大量无关文件还能绕过 pending 扫描上限。一条远端已明确失败的老 operation 会持续占据队列首项,挡住后续已受理或已下载任务;manifest 已写而 project revision 未写时,又可能被误标为 committed。durable committed 后遗留 staging 可能因一次删除失败而被误报为提交失败,也可能在正式媒体或 manifest 身份已经漂移时被直接删除;manifest 已有派生子版本而 journal 缺失,或旧 journal 没有 revision 身份时,也可能被猜成已经提交。派生视频再次编辑时若把 `assetObjectId` 当远端引用,生成会失败或指向错误身份。
- 原因:早期账本只保存显示层资源 ID,恢复时又依赖当前页面资源对象;配置指纹混入 Key;扫描计数只在识别出 pending JSON 后递增;本地登记 ID 与 External generation 接受的稳定 `objectKey` 没有分层。恢复 UI 只选排序后第一项,而账本又没有远端终态失败/归档阶段;资产提交没有一份同时证明文件、manifest 和 revision 的 durable journal,提交后的清理也没有区分可证明的 staging 与需要保留的对账证据;旧 version journal 只保存 base/target 数值,不能证明完整 project revision before/after 身份。
- 处理:新账本冻结完整源快照,旧账本从权威 manifest、完成任务和版本记录有界恢复;服务身份用 `service-origin-v1` 哈希规范化 External base URL,确认 UI 只展示去除路径与凭据的服务 origin。对素材画布生成账本,当前 Key 不能验证旧 Key-bound 指纹,确认前保持零网络动作,由用户显式确认当前服务后才继续原 GET 或精确重放冻结 POST;对全类型资源编辑,同类旧指纹保留原 operation 进入对账,不自动接管或重放。扫描在读取每个目录条目时先计数,任何文件都消耗预算。提交前复验源摘要,远端请求只使用账本已确认的稳定 `objectKey`,恢复始终复用原 operation 和请求字节。
- 队列与事务:独立恢复面板必须展示后端权威队列的所有 operation,读取失败不能伪装为空。`remote-failed` 不再重放,只能显式标为 `archived` 并保留账本;`reconciliation-required` 不能归档。派生 asset 使用 `prepared -> media-installed -> manifest-written -> revision-written -> committed` journal,只对可证明状态前向恢复;文件/摘要、manifest before/after 或 revision before/after 不匹配时进入人工对账。committed 后只有 staging 与正式媒体摘要一致、manifest 按 ID 或路径唯一精确匹配 journal asset 时才尽力清理;删除 I/O 失败保持 durable committed,身份或媒体漂移保留 staging 并进入对账。version journal 同样冻结 project revision before/after 身份;manifest 已有子版本但 journal 缺失,或旧 journal 面对已推进 revision 无法补证时都失败关闭。
- 验证:覆盖任务视频/版本旧账本恢复、所有目录条目上限、Key 轮换与旧 Key 无法验证时的显式确认、401/403/404 保留 operation、远端明确失败只归档且零新网络/扣费、三条乱序恢复队列、项目切换迟到结果、asset transaction 各崩溃阶段、committed 后 staging 清理成功/删除 I/O 失败/媒体或 manifest 漂移保留、源摘要漂移拒绝、committed 视频二次派生,以及 manifest 子版本缺 journal、旧 version journal 无法证明 revision 推进与 version journal exactly-once。
- 现象:应用重启后,任务视频项目版本或 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 后解析/stagingissued 无 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)