完善 Game Agent 资源画布与图片精修生成事务 #181
Reference in New Issue
Block a user
Delete Branch "codex/game-agent-resource-section-pages"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
背景
本合并请求整合 Game Agent 资源管理的栏目分页自由画布,以及图片持续精修、候选生成和本地恢复事务。
当前远端比较基线:
master@b00a3f80576949567bf3320f0c6c92b4ed0a649dcodex/game-agent-resource-section-pages@5795d2f9b8797c342dcf2920ccefec2a3ff3f472+3746 / -1333)主要改动
资源管理分页自由画布
按依赖与按类型共用栏目分页画布;每个“排序模式 + 栏目”组合独立保存 viewport。图片持续精修与生成事务
candidate-ready。合同与文档
acknowledgeCandidateLayers、importLocalImages、archiveFailedGeneration为必选 Host Port;不支持的宿主返回结构化unsupported-capability。验证
CI run #1290 针对当前 head 已完成且四项均成功:
另有本地定向检查:
npm run agc:typecheckcargo fmt --checknpm run check:encodinggit diff --check评审 provenance
5456a1d…head,提出 PRD 退役入口、共享 pitfalls 的 extent 旧合同和 PR 正文事实更新三项 P2;对应文档已在5795d2f…head 修复并推送。影响范围
/api/external/v1路由。当前 head
c97130f0934仍有 12 个 P1 和 1 个 P2,需要请求更改:其中 5 个是本次栏目分页/拖拽增量问题,5 个是 PR 181 随仍开放的 PR 176 一并带入的生成持久化与 Host Port 问题,另有 2 个权威文档合同冲突。实际 diff 为 18 commits、45 files、+12,329/-2,537,包含 Rust、共享合同与持久化坐标语义,不符合正文“仅 UI、不修改持久化”的范围声明;请修复行内问题,并同步重写标题、正文、权威文档和验证范围。当前精确 head 的本地 typecheck、AppSurface 390/390、布局与资源实时集成 49/49、编码检查和 diff-check 已通过,但现有测试没有覆盖这些触发路径。@@ -2482,3 +3362,3 @@root,&mut ledger,GenerationLedgerPhase::AssetDurableCommitted,GenerationLedgerPhase::CandidateReady,[P1][继承自 PR #176 的早期提交,非 PR #181 本次增量]
CandidateReady不能早于候选事实落盘。这里先把私有 ledger 写成恢复逻辑会直接视为终态的CandidateReady,下一步persist_generation_candidate_at仍可能因草稿写入、revision 或 I/O 失败退出;重启后恢复入口会跳过重放并直接读取尚不存在的候选,形成无法自动收敛的终态账本。请先以幂等方式持久化候选草稿/图层并回读成功,再推进终态,或增加可重放的中间 phase;补在两次写之间故障注入并重启恢复的测试。@@ -2575,0 +3496,4 @@}ledger.phase = GenerationLedgerPhase::Archived;ledger.archived_at = Some(asset_canvas_now());write_generation_ledger(root, &mut ledger)?;[P1][继承自 PR #176 的早期提交,非 PR #181 本次增量] 归档终态写在草稿删除之前会留下永久半归档。此处 ledger 已写成
Archived后,草稿移除 generation 的写入仍可能失败;恢复扫描会跳过 Archived,而再次归档又受调用方持有的旧 revision/公开记录状态影响,导致任务在草稿中残留且无法可靠收敛。请使用可恢复的中间 phase/事务 journal,确保草稿删除成功并回读后才发布 Archived,且重放保持幂等;补 ledger 写成功、draft 写失败后的重启恢复测试。@@ -1751,0 +2360,4 @@createdAt: Date.now(),updatedAt: Date.now(),};const initialGenerationRecord: ImageCanvasGenerationRecord = {[P1][继承自 PR #176 的早期提交,非 PR #181 本次增量] 不要在宿主接受生成前持久化无法收敛的 running 记录。这里把
generation-running先塞进 draft 并由后续persistDraft()落盘;如果generateImage在创建私有 ledger 前失败或抛错,下面只更新内存 task/lifecycle,没有移除或持久化失败后的 generation。刷新后公开记录会永久显示运行中,恢复又找不到对应私有账本。请让占位与宿主创建生成形成可恢复事务:失败时以当前 revision CAS 删除/终结该记录并持久化,或由后端原子创建权威记录后再 hydrate;补“草稿保存成功、生成调用失败/抛错、刷新恢复后无幽灵 running 任务”的回归。@@ -1769,0 +2489,4 @@result.value.images.forEach((image) =>URL.revokeObjectURL(image.previewUrl),);await hydrateDraft(result.value.draft, epoch);[P1][继承自 PR #176 的早期提交,非 PR #181 本次增量] 不要用生成返回的整份 draft 覆盖生成期间仍可编辑的本地画布。当前生成开始后没有进入
canvas.generating或锁住画布交互,拖动、视口和选择修改仍可能进入 180ms autosave/save queue;这里的hydrateDraft会直接替换 layers、viewport、selection,清空 history,并把 document version 归零,尚未落盘或晚于生成快照的本地修改会被静默丢失。请在 hydrate 前等待并校验保存屏障,或只把候选/generation 权威事实合并进当前 dirty 画布并重放本地修改;补生成进行中编辑且 autosave 与生成完成交错的回归。@@ -1923,0 +2706,4 @@quickEditOpen && quickEditSourceLayerId? (layers.find((layer) => layer.id === quickEditSourceLayerId) ?? null): null;const quickEditPanelStyle: CSSProperties | null = quickEditSourceLayer[P2][继承自 PR #176 的早期提交,非 PR #181 本次增量] 快速编辑卡位置需要按 viewport 四边钳制或自动翻转。当前直接用图层底部中心作为
left/top,卡片自身宽高没有参与计算;图片靠近右侧/底部(移动端也包括左侧)时,卡片会被 viewport 的 overflow 裁掉,输入框和操作按钮不可达。请在测得/约束卡片尺寸后选择上/下方并 clamp 到可视区域,滚动、缩放/resize 时同步更新;补四边及窄 viewport 的交互回归。@@ -676,2 +748,4 @@'--resource-card-height': `${cardSize.height}px`,} as CSSProperties}onPointerDown={(event) => onPointerDown(event, resource, x, y)}[P1] 卡片内的媒体播放按钮只在 onClick 阶段 stopPropagation,pointerdown 已经先冒泡到这里并启动卡片拖动。用户点播放/暂停时只要轻微移动超过阈值,pointerup 就会提交一次资源坐标 CAS。请在开始拖动前过滤交互控件,或在媒体按钮的 pointerdown 阶段阻止冒泡;补播放/暂停不触发布局写入的回归。
@@ -1439,2 +1774,4 @@};const wheel = (event: WheelEvent) => {const target = event.target instanceof Element ? event.target : null;if (target?.closest('button, input, textarea, select, a')) {[P1] 这里把所有按钮和表单控件上的 wheel 提前放行,但资源卡的详情按钮覆盖整张卡,栏目 Dock、复位和下一页入口也都是 button。鼠标位于这些区域时,普通滚轮不能切换栏目,Ctrl/Meta+滚轮也不会缩放画布,而且默认滚动/缩放没有被阻止。请只排除确实需要原生滚动的控件,或先识别画布手势并统一 preventDefault;补资源卡和 Dock/复位按钮上的 wheel 回归。
@@ -2084,0 +2627,4 @@}if (!drag.captured) {drag.captured = true;event.currentTarget.setPointerCapture?.(event.pointerId);[P1] pointer capture 直到越过拖动阈值后才建立;鼠标在阈值内移出卡片并在外部释放时,卡片收不到 pointerup/pointercancel,resourceCardDragRef 会残留。栏目点击和排队滚轮切页也只清理 canvas pan,没有统一释放卡片 capture、drag ref 与 preview,切页卸载卡片时同样会留下半截拖动。请在 pointerdown 建立可靠所有权,或增加统一 cancel 并在切页/卸载调用;补移出释放和拖动中切页回归。
@@ -3434,0 +3893,4 @@/>) : mode === 'resources' ? (<>{focusedResource ? ([P1] 这里已经把详情改成浮层并继续渲染下方资源画布,但上面的 wheel effect 仍在 focusedResourceId 非空时直接退出。因此详情打开后,背景虽然还能点击和拖拽,却不能滚轮切栏目或 Ctrl/Meta+滚轮缩放,不符合“非模态、背景画布仍可操作”的契约。请让画布 listener 在详情态继续存在,仅隔离详情面板自身的滚动,并补详情打开时的背景交互回归。
@@ -966,1 +1265,3 @@position.x + RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP,...positions.map((position) =>Math.ceil(position.x +[P1] 本次已允许手动位置为负数,但 extent 仍只计算 x + width / y + height,调用方又把导航 bounds 的 x、y 固定为 0。卡片拖到左侧或上方后不会进入 fit bounds,复位或重新进入栏目可能无法找回该资源。请按所有卡片计算 minX/minY/maxX/maxY 并把非零原点传给 fit,补负坐标资源的复位与栏目重进回归。
@@ -345,3 +345,3 @@### 5.3 资源类型与替换兼容性(P1)实现状态(2026-08-10):当前资源投影已收口到固定的“文档 -> 项目版本 -> 美术资源 -> 音乐音效资源”四区。文档接收受支持的 UTF-8 文档/代码和合法 Agent 文本回执;项目版本只接收显式 `ProjectVersionResourceSummary` read model,未知任务产物不得兜底为版本;美术接收图片、SVG、动画和视频类产物;音频接收 manifest 资产、上传登记资产和已完成任务 `artifacts` 明确声明的音频产物。无法识别的二进制任务产物和附件不进入资源画布。受控读取、中央聚焦、失败空态与媒体播放不改变 manifest 真相;编辑成功后只追加新的 asset 或版本子记录。实现状态(2026-08-22):当前资源投影与栏目页顺序收口为“文档 -> 美术资源 -> 音乐音效资源 -> 游戏代码 -> 项目版本”。文档接收受支持的 UTF-8 文档、代码资产中的文档类型和合法 Agent 文本回执;项目版本只接收显式 `ProjectVersionResourceSummary` read model,未知任务产物不得兜底为版本;美术接收图片、SVG、动画和视频类产物;音频接收 manifest 资产、上传登记资产和已完成任务 `artifacts` 明确声明的音频产物;游戏代码接收 Direct Codex / 任务产物登记的 HTML、CSS 和 JavaScript。无法识别的二进制任务产物和附件不进入资源画布。受控读取、中央聚焦、失败空态与媒体播放不改变 manifest 真相;编辑成功后只追加新的 asset 或版本子记录。[P1] 这里新增了“游戏代码”栏目,但权威描述仍写成“文档 / 音乐音效资源”,与同一变更中的正式标签“设计文档 / 音乐音效”不一致;紧接着的
ProjectResourceDescriptor.category仍只有 document/version/art/audio,也缺少实际合同中的code。请先统一栏目名和类型合同,再让代码、测试与文档共同消费同一组枚举,避免 PRD 继续成为过期接口。@@ -0,0 +13,4 @@### 栏目分页投影与设计文档缺失边界- 非空栏目页只由当前权威资源投影构造:manifest assets、已完成任务的 artifacts、用户已导入附件和持久化 Agent 文本回执。前端不得扫描项目目录自行发明资源。- 设计文档栏因此只有在上述来源存在 Markdown / TXT / JSON / YAML 等文档类条目,或存在 Agent 文本回执时才出现。若 manifest 没有文档资产、任务 artifacts 未完成且没有附件 / 回执,资源页不显示设计文档页;这是生成或登记缺失,不由 UI 补假数据。[P1] 本文档在这里规定“无文档时不显示设计文档页”,但第 84 行和验收第 111 行又规定分页 Dock 展示全部栏目、空栏目可打开空画布;实现采用后者。请确定唯一产品合同并同步改写全文、PRD 与测试,不能让同一份权威方案同时要求隐藏和展示空栏目。
@@ -193,6 +200,11 @@ export interface ImageCanvasProjectPort {}export interface ImageCanvasAssetPort {importLocalImages?(input: {[P1][继承自 PR #176 的早期提交,非 PR #181 本次增量] 保持冻结 Host Port 的“方法始终存在”合同。把
importLocalImages(以及下方archiveFailedGeneration)声明成可选后,共享 UI 开始靠方法是否缺失分支能力,和合同规定的“能力不可用时返回结构化unsupported-capability”冲突,也会让不同 adapter 静默漂移。请把两个方法都改为必选,由不支持的宿主返回unsupportedImageCanvasCapability(...),并增加对所有 Host adapter 的方法存在性与 unsupported 结果测试。更正 review
247的归属说明:原正文中的“5 个由 PR 176 带入”只统计了 P1,且行内没有逐条标识;准确归属如下。继承自 PR 176 的早期提交、并非 PR 181 本次栏目分页 / Dock 增量(共 6 条):
AssetCanvasSurface.tsx:2363:失败后遗留 running 公开记录(P1)AssetCanvasSurface.tsx:2492:全量 hydrate 覆盖生成期间并发编辑(P1)generation.rs:3364:CandidateReady早于候选事实落盘(P1)generation.rs:3499:Archived早于公开 generation 删除(P1)ports.ts:203:冻结 Host Port 方法被改成可选(P1)AssetCanvasSurface.tsx:2709:快速编辑卡未做 viewport 四边钳制(P2)PR 181 本次栏目分页 / Dock 增量自身的问题是 #2315、#2316、#2317、#2318、#2319;权威文档合同问题是 #2326、#2327。
这里的“继承自 PR 176”描述的是 PR 181 所包含的早期提交来源;PR 176 当前 head 已继续推进,不能据此断言上述问题仍全部存在于 PR 176 当前 head。
WIP: 优化 Game Agent 资源管理页面UIto 完善 Game Agent 资源画布与图片精修生成事务当前 head
f9bca1f9adc74d60a8cea235b633a3485e29df72(base15a524e6e0026c1ae0f5a82e90781e3d49fa3f8b)复核结论:REQUEST_CHANGES。证据:
git diff --check通过;git merge-tree --write-tree无冲突。mergeable=true,但 master 保护要求至少 1 个 approval;review 247/248 都是旧 headc97130f0934的 stale review,当前 head 没有 approval。当前 head 的阻断项:
[P1] 候选确认操作没有接上线。
generation.rs:1575-1614的普通 update 现在只保护尚未确认的 CandidateReady 图层,真正解除保护需要acknowledge_candidate_layers_at(generation.rs:1617)。但AssetCanvasSurface.tsx:2758-2761生成成功后仍只调用persistDraft(),仓库里没有任何acknowledgeCandidateLayers调用;新建的 Tauri adapter 方法只是 dead code。结果是用户删除候选后,下一次普通保存会被后端把候选重新加回,候选无法按合同被删除。请把独立确认接入保存 FIFO,并让设为最终图、导入、生成、归档、放弃等 revision-sensitive 操作等待确认屏障;补 generate -> delete candidate -> save 不复活的集成回归。[P1] 新增的
ImageCanvasProjectPort.acknowledgeCandidateLayers在packages/image-canvas-core/src/ports.ts:202-205仍是 optional,而 Tauri adapter 又在tauriImageCanvasHostAdapter.ts:157-158强制 Required;这与本 PR 新增的docs/project-memory/shared-memory/pitfalls.md候选确认“独立幂等宿主操作 / 所有宿主方法始终存在”合同不一致。应统一为必选方法(不支持宿主返回结构化 unsupported),并覆盖 adapter 与 Surface 调用。[P1] 权威文档仍自相矛盾:PRD
:260说当前界面不会因指针操作新增manuallyPlaced=true,但同文件:315-316又规定拖动释放提交manuallyPlaced=true;PRD:56/GameAgent 方案:88-90已切换到非空分页无限画布,但 PRD:321、:529-536仍要求分区高度、内部滚动、内容倍率和旧验收。请选择唯一合同并同步代码、测试、PRD、技术方案与 project-memory。继承项(本轮明确不重复请求):
15a524e6e合并;review 247 的 #2320/#2321/#2322/#2323/#2324/#2325 明确标注为“继承自 PR176”,当前 head 已包含对应修复,未再作为当前 blocker。PR 正文仍需刷新:正文的 19 commits / 45 files、499/499(AppSurface 390/390)和“已处理 review #247”均不是当前 head 的事实;请改成当前 25 commits / 31 files、run 1285 的验证结果,并保留上述 PR176 继承 provenance。
当前 head
5456a1d86812c1679453c9ac5beaf4ed594c03e9(baseb00a3f80576949567bf332f0c6c92b4ed0a649d)复核结论:REQUEST_CHANGES。本轮不重复旧 head 的代码问题,只保留当前 head 仍真实存在的合同与追踪问题。证据:
git diff --check通过,git merge-tree --write-tree无冲突(b4145673e367b97dda2f312c94f04ca5c8c19efd)。mergeable=true,但 master 保护要求至少 1 个当前 head approval;review #247/#248/#252 均为旧 head 的 stale review,当前没有 approval。当前仍需处理:
[P2] PRD 仍残留已退役入口的旧合同。当前 head 的
docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md:55仍写“打开详情不得卸载新增资源 / 按依赖 / 按类型”,但本 head 的技术方案docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md:101和 2026-08-24 decision-log 已明确顶部移除“新增资源”手动入口;当前 PRD 的分页无限画布验收也只保留按依赖、按类型、复位等画布级动作。请把该处改成当前仍存在的动作,或明确标注为历史合同,避免后续按权威 PRD 把已删除入口重新接回。[P2]
docs/project-memory/shared-memory/pitfalls.md:4830-4833仍要求所有 viewport 经过资源 extent 的隐藏边界函数,并在 extent 变化时重新约束;当前 head 的技术方案:90-91明确栏目 viewport 可沿 x/y 任意方向平移、不以资源 extent 作为导航边界,PRD:320-321也采用同一无限画布合同。请在原坑条目上明确 superseded / 替换为当前语义,而不是只在 decision-log 追加新结论,否则后续 Agent 仍会按旧坑实现回归。[P2] PR 正文已过期:正文仍写 19 commits / 45 files、前端 499/499(AppSurface 390/390)以及“已处理 review #247”;当前事实是 27 commits / 33 files,run 1289 四项全绿,且 #247/#248/#252 都已因 head 更新变 stale。请刷新验证数字、当前 head/base、CI run 和评审 provenance。
已确认闭环、这次不重复请求:
@@ -56,2 +55,2 @@2. 阶段二“分区独立缩放”(已完成)只适用于 `type` 模式,同时包含互不替代的分区可视高度和内容倍率。文档、项目版本、美术资源、音乐音效资源的标题栏分别保留高度减小 / 恢复 / 增大,并新增 `50%..200%` 内容缩放减小 / 百分比复位 / 增大;触摸板捏合、`Ctrl/Cmd + wheel` 与按钮使用同一倍率模型,普通双指滚动仍只滚动。高度与倍率都以 `projectId + type + category` 隔离并只保留在当前会话内存;倍率以分区 viewport 中的手势中心为锚点,逻辑 `x / y` 和媒体比例不变;非图片资源维持 `180×128`,美术图片按真实比例和统一边界计算本体矩形。最小高度固定容纳标题操作与至少一排卡片,最大高度以中央资源画布当前可用高度为上限。分区保持正常文档流,后续分区由外层资源画布滚动访问,单区内容在缩放后仍只在该区内部滚动。两类表现态都不修改 manifest、`game-creator-resource-layout.v1`、布局 CAS 或资源身份,不重建媒体控件、不抢走焦点,也不恢复卡片拖动。`dependency` 模式使用统一世界自由画板,不展示这些分区滚动与缩放操作。3. 阶段三“依赖布局聚类”(已完成):仅在 `dependency` 模式中,按固定资源分类对当前 Rust 只读图返回的同类型精确引用边和同类型聚合任务流做弱连通分组;任务流以流节点连接其成员,绝不展开成资源笛卡尔积,但只作为布局邻近提示,不绘制虚线、不进入画布关系说明。`dependencyDepth` 是唯一横向层级权威;每个相关簇先按“最小深度 -> 最小稳定资源 ID”排序,再在同层进行固定两轮由左至右、由右至左的中位数扫描,平局回退稳定资源 ID;同 SCC/环成员保持连续,环后资源继续按 Rust 深度推进。孤立资源是稳定单例组,紧凑排在相关簇之后,簇之间保持固定留白。跨分类精确引用不参与前端布局邻接,但会在统一世界 SVG 中显示并进入同源的无障碍关系说明;搜索只隐藏卡片及任一端点被隐藏的橙色精确引用边,不重算组或压缩坐标。历史 `manuallyPlaced=true` 坐标继续原样保留并参与自动坐标避让。聚类是可派生表现,不新增 manifest 字段、sidecar schema、SpacetimeDB 表或第二套依赖真相。1. 阶段一“资源卡本体化”:`dependency / type` 两种布局共用同一个资源卡组件和预览调度器。图片与安全 SVG 直接显示主体并保留透明棋盘底;图片卡按真实纵横比在统一边界内计算展示矩形,视频、音频、文档和项目版本继续使用稳定固定卡片。资源详情是叠加在原画板上的非模态独立卡片,以元数据、Rust 权威依赖关系和版本信息为主;打开详情不得卸载“按依赖 / 按类型 / 复位”等现役画布级动作、资源卡、依赖连线或重置 viewport、搜索和排序模式。2. 阶段二“分区独立缩放”是历史实现;2026-08-22 起仅在项目完全没有任何资源投影时保留“按类型式分区展览”作为空态。任一栏目出现资源后,`dependency / type` 两种模式均切换为栏目分页画布,默认停留在固定栏目顺序中的第一个非空栏目;大纲展示全部栏目,空栏目计数为 0 且可打开空画布,不再提供分区高度、分区滚动和分区倍率操作。1