Files
Genarrative/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md
T
suzmii 2407295363
Project CI / Repository checks (push) Successful in 2m58s
Project CI / Frontend tests (push) Successful in 3m23s
Project CI / Backend tests (push) Successful in 5m0s
Project CI / Native shell tests (push) Successful in 13m37s
修复资源管理滚轮翻页偶发失效 (#197)
## 变更背景

资源管理画布在连续滚轮翻页时,可能出现偶发失效或中间栏目被逐页渲染,导致后续滚轮输入被资源渲染阻塞;同时,缩放、手动切页、排序和拖拽过程中还可能执行已经过期的翻页任务。

本 PR 将翻页意图与资源渲染提交解耦,并补齐交互取消边界,让连续输入始终基于最新逻辑目标栏目计算。

## 主要修改

- 滚轮分页改为同步维护逻辑目标栏目,可见栏目通过 React transition 异步提交;渲染尚未完成时,新滚轮事件仍基于最新逻辑目标继续计算,不逐页加载中间栏目。
- 待执行翻页队列改为有界合并与延迟 flush,修复旧定时器清理不完整的问题。
- Ctrl/Meta 缩放、触控板缩放手势、手动点击切页、切换排序、开始画布空白拖拽或资源卡拖拽时,取消尚未执行的过期翻页任务。
- viewport 更新显式携带目标栏目,画布拖拽记录发起时的栏目,避免过渡渲染期间把旧栏目 viewport 写入新栏目。
- 每个排序模式和栏目继续保留独立 viewport,复位、首次 fit 与保留用户当前平移缩放的路径统一使用同一套数据流。
- 依赖浮层的 ResizeObserver、scroll 和 resize 监听只在挂载时建立一次;viewport / 几何 revision 变化时通过 ref 合并调度一帧测量,避免连续输入时重复重建观察器。
- 更新资源自由画板技术方案,并补充回归测试覆盖连续滚轮、缩放取消、大步跳转、切页边界和依赖浮层观察器生命周期。

## 验证

- `npm run test -- apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts apps/ai-game-creator-shell/tests/appSurface.test.ts --run`
  - 2 个测试文件通过
  - 393 / 393 tests passed
- `npm run agc:typecheck`
- `npm run lint:eslint -- apps/ai-game-creator-shell/src/view/project-development/index.tsx apps/ai-game-creator-shell/src/view/project-development/ResourceDependencyOverlay.tsx apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`
- `npm run check:encoding`
- `git diff --check`

以上均为合并 `origin/master@2f3c578db` 后在 `b5c98af42` 上执行。

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/197
Co-authored-by: suzmii <suzmii@qq.com>
Co-committed-by: suzmii <suzmii@qq.com>
2026-08-26 15:29:35 +08:00

27 KiB
Raw Blame History

Game Agent 资源自由画板与快速编辑

目标

资源管理在空项目中展示现有按类型式分区展览。任一栏目出现资源后,“按依赖”和“按类型”共用同一套栏目分页画布:固定五个栏目各自拥有一个铺满资源管理区域的独立画布,空栏目也保留可打开的空画布;栏目大纲悬浮在画布左侧中间,只显示图标和栏目名称,不占用画布布局宽度;顶部显示当前栏目标题,底部显示下一页标题。普通滚轮切换栏目,按住 Ctrl/Meta 的滚轮以指针为锚点缩放当前画布。两种模式唯一差异是“按依赖”在当前栏目内绘制引导线。资源详情是叠加在资源画板之上的非模态卡片;打开、切换和关闭详情都不得卸载背景工具栏、资源卡、依赖连线或重置 viewport、搜索和排序模式。

图片资源从“编辑资源”进入持续精修草稿后,点击任意图片直接在图片下方显示快速编辑卡。每次快速编辑创建新的候选图,不覆盖来源图层;生成任务立即在画布中创建占位并进入任务侧栏。用户显式选择候选图“设为最终图”后,保持原资产 ID 不变,以事务方式切换 manifest 指向的正式 PNG;精修草稿和其它候选图继续保留。

资源总览中的美术图片卡按真实纵横比计算展示矩形,并使用统一边界限制极端长图或宽图。非图片资源继续使用固定卡片尺寸。

权威数据与持久化

栏目分页投影与设计文档缺失边界

  • 栏目内容只由当前权威资源投影构造:manifest assets、已完成任务的 artifacts、用户已导入附件和持久化 Agent 文本回执。前端不得扫描项目目录自行发明资源。
  • 固定栏目页始终为 设计文档 -> 美术资源 -> 音乐音效 -> 游戏代码 -> 项目版本。若某栏目没有对应投影,Dock 仍显示该栏目并允许打开空画布;空画布只表达“当前没有已登记资源”,不得由 UI 补假数据。
  • Direct Codex 的游戏生成链路以 game/index.htmlgame/style.cssgame/game.js 作为代码资产登记;是否额外产出设计文档由当前任务和 Agent 决定,资源页只消费已登记结果。

持续精修草稿

  • 一个图片 sourceAssetId 唯一发现一个活动精修 draftId;重新进入时恢复原草稿,不依赖进程内状态或创建平行画布。
  • 草稿 JSON 位于 .agent/workbench/asset-canvas/drafts/<draftId>.json,只保存图层、viewport、选择、generation 投影和提交记录,不内嵌图片 Base64。
  • 入口图片图层使用 project-asset 引用稳定 sourceAssetId。AI 生成、导入和中间图片使用不可变 draft-media,二进制位于 .agent/workbench/asset-canvas/media/<draftId>/<mediaId>.png,引用同时冻结 SHA256、字节数和像素宽高。
  • 候选媒体只属于精修草稿,不写入 manifest、不出现在资源总览,也不成为正式依赖。删除图层只删除草稿引用;物理媒体在确认不被草稿、generation ledger、staging、transaction journal 或恢复记录引用后延迟清理。
  • generation 占位与稳定 generationId 绑定。确认后 UI 立即显示内存占位;宿主创建私有 ledger 并接受请求后,才把落点、目标尺寸、阶段和结果图层身份投影到公开草稿。接受前失败或异常不得留下公开 generation-running 记录;成功候选必须使用同一落点,页面重启后从草稿和私有 ledger 恢复。

最终图事务

  • “最终图”权威定义为 manifest 中 sourceAssetId 当前指向的正式文件,不使用前端布尔值作为业务真相。
  • 前端只提交 projectId + draftId + sourceAssetId + sourceLayerId + expectedProjectRevision + expectedDraftRevision + commitId + idempotencyKey。后端重新读取权威草稿,验证目标图层属于当前 refine 草稿并引用完整 PNG draft-media
  • 每次提交将候选图安装到新的 assets/canvas/<name>--<commitId>.png,保持原 assetId 不变,只更新该 manifest 条目的 mediaType/localPath,随后推进 project revision。已登记 refine 入口必须保留原 source.resourceId(例如 editor-resource-*);只有 create 或缺少登记来源的本地资产才补为 local-asset:<assetId>。事务回读校验按同一来源解析规则验证,不能无条件要求 local-asset:*
  • 文件、manifest、revision、ledger、draft 和事件继续使用现有 transaction journal、before/after snapshot、回读验证、幂等返回、回滚与 reconciliation 合同;任何 revision 冲突都不得静默覆盖其它窗口的更新。
  • 提交成功后草稿回到可编辑状态,并记录 sourceLayerId + mediaSha256 + assetId + commitId + committedProjectRevision 的最后提交投影。不得清空候选图或永久终结草稿;再次选择其它候选图可产生下一次最终图事务。
  • 删除已提交候选图层只影响草稿;正式项目资源继续指向已验证的 PNG。当前最终图显示必须由 manifest/revision 与最后提交摘要共同投影。

精修来源规范化

  • refine 主来源不得仅凭 manifest 中已有的 source.resourceId 判断其可直接用于 sourceReferenceId。历史 Game Agent 资源可能以 game-backgroundart-spritesheet 等私有 kind 登记,虽然 ID 形态是正式 Editor Resource,仍不满足 External v1 快速编辑的 canonical 类型合同。
  • 客户端必须从本地正式图片重新取得受控字节,按现役映射登记 canonical 主来源,例如 game-background -> sceneart-spritesheet -> icon-spritesheet/api/external/v1/editor/images/edits 仍只提交登记后返回的业务资源 ID,不扩大服务端白名单,也不在 edit DTO 中补发 assetKind
  • canonical 来源缓存属于本地私有恢复数据,不写入 manifest。缓存身份至少绑定平台服务身份、平台 owner、Editor Project ID、源图片 SHA256 与 canonical kind;任一身份或图片内容变化都必须重新登记。
  • 缓存命中时复用已登记资源,避免每次快速编辑重复上传。平台明确返回来源不存在或类型不支持且请求尚未入队时,可以失效缓存并重建一次;不确定受理、已受理和对账状态不得换幂等键重提。

最终图事务恢复对账问题与修复

现象:用户连续两次“设为最终图”后重新打开画布,恢复阶段返回 reconciliation-required,前端提示“安全恢复原任务需要对账 / 素材画布存在无法自动证明的未完成事务”。

根因:

  • 提交成功后 transaction 目录不会归档,recover_asset_canvas_transactions_at 会重放全部历史事务。
  • recover_asset_canvas_transaction_lockedledger=Committed 分支要求当前 manifest 中该 asset 仍指向本事务的 finalImageRelativePath;后续提交已把同一 asset 指向新的 assets/canvas/<name>--<commitId>.pngasset_present=false 后直接 mark_asset_canvas_reconciliation_locked,没有先执行 find_superseding_asset_canvas_commit(该检查目前只在 unresolved 分支生效)。
  • 前端 recover() 只要存在任一 reconciliation-required outcome 就把整次恢复判失败,因此一个已被后续提交取代的旧事务会阻塞整个画布打开。
  • 现有测试此前只覆盖“第一个事务被强制标记 unresolved 后由后续已提交事务取代”,缺少“两个事务都成功提交后恢复”的用例,因此未暴露。
  • 单步 supersede 修复只接受旧事务的 after 快照与当前事务的 before 快照直接衔接。连续提交三次及以上时,最早事务必须跨过一个或多个已提交中间事务才能到达当前 manifest/revision;把“不是直接前驱”当作无法证明仍会误报对账。

修复合同:

  1. ledger=Committed 分支在进入 reconciliation 前先执行同一套 supersede 证明;命中则把旧 journal/ledger 标记为 Superseded,返回 Superseded,不得阻塞画布打开。
  2. supersede 证明继续要求每个后续事务已完整提交、正式文件哈希匹配,且相邻事务的 before 快照精确衔接前一事务的 after 快照;允许沿一条逐笔验证的提交链到达当前 manifest/revision,不得跳过中间事务或仅凭时间戳、revision 数值、路径变化猜测。
  3. 重复恢复幂等:已 Superseded 的事务直接返回既有状态,不再改成对账。
  4. 回归测试覆盖“两次连续成功提交”和“三次连续提交且最早事务已进入对账态”后恢复:所有旧事务必须收敛为 Superseded,当前事务为 AlreadyCommitted,整轮及重复恢复均不得出现 ReconciliationRequired
  5. 恢复失败可观测性仍保留后续增强方向:失败结果应携带 outcomes 明细(commitId、status、stage、证据摘要),前端展示具体事务并支持重试。

验收:

  • 连续两次“设为最终图”后关闭再打开画布不再出现对账提示,且最终 manifest 指向最新提交。
  • 现有 fault-injected 恢复用例(RevisionInstalledLedgerCommitted 等)全部保持通过,未出现新的 ReconciliationRequired 回归。
  • 重复恢复幂等,已 Superseded 事务不再重放为对账。

本地图片批量导入

  • Tauri 正式链路使用原生多选文件对话框,不以隐藏 DOM input[type=file] 作为唯一入口。取消选择返回受控 cancelled,不修改草稿。
  • 一批导入在 Rust 中完成安全读取、PNG/JPEG/WebP 签名与尺寸校验、总量校验、媒体安装、图层追加和一次 draft revision 推进。全部身份、路径、层序和新 revision 必须在首个文件安装前完成验证;任一文件失败时不得留下公开部分成功,回滚删除失败必须返回明确对账错误。草稿写入结果无法回读确认时保留本批媒体并进入显式对账错误,禁止冒险删除可能已被新草稿引用的文件。
  • 导入图层保存真实原始宽高,显示矩形保持比例并受当前 viewport 可见边界限制;首张落在当前视口中心附近,多张确定性错位并避让现有图层。成功后返回权威 draft 与 importedLayerIds,前端 hydrate 后选中并定位。
  • UI 必须区分正在选择、正在导入、已导入数量和安全失败原因。不得把文件读取异常或 host failure 留成未处理 Promise,也不得依赖稍后的前端 autosave 才把已写媒体绑定到图层。

失败生成任务归档与任务侧栏

  • 用户界面的“删除失败任务”语义是归档,不物理销毁私有 generation ledger。只有平台明确失败的 failed 任务可归档;reconciliation-required、已受理、运行中和结果未知任务不得移出恢复队列。
  • 归档命令校验 project、draft、generation 与 expected draft revision,先把私有 ledger 写入可重放的 archiving/archivedAt,再从 draft.generations 移除公开投影并推进一次 revision;草稿删除成功并回读后才发布 archivedarchiving 以及历史上已写 archived 但仍残留公开记录的状态都必须在恢复阶段幂等收敛,且不依赖图片生成服务凭证。
  • 失败占位和右上角任务项复用同一个归档动作,成功后两处同时消失,其它任务、图层和候选不受影响。
  • 任务侧栏折叠只属于当前会话 UI 状态,不写入 draft 或 manifest。视觉和交互复用现役美术画布:右上角独立“任务列表”图标按钮、20rem 白色模糊卡、总数徽标、排队/生成中已完成 双 Tab、状态圆形图标、阶段进度和时间信息;折叠后只保留图标按钮,不显示摘要卡。用户显式新建 generation 时自动展开并切回活动 Tab,普通进度更新不得推翻用户已有折叠选择。Game Agent 的失败归档作为任务行扩展保留。

操作边界

  • 栏目顺序固定为 设计文档 -> 美术资源 -> 音乐音效 -> 游戏代码 -> 项目版本。完全空项目显示全部栏目的分区展览;任一栏目出现资源后,分页大纲以左侧垂直居中的悬浮 Dock 展示全部栏目。常态缩小、降低不透明度并移除容器与选中项背景,只露出栏目文字;悬停或键盘聚焦时平滑恢复完整尺寸,显示栏目图标、Dock 背景和选中态视觉强调。默认停留在该顺序中的第一个非空栏目,空栏目仍可打开空画布。
  • 普通滚轮向下切到下一栏目、向上切到上一栏目并循环;持续滚动时将离散切页意图加入有界队列,浏览器合并形成的单个大幅滚轮事件也要按输入强度拆分为多个切页意图。同一节流窗口内的待处理步数必须合并为一次目标栏目切换,不能逐页挂载并加载中间栏目的资源,以免资源渲染阻塞后续滚轮输入;逻辑目标栏目在事件处理中同步推进,可见栏目和 viewport 通过 React transition 异步提交,渲染尚未完成时到达的新滚轮仍基于最新逻辑目标继续计算,渲染过程不得把目标栏目回写成旧页面。同时限制切页频率和最长排队距离,避免触控板惯性长时间自动翻页。点击大纲、底部“下一页”标题或自动定位资源属于显式切页,必须先取消尚未执行的滚轮队列,不能在显式切页后继续跳转;Ctrl/Meta 缩放、触控板缩放手势、排序切换和开始拖动画布或资源卡也必须取消待处理切页,缩放后的下一次普通滚轮应立即建立新的翻页意图。切页前必须终止旧栏目的画布拖动和 pointer capture,避免旧 viewport 写入新栏目。
  • 每种排序模式下的每个栏目画布都保留独立 viewport;首次进入该“排序模式 + 栏目”组合时按当前内容适配视口,离开后再返回则恢复该组合上次的平移和缩放。空白处拖拽平移画布,资源卡拖拽移动卡片并更新依赖线,Ctrl/Meta 缩放只作用于当前组合,不能牵动其它排序模式或栏目。
  • 依赖画布复用 @genarrative/image-canvas-core 的 viewport 计算,并复用现有资源卡片、布局和依赖连线模型。
  • 非空状态不使用资源分区滚动条、分区缩放或分区高度操作作为主要导航;栏目通过大纲、底部下一页标题和滚轮切换。
  • 当前栏目画布背景是无限的:用户可以将 viewport 沿 x/y 任意方向平移,画布不以资源 extent 作为导航边界,也不显示可见画布边缘。资源卡片的持久化布局坐标允许落在 -1_000_000..=1_000_000,用于支撑元素位于世界原点左上方;超出该范围仍拒绝写入,避免持久化非法布局。这与 viewport 能否继续平移是两层独立语义。搜索、详情卡和临时隐藏不得改变 viewport。
  • 普通滚轮切换栏目,指针拖动空白平移画布,Ctrl/Meta 缩放、复位以及容器 resize 后都必须保持同一套 viewport 数据流。只有“排序模式 + 栏目”组合首次获得可测量容器尺寸或用户显式复位时才重新适配内容;返回已访问组合、图片尺寸测量、布局拖动或资源 extent 变化只归一化并保留该组合的当前 viewport,不得意外重置用户已经完成的平移和缩放。普通平移不夹取 x/y;缩放仍受共享画布的最小/最大比例限制。初次 fit 与显式复位只使用资源卡真实包围盒,不把导航最小尺寸、原点空区或额外布局 gap 算入,并以 16px 紧凑留白在共享缩放上限内尽量铺满视口。
  • 资源卡拖动使用 5px 阈值区分点击与移动;移动期间按当前 scale 乐观换算世界坐标、显示拖动态并同步依赖线,释放时提交一次 manuallyPlaced=true 布局 CAS,取消则回滚预览且不提交。拖动后的释放点击不打开详情。
  • 依赖模式只在当前栏目画布内显示两端都属于该栏目的合法精确引用;装饰 SVG 与视觉隐藏的关系说明消费同一组可见边,搜索隐藏任一端点时两者同步移除。依赖线的 viewport 测量按动画帧合并,平移和缩放只更新已挂载观察器消费的最新 viewport,不得在每次输入时重建 ResizeObserver、scroll 或 resize 监听。任务流仍只参与同类型布局聚类,不绘线也不进入关系说明。
  • 资源详情卡包含元数据、媒体预览和“编辑资源”操作,但不使用全屏 backdrop、不声明 aria-modal=true、不把 focusedResource 作为背景工具栏渲染条件。角色资源同时显示“生成动画”时,两个业务操作按钮必须使用一致样式,不能依赖 DOM 中的首按钮位置。桌面端允许继续操作背景画板;窄屏可以使用有边界的贴边卡,但背景组件必须保持挂载。
  • 完全空项目继续使用可纵向滚动的五分区展览,不挂载分页画布或依赖画布的 overflow: hidden 交互壳;低高度窗口在两种排序模式下都能滚动到“项目版本”。
  • 在“按依赖 / 按类型”之间切换或离开资源管理进入运行视图后返回时,恢复对应“排序模式 + 栏目”的现有 viewport,不自动触发“复位资源画布”,也不得用另一排序模式的 viewport 覆盖用户已经完成的平移和缩放。首次 fit 与用户显式复位仍使用同一套真实资源包围盒算法。
  • 资源详情卡定位相对 .game-workbench-stage,桌面端在中间主视窗居中并受主视窗宽高边界约束;禁止使用相对整个窗口的右上角 position: fixed 定位。
  • 图片选中后的浮动工具栏整体退役。普通点击图片直接打开唯一的图片下方快速编辑卡;拖动不误触发,点击另一张图片切换卡片,点击画布空白关闭卡片。
  • 快速编辑卡底部统一承载“删除”“设为最终图”“修改”。“删除”只删除当前绑定图层并进入画布 history;“设为最终图”只对完整候选媒体可用;“修改”创建新 generation,不覆盖来源图层。
  • 页面级返回、导入、撤销和重做不属于图片浮动工具条,本次不以复制另一套图片操作条替代已退役工具条。
  • 资源自由画板顶部不展示“生成视频”“生成音效”“生成背景音乐”和“新增 UI 设计”等手动新建入口;顶部只保留播放、未完成编辑恢复、排序与复位等画布级动作。无源资源生成由 Agent 语义工具进入既有生成合同,已有资源的编辑、图片“生成动画”和精修图片“修改”入口不受影响。
  • refine /api/external/v1/editor/images/edits 严格遵循 EditorImageEditRequest,不得携带 assetKindsourceImageSrcsourceResourceId。Game Agent 本地 kind 在登记编辑器来源前映射到现役 canonical kind;服务端安全业务错误必须结构化返回,不能把全部 400 折叠为同一个“平台明确拒绝”。
  • 生成提交后立即创建内存画布占位和任务侧栏记录,宿主接受后再持久化公开 generation 投影;排队、运行、远端完成、下载、候选就绪、失败和对账是任务级状态,不得用全屏 canvas.generating/canvas.failed 遮挡或终结整张画布。候选持久化必须先写入并回读候选媒体、图层和公开记录,再发布私有 candidate-ready 终态;生成返回时前端只合并候选与 generation 权威事实,并在保存队列屏障后把生成期间的最新本地画布状态写回,禁止全量 hydrate 覆盖并发编辑。重新进入时,关键提交事务由 host.recover() 在 hydrate 前收敛;草稿 hydrate 完成后立即进入 canvas.editing,耗时的 recoverImages() 转为后台任务投影恢复,只更新任务、候选和 notice,不能继续锁住画布或因后台恢复失败切换为全屏失败。真正阻断草稿/提交/恢复的错误使用挂到 document.body 的全窗口 portal,必须覆盖整个 Tauri WebView,而不是只覆盖左侧画布区域。

美术预览几何

  • 图片预览读取返回真实 pixelWidth/pixelHeight。PNG、JPEG、WebP 复用 Tauri 已完成的头部尺寸校验;GIF、AVIF、BMP 和安全 SVG 的媒体预览也只读取受限头部/根元素元数据,无法安全解析时回退固定卡片尺寸。
  • 第一版图片展示边界为最大宽 220px、最大高 180px、最小短边 96px,展示框纵横比夹取在 1:2..2:1;图片始终 object-fit: contain,不得拉伸或裁剪。
  • resourceCanvasLayoutModel 的碰撞、自动布局、section/world extent、适配视口和拖动几何必须消费每个资源自己的 width/height,不能只改 CSS。
  • ResourceDependencyOverlay 的端点和绕障矩形必须消费同一资源矩形表,保证横图、竖图和 fallback 卡片的连线均对齐。
  • code/document/audio/version 继续使用固定 180x128;尺寸只属于展示投影,不写入 manifest,也不成为第二份资源业务真相。

验收

  1. 完全空项目在按依赖与按类型下都显示相同的分区展览;任一栏目出现资源后,两种模式都切换为栏目分页画布,大纲以左侧垂直居中的悬浮 Dock 覆盖在全宽画布上,包含全部栏目的图标和文字、不显示数量,空栏目仍可打开空画布。
  2. 栏目顺序为 设计文档 -> 美术资源 -> 音乐音效 -> 游戏代码 -> 项目版本,游戏代码不再固定在首位;普通滚轮可循环切换栏目,点击大纲和底部下一页标题也可切页。
  3. 在当前栏目内拖拽空白可以无限平移画布,拖拽资源卡可以移动卡片并同步依赖线,Ctrl/Meta + 滚轮以指针位置为锚点缩放,复位按钮可以适配当前栏目内容;普通平移不会因资源 extent、图片测量或窗口 resize 被拉回,连续缩放仍停在共享画布的最小/最大比例范围内。
  4. 按依赖模式显示当前栏目内同类型两端资源的合法精确引用,并具有与当前可见连线一致的无障碍关系说明;按类型模式不显示引导线;搜索隐藏任一端点后连线和说明同时消失。
  5. 点击任意资源后“按依赖 / 按类型 / 复位”等画布级动作仍保持挂载和原状态;顶部不出现手动新建入口。详情为非模态独立卡片,背景画板 viewport、搜索、排序、卡片和连线不卸载、不重置。
  6. 图片资源点击“编辑资源”后恢复唯一活动 refine 草稿,入口图进入默认参考;点击任意图片直接在其下方显示快速编辑卡,图片上方不再出现浮动工具栏。
  7. 快速编辑卡底部显示适用的删除、设为最终图和修改动作。拖动图片不误触发卡片;删除只影响草稿图层;候选设为最终图后保持原 asset ID、更新正式 PNG,草稿及其它候选仍可继续编辑。
  8. 点击修改后立即创建避让现有内容的生成占位和任务侧栏记录;成功图在同一落点出现,失败只影响该占位和任务,重启后恢复原 generation/operation,不重新扣费提交。
  9. refine 请求通过严格 DTO 合同且不携带非法 assetKind;参数、来源类型、余额、鉴权和对账错误显示不同的安全错误码。
  10. 1:1、16:9、9:16、2:3、3:2 和极端横竖图片按真实比例显示并受边界限制;卡片不重叠,当前栏目 extent、复位视口和依赖连线端点正确。非图片资源尺寸不变。
  11. GIF、AVIF、BMP、SVG 等不满足栅格快速编辑能力的资源继续使用能力模型中的降级编辑路径,不伪装成可提交 PNG 候选。
  12. 历史 game-background 等私有类型来源不得直接作为 refine 主来源;首次快速编辑完成 canonical 登记,后续同源请求复用缓存,并实际到达 candidate-ready
  13. 点击导入可以原生多选图片;成功批次只推进一次草稿 revision、立即出现在当前视口并被选中,取消和失败不改变草稿且不遗留部分导入。
  14. 明确失败任务可从占位或任务侧栏归档,重启后不恢复;reconciliation-required 不显示删除能力。任务列表具有与现役美术画布一致的独立按钮、双 Tab、状态图标与时间/进度信息;折叠后只保留按钮且不丢任务数据,新建任务自动展开。
  15. 画布带运行中 generation 重新进入时,在远端恢复完成前已经可选图、平移和继续编辑;后台恢复继续推进原 operation,不重复 POST、不重新扣费。阻断性提交/恢复失败的遮罩覆盖整个 Tauri 窗口。
  16. 已登记 refine 资产的候选设为最终图后保持原 assetId、原 source.resourceId 与新正式 PNG 路径;revision-installed 中断后重新加载可前向收敛为 committed。

PR 176 事务与宿主合同补充

  • 生成失败时,公开 generation 记录必须在同一草稿 revision 链路中落为 failedreconciliation-required,不得留下持久化的 generation-running 幽灵任务。
  • 客户端处理生成失败时必须调用宿主的权威失败结算接口并携带 expectedDraftRevision;结算返回 revision 冲突或失败时保持失败/对账阻断态,不得把画布静默恢复为 clean editing。无私有账本的早期失败只允许由后端原子移除公开占位并返回 not-started,已进入候选或正式提交终态的结果返回 already-terminal,等待重新打开对账。
  • 权威失败结算与生成流程内部错误统一先将私有 ledger 写为 failure-settlement-pendingreconciliation-settlement-pending,再幂等更新公开草稿与 staging revision,最后发布 failedreconciliation-required,任何路径都不得先发布终态 ledger。任一持久化边界中断后,启动恢复必须在解析生成服务凭据前完成 pending 结算;公开草稿或 staging revision 已写入时不得重复推进。
  • 候选成功回包不得整体 hydrate 覆盖生成期间的本地未 autosave 状态;必须以当前 runtime 的图层、viewport、背景、selection 和 history 为基线,只合并权威候选图层、预览地址与 generation 记录,并用一次带 CAS 的草稿保存确认候选。
  • 候选生成必须先幂等落盘候选媒体、候选图层和公开记录,回读成功后才能推进私有 ledger 的 candidate-ready 终态;两次写入之间中断时,恢复必须可重放。
  • 失败归档使用 archive-pending 中间态:先记录归档意图,再删除草稿公开记录,最后发布 archived;任一边界中断都必须在重启后收敛且保持幂等。
  • ImageCanvasAssetPort.importLocalImagesImageCanvasGenerationPort.archiveFailedGeneration 是冻结合同中的必选方法;不支持的宿主返回结构化 unsupported-capability
  • 快速编辑卡按自身测量宽高选择上下方位置,并在 viewport 四边保留安全留白;窄屏、滚动、缩放和 resize 后都重新计算。

验证命令

npm run ai-game-creator-shell:typecheck
node node_modules/vitest/vitest.mjs run apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx apps/ai-game-creator-shell/tests/resourceCanvasLayoutModel.test.ts apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts apps/ai-game-creator-shell/tests/useProjectResourceCardPreviews.test.ts apps/ai-game-creator-shell/tests/resourceCanvasLayoutContract.test.ts
node node_modules/vitest/vitest.mjs run apps/ai-game-creator-shell/tests/appSurface.test.ts
cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project::asset_canvas -- --test-threads=1
npm run check:encoding
git diff --check