Merge remote-tracking branch 'origin/master' into opt/design_agent
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m22s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m24s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m45s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m39s
Project CI / Repository checks (pull_request) Failing after 19s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m1s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m10s
Project CI / Frontend tests (pull_request) Successful in 4m50s
Project CI / Native shell tests (pull_request) Successful in 7m19s

This commit is contained in:
2026-09-14 21:02:15 +08:00
40 changed files with 8890 additions and 527 deletions
@@ -1,6 +1,6 @@
# AI 游戏创作项目开发工作台 PRD
更新时间:`2026-09-13`(新增功能画布底部工具栏入口矩阵 §3.10 / §7.9;右侧 Supervisor 对话气泡、可访问对比度与过程卡布局收口,以及资源卡预览、分区布局、非破坏性资源编辑、资源替换与 Godot 双根合同保持不变)
更新时间:`2026-09-14`(同步功能画布图片类生成后台化的当前事实:入口 IPC 改为 `start_local_project_asset_generation` + 项目内任务账本 + 本地排队 + 非模态「生成任务」面板,见 §3.10 / §7.9;2026-09-13 新增的功能画布底部工具栏入口矩阵 §3.10 / §7.9,以及右侧 Supervisor 对话气泡、可访问对比度与过程卡布局收口,资源卡预览、分区布局、非破坏性资源编辑、资源替换与 Godot 双根合同保持不变)
## 1. 产品定位
@@ -142,7 +142,7 @@
### 3.10 功能画布底部工具栏(入口矩阵)
实现状态(2026-09-13):功能画布 = 资源栏目页(`resourceBookState.view === 'child'`)。栏目页左下角的底部工具栏按 manifest 资产的**功能分类** `category` 分流,矩阵由用户 2026-09-13 拍板,工具项顺序即下表顺序(最右一项为「上传」)。
实现状态(2026-09-13;2026-09-14 图片类生成后台化:提交即返回 + 项目内任务账本 + 本地排队 + 非模态「生成任务」面板):功能画布 = 资源栏目页(`resourceBookState.view === 'child'`)。栏目页左下角的底部工具栏按 manifest 资产的**功能分类** `category` 分流,矩阵由用户 2026-09-13 拍板,工具项顺序即下表顺序(最右一项为「上传」)。
| 栏目 | 工具栏里的生成入口 |
| --- | --- |
@@ -154,11 +154,12 @@
- 位置与层级:画布左下角(`left: 14px; bottom: 14px; z-index: 40`)。右下角是既有的缩放 / 撤销 Dock(`right: 14px; bottom: 14px`),左下角是画布上唯一两者都不占的稳定空位。工具栏是管理区 `.game-resource-book-manager` 的**直接子节点**、与画本场景并列,不进带 `scale()` 的场景层;二级菜单与「入口不可用原因」都贴着工具栏上沿弹出,不做内嵌内容。
- 外壳用共享 chrome(`packages/image-canvas-react` 的 `CanvasToolbar / CanvasToolbarGroup / CanvasChromeButton`),样式落在 AGC 的 `resourceCanvasChrome.css`;共享包只承接通用表现,不含业务规则。
- 接线:图片类入口走本地 IPC `generate_local_project_asset`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`);音频入口复用既有的无源生成链路 `derive_local_project_resource`(`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`);「上传」复用 `upload_local_asset`。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
- 面板形态:独立浮层(`ThemedModal`),**不在当前面板下面追加内容**;面板内不写功能说明或规则解释文案。
- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口复用既有的无源生成链路 `derive_local_project_resource`(`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`);「上传」复用 `upload_local_asset`。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
- 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(形态对齐网页端美术画布的任务侧栏):折叠时留贴边把手并在把手上显示在途数量,展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界);每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布左侧标题栏之下、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。
- 面板形态:独立浮层(`ThemedModal`),**不在当前面板下面追加内容**;面板内不写功能说明或规则解释文案。**点「生成」即同步关闭面板**(不等 IPC、不等排队、不等生成),面板里**不出现**「排队中。」「正在生成。」「提交中…」这类阶段文案——阶段文案的唯一去处是任务侧栏与工具栏提示条。**只有「点击瞬间就失败」**(校验不过、权限拒绝、start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消请求(请求挂在任务与账本上,不挂在面板生命周期上)。
- 参数口径:比例 / 尺寸选项来自网页端美术画布的纯模型(`src/components/image-editor/ImageCanvasGenerationModel.ts`),并按本地 IPC 白名单收窄(本地通道明确拒绝 `4:3`);默认档 `1:1 · 1K`,生成 UI 设计图沿用网页端 UI 设计面板的默认 `16:9 · 1K`。本地 IPC 没有 `model` 入参,因此面板**不渲染模型选择器**(渲染一个改不了请求的控件就是假控件)。
- 规范类和固定档:生成规范下的图标规范 / 角色规范 / 自定义规范都是固定档——只读展示当前规格,不给点了不生效的比例控件。本地 IPC 只有一条规范通道(`spec` 在 Rust 侧收口到 `icon-spec`),**没有 `specType` 入参**,因此角色规范与自定义规范共用该通道,靠素材名与提示词区分。
- 前置规范图:`generate_local_project_asset` 的 `ui-prototype` 与 `art-spritesheet` 要求项目里已有登记并绑定当前账号的 `assets/art-spec.png`(`kind: icon-spec`、图片媒体类型、来源为画布)。缺前置时对应入口**保持可点击**并给出可执行的原因与指路(仓库口径:视觉不可用但可点击说明原因,不用原生 `disabled`),原因文案包含 `assets/art-spec.png` 与「生成规范 → 图标规范」。
- 前置规范图:`start_local_project_asset_generation` 的 `ui-prototype` 与 `art-spritesheet` 要求项目里已有登记并绑定当前账号的 `assets/art-spec.png`(`kind: icon-spec`、图片媒体类型、来源为画布)。**后台化没有改这条前置判据**:Rust 侧仍是同一口径,前端 `projectHasIconSpecReference` 仍与它逐字对齐,缺前置时对应入口**保持可点击**并给出可执行的原因与指路(仓库口径:视觉不可用但可点击说明原因,不用原生 `disabled`),原因文案包含 `assets/art-spec.png` 与「生成规范 → 图标规范」。
- 权威规范图由工具栏自身可满足:「图标规范」入口在项目尚无权威规范图时按 `assets/art-spec.png` 落盘;已有权威规范图时**不再传 `outputPath`**(Rust 侧 `replace_existing` 固定为 `false`,指向已存在文件会被硬拒绝「outputPath 已存在,禁止静默覆盖」),改为生成一张新的普通图标规范资产。这条前端策略不扩接口。
- 音频入口收敛:音频画布的「生成背景音乐 / 生成音效」由工具栏承载,既有「生成素材」浮层入口**只保留生成视频**(视频能力不删,只是不出现在工具栏里)。
- 本轮明确不做:生成视频(保留在既有浮层入口)、宣发素材、生成游戏场景、画布级「选择工具 / 抓手工具」(AGC 现无画布级工具模式)。这五类都**不得**出现在工具栏里。
@@ -652,11 +653,12 @@ type ProjectAgentMudPointAttribution = {
1. 工具栏只在资源栏目页且分类命中矩阵四个栏目时渲染;资源总览、「所有资源」展开态、文档 / 待归类 / 项目版本都不渲染。稳定 DOM 判据 `[data-resource-bottom-toolbar="<category>"]`;总览与「所有资源」展开态下该节点必须不存在。
2. 工具栏是管理区 `.game-resource-book-manager` 的直接子节点、与画本场景并列(`closest('.game-resource-book-scene')` 为 `null`),且与右下角 Dock 互不抢位(左下角锚定:`left` 有值、`right` 无声明;Dock 相反)。二级菜单只在展开时出现,展开时可被外部点击 / Esc 收起。
3. 每个图片类入口一次提交**一条** `generate_local_project_asset`,载荷字段逐字为 `{ projectPath, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`;`kind` 映射:生成图片 `image`、生成角色形象 `character`、图标规范 `icon-spec`、角色规范 / 自定义规范 `spec`、生成图标素材 `art-spritesheet`、生成 UI 设计图 `ui-prototype`。
3. 每个图片类入口一次提交**一条** `start_local_project_asset_generation`,载荷字段逐字为 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`——**原同步命令的生成载荷字段名与取值口径逐字不变**(`kind / prompt / aspectRatio / imageSize / assetName / outputPath` 全部原样搬过来),只多了任务身份 `projectId` 与前端每次提交新铸的本地 `taskId`;`kind` 映射:生成图片 `image`、生成角色形象 `character`、图标规范 `icon-spec`、角色规范 / 自定义规范 `spec`、生成图标素材 `art-spritesheet`、生成 UI 设计图 `ui-prototype`。命令**提交即返回**一条任务记录,生成在 Rust 后台任务里跑,状态与阶段由 `list_local_project_asset_generations` 从项目内账本读回。
4. 比例 / 尺寸选项**不含**本地通道拒绝的 `4:3`;比例与尺寸的切换都进入载荷;固定档规范入口只读展示当前规格(如 `1:1·1K`),不渲染可点比例控件。
5. 缺权威规范图时「生成图标素材 / 生成 UI 设计图」保持可点击、给出含 `assets/art-spec.png` 的原因说明,且**不发出任何生成请求**;「图标规范」此时把 `outputPath` 指向 `assets/art-spec.png`;项目已有权威规范图时 `outputPath` 为 `null`(两种状态各一条用例)。
6. 生成 / 上传成功后走既有刷新路径:`get_local_game_project_revision` + `get_local_game_manifest` 的配对读发生在写入命令之后,结果交给 `onManifestChange`;音频入口提交的是 `derive_local_project_resource`(`editKind` = `sound-effect` / `background-music`、`generationMode: 'create'`、`sourceMediaType: 'audio/mpeg'`)。
6. 生成 / 上传成功后走既有刷新路径:`get_local_game_project_revision` + `get_local_game_manifest` 的配对读发生在**该任务收口之后**(提交命令不再等待生成,配对读由任务终态驱动,顺序仍在写入之后),结果交给 `onManifestChange`;音频入口提交的是 `derive_local_project_resource`(`editKind` = `sound-effect` / `background-music`、`generationMode: 'create'`、`sourceMediaType: 'audio/mpeg'`)。
7. 既有「生成素材」浮层入口只呈现视频:面板不再渲染音效 / 背景音乐类型选择器(单类型入口没有类型选择器),音频入口只出现在音频栏目工具栏。
8. 生成期间可退出与本地排队(2026-09-14):提交后点 × / 遮罩 / Esc /「后台运行并关闭」任一都能关面板,画布立即恢复可交互,关闭不取消请求;面板关闭后任务仍在「生成任务」面板(非模态浮层,无 `aria-modal`)里显示后端 `phaseDetail`;第一条未终态时提交第二条 → 第二条显示「排队中。」且生成提交 IPC 次数仍为 1,第一条终态后自动补发(次数为 2);两条各自完成后各走一次「配对读 + 落卡」,manifest revision 单调、不被后到者覆盖。
## 8. 非目标
@@ -3,6 +3,44 @@
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
> 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
## 2026-09-14 生成进度面收敛为「常驻可折叠任务侧栏」;提交即关面板、阶段文案只归侧栏;定位动作终局化
- 背景(验收人在真机上连报三条):① 提交按钮上渲染了后端 `phaseDetail`,「生成图片」的主按钮变成写着「排队中。」的状态胶囊;② 提交后提交面板不关、一直占着屏幕等生成,用户原话「不要显示排队中,点生成直接把窗口藏起来啊,你留个窗口意义何在」;③ 任务进度面是一个工具条按钮 + 非模态浮层,跟网页端美术画布的任务侧栏不是一个形态,用户原话「你把美术画布的照抄过来都不会吗」;④ 「定位到素材」点了没反应,提示条永久停在「正在定位生成的素材…」。
- 决策(提交面板):**点「生成」即同步关闭面板**——不等 IPC、不等排队、不等生成;面板内**不出现**任何阶段文案(主按钮文案恒为动作名)。**只有「点击瞬间就失败」**(后端校验 / 权限拒绝 / start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在任务侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消(请求挂在任务与项目内账本上,不挂在面板生命周期上)。
- 决策(进度面形态):改为**常驻画布的可折叠任务侧栏**(对齐网页端 `ImageCanvasTaskSidebarView`)——折叠时留贴边把手并在把手上显示在途数量;展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 提示「仅显示最近 N 条」);每项显示状态徽标 / 后端阶段文案 / 已耗时(前端 1s 计时)/ 素材名 + 「定位到素材」;侧栏非模态(不铺遮罩、不做焦点陷阱、不进模态遮挡判据),工具栏入口按钮常驻并显示 `生成任务 · N`,提交受理后自动展开。**原先的非模态浮层形态已删除,不留平行入口。**
- 决策(侧栏位置):挂在画布**左侧、标题栏之下、工具栏之上**,宽 300px、**覆盖式**(不 reflow 挤窄画布视口)。理由:右侧已被「智能创作」对话面板占用、顶部是栏目标题栏、底部是栏目工具栏;覆盖式不触碰画布视口数学与资源卡排布,收起即完全让出画布。若产品要求「画布被挤窄」的 flex 兄弟列形态(网页端是那种),需要改 `game-resource-book-manager` 那段布局并单独排期。
- 决策(定位终局化):根因是聚焦 effect 的依赖全是画布自身状态,**手动点定位不改其中任何一项** → effect 不重跑、`pendingResourceFocusRef` 无人消费、提示条永久停在中转文案。修法:新增聚焦请求序号并加入 effect 依赖;handler 重写为「能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给『素材已不在项目里 / 已登记但尚未同步』的结论;挂 intent 后推进序号 + **3 秒有界兜底**;intent 被判 invalid 时也给『定位请求已失效』」,并修掉「已聚焦过」提前返回分支不清提示的同类问题(自动落卡那条链同源)。
- 影响范围:`src/features/resource-canvas/{ResourceCanvasAssetGenerationPanelView.tsx,ResourceCanvasGenerationPanelView.tsx,ResourceCanvasAssetGenerationTasksPanelView.tsx,resourceCanvasAssetGenerationTaskModel.ts,resourceCanvasAssetGenerationQueue.ts}`、`src/view/project-development/index.tsx`,测试 `tests/{resourceCanvasAssetGenerationBackgroundClose.test.tsx,resourceCanvasAssetGenerationTasksPanel.test.tsx,resourceCanvasBottomToolbar.test.tsx,appSurface/project-development.suite.ts}`。**未改** IPC 形状与 Rust 生成通道、未改本地排队语义(仍单条在途)、未改 `packages/**`。
- 验证方式:`appSurface.test.ts` 439 passed、定向 7 文件 93 passed、侧栏用例 7 passed;变异验证(均已实测):① 提交后不关闭面板 → 面板用例 `expected "spy" to be called 1 times, but got 0 times` 与 AppSurface `expected <section …> to be null` 红;② 去掉即时失败重开 → `Unable to find role="dialog" and name "生成 UI 设计图"` 红;③ 去掉提交后自动展开 → `Unable to find an accessible element with the role "region" and name "生成任务"` 红;④ 折叠顺手清空任务列表 → `expected '0' to be '1'` 红;⑤ 去掉聚焦请求依赖 → `expected null not to be null`(卡片从未被选中)与 `expected <span></span> to be null`(提示条仍停在中转文案,即用户报的现象)红。
- 关联文档:[栏目画布底部工具栏入口矩阵](../../technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md)、[项目开发工作台 PRD](../../prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md)、[踩坑记录](pitfalls.md)。
## 2026-09-14 放开 AGC 手工图片生成的本地并发:durable 输出槽身份改为「精确动作指纹」
- 背景:AGC 手工图片生成的 durable 输出槽身份是 `run_id = slot-<sha256({outputPath, requireSlices})>`,而工具栏除「图标规范」首次外 `outputPath` 恒为 `null`、`requireSlices` 恒 false → 同项目所有图片类生成共用一个槽,第二条并发请求在任何远端 POST 之前就被拒(`external_generation_state.rs` 的 singleflight 报「durable 图片生成输出槽已有请求执行中」)。验收反馈里的高优问题(生图期间不能退出、也不能在生成 A 的过程中生成 B)既要前端可退出,也要后端具备并行能力。
- 决策(槽身份 = 精确动作身份):`run_id = slot-<sha256(动作身份材料)>`,材料为 `prompt / output_path / aspect_ratio / image_size / asset_kind / asset_label / replace_existing / require_slices`(`canvas_generation.rs`)。不同 prompt 或素材名 → 不同槽 → 不同进程锁键与不同 `.lock` 文件 → 可同时在途。**不用随机 uuid**:随机身份会让「同一精确动作重放」落到新路径,必须再造一层 action→ledger 索引才能保幂等;用动作指纹让「槽身份 ≡ 精确动作身份」,路径查找即幂等查找。
- 决策(幂等不变):同一精确动作 → 同一路径 → 命中已有 prepared/accepted 账本并复用原 `idempotencyKey` / `operationId`,不二次 POST;相同动作并发仍被拒的既有语义保持。
- 决策(旧槽账本最小懒迁移):旧槽账本形状可读、不 panic、不 fail-closed;在 durable guard 之后、任何远端 POST 之前,**仅当**旧槽账本的 `agentId / runId / actionFingerprint` 与本次精确动作一致时,把它迁移到新路径(保留 `idempotencyKey` / `operationId` / 状态)并删除旧文件;属于其他动作的旧账本一律不动。旧「固定槽」(`run_id == agent_id`)账本的 fail-closed 拒绝保持原样。
- 已知边界:`slice_count` **不进**身份(保留升级前粒度,也是旧账本迁移可行的前提)→ 仅切片数不同的两条图集请求仍共槽、第二条失败关闭;当前所有 standalone 槽的生产调用方都把 `slice_count` 传成 `None`(唯一能传 sliceCount 的是 agent 工具通道,它不走 standalone 槽),该边界当前不可达,但**缺负向用例**。
- 前端口径:本批**仍保留单条在途的前端排队**(提交节流),真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs`、`.../external_generation_state.rs`。**未改** `/api/external/v1` 契约 / OpenAPI / DTO,未改 `recovery_scan.rs`(身份白名单与孤儿清理语义不变),账本 schema 仍是 v3、字段集不变,只改 `runId` 取值来源。
- 验证方式:新增 `standalone_generation_binds_each_exact_request_to_its_own_stable_slot`、`distinct_standalone_actions_hold_independent_durable_output_slots`、`concurrent_distinct_standalone_generations_both_succeed_with_one_post_each`(端到端:两条 `outputPath=None` 的不同动作要求两条 POST 同时到达,各自 poll → read-url → 下载 → 落盘)、`legacy_output_slot_ledger_is_adopted_by_the_same_exact_action_only`。变异验证(已实测):把 `run_id` 退回旧公式 → 4/4 红(含「durable 输出槽身份必须等于该精确动作的身份」与并发用例的「任何远端 POST 前拒绝并发请求」);把懒迁移短路 → 旧账本用例红。定向 `agent::generation::` + `recovery_scan` 95 passed、`direct_runtime media` 195 passed。
- 关联文档:[栏目画布底部工具栏入口矩阵](../../technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md)、[踩坑记录](pitfalls.md)。
## 2026-09-14 「素材类型」从「编辑素材标签」面板拆成独立入口
- 背景:2026-09-11 的决策把类型选择器放进「编辑素材标签」面板(与标签同一次保存、同一条写入路径,不在工具条另开入口)。真机使用暴露两个问题:① 那排 chip 在弹窗里没有任何标题,读不出是什么;② 类型改动**没有自己的提交动作** —— 点 chip 只写本地 state,落盘发生在底部「添加」上(那是标签语义的按钮),只选类型后直接关弹窗会静默丢失。
- 决策(入口独立):新增 `ResourceTypePanel`(标题与 `ariaLabel` 均为「设置素材类型」),**选中即落盘**(一次动作一步完成),`category` 传用户选中值、`tags` 传 `gameCreationAppAssetTags(asset)` 的落盘原值。入口两处:选中卡浮动工具条「素材类型」按钮 + 信息浮层「分类」行的「设置」。
- 决策(选项形态,真机反馈后修订):选项区**必须是纵向单选列表**,一行一个(容器 `role="radiogroup"`、每项 `role="radio"` + `aria-checked`,选中态与读屏共用 `aria-checked` 并由 CSS 直接驱动;roving tabindex + 方向键移焦点、**Enter/Space 才落盘**)。真机上第一版用了共享 `PlatformSegmentedTabs`(`columns="threeToSix"`)→ 6 个选项挤成一行互相叠字,验收人原话「做成列表,而不是全都一条」。**有意偏离 APG**:方向键不顺手选中——本面板「选中 = 一次 CAS 写盘 + 宿主收窗」,方向键即选中会让浏览 6 个选项变成连环写盘、第一次按键就关窗。行骨架复用共享 `PlatformNavigableListItem`(未复制共享 UI、未改 `packages/**`);列表 `max-height: min(320px, 40dvh)` + 独立滚动,标题/素材名/错误提示不滚,行高 44px 移动端优先。这一版值得后续抽成 `packages/shared` 的 `PlatformRadioList`(或给 `PlatformSegmentedTabs` 加 vertical 档),本批按边界未做。
- 决策(标签面板去掉类型控件):「编辑素材标签」面板删除 chip 与 `categoryChoice` 分叉,保存时 `category: gameCreationAppAssetPersistedCategory(asset)`;「没碰过分类就回传落盘原值」这条不变量改为**结构性保证**(面板里根本没有类型控件),两条对照用例迁到新面板并保留。
- 影响范围:`src/view/project-development/{ResourceTypePanel.tsx,ResourceClassificationPanel.tsx,ResourceInfoPanelView.tsx,resourceCanvasInfoModel.ts,index.tsx}`、`src/features/project-workspace/resourceTypePanel.css`、`tests/{resourceTypePanel.test.tsx,resourceClassificationPanel.test.tsx,projectResourceLiveIntegration.test.tsx,appSurface/project-development.suite.ts}`。不改 `update_local_project_resource_classification` 的入参形状与 CAS 口径、不改读时自愈语义、无后端与 schema 变更。
- 验证方式:`resourceTypePanel.test.tsx` 新 12 条 + `resourceClassificationPanel.test.tsx` 19 条 + `appSurface.test.ts` 431 passed。变异验证(已实测):新面板 `category` 改回回传落盘原值 → 「改类型生效」用例红;标签面板改用显示口径 → 对照用例出现 `- "category": "unclassified" / + "category": "ui-interaction"`;去掉浮层判据里的新 state → 点外部串台用例红。
- 关联文档:[AGC 资源工作台 V3 端到端验收用例](../../technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md) 的 S12。
## 2026-09-14 同步命令 `generate_local_project_asset` 退役为「仅测试调用」
- 决策:图片类生成接线改为 `start_local_project_asset_generation` + `list_local_project_asset_generations` 后,同步命令 `generate_local_project_asset` **已无生产调用方**,只剩 `src-tauri/src/tests/project.rs` 的三条集成用例与 `commands.rs` 的自身单测在调它;因此登记进 `scripts/check-config.mjs` 的 native-only 白名单(该门禁有「App invoke 与白名单互斥」断言,谁重新给它接调用方就必须同时删掉这条白名单项)。
- 待办:它是**注册中的可调用 IPC**,一旦被将来代码调用就是一条绕过任务账本、单次阻塞最长 35 分钟的并行生成路径。下一批次应删除它,或改为转调 `start_local_project_asset_generation`(连带迁移那三条集成用例)。
- 关联文档:[栏目画布底部工具栏入口矩阵](../../technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md)。
## 2026-09-14 AGC 壳 Rust 套件按「一片一 job」拆分,客户端 Rust 关键路径压到 7 分钟以内
- 背景:`AI game creator shell Rust tests` 是客户端 CI 的关键路径(run 2097 实测 15 分 27 秒)。拆开来看:前置 5 分 30 秒(checkout 10s + `npm ci` 2m45s + Cargo fetch 2m35s)、编译 1m39s、**AGC 壳 bin target 的 2466 条单测串行 507s**、`agent-run` smoke 51s。这 2466 条全在 `apps/ai-game-creator-shell/src-tauri` 的 bin target 里,一条 `cargo test … -- --test-threads=1` 跑完。
@@ -15,6 +53,15 @@
- 影响范围:`.gitea/workflows/project-ci.yml`(十一个 job)、`scripts/check-native-shells.mjs`(分组由五个到十个:新增 `agc-rust-crates`、`agc-rust-shard-1..4`、`agc-rust-smoke`,移除 `agc-rust` 与随后的 `agc-rust-shell`)、根 `package.json`、`scripts/project-ci-workflow.test.ts`(新增纯 cargo job 免 `npm ci`、分片运行器覆盖校验、crate 级 job 预热顺序断言)、开发运维文档与共享记忆。本仓库不把 Project CI 的 context 配成 `master` 分支保护的合并必需检查(2026-09-14 复核),合并前由人工确认结果,因此 job 拆分/改名不需要同步分支保护设置。
- 验证方式:`npx vitest run scripts/project-ci-workflow.test.ts`;分片运行器本地以 `agent-runtime-core`(7 条 → 2/2/2/1)与 `platform-llm`(146 条 → 49/49/48)验证分片、`--exact` 与片 TMPDIR 隔离,负例 `--shard-index=5` 立即失败;`node scripts/check-native-shells.mjs --groups=contract` 回归。预期每个分片 job 收敛到 5 分钟以内(前置约 1 分 30 秒 + 编译约 1 分 39 秒 + 约 617 条用例)。
- 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)、[踩坑记录](pitfalls.md)。
## 2026-09-14 AGC 资源画布改为「手动整理」:新素材不再自动重排,整张重排只由「整理画布」发起
- 背景:生成一张新素材会让整张资源画布重排。两个 layout hook 都把 `rederiveAutomaticPositions` 打开(type 侧无条件 `true`,dependency 侧长期等于 `resourceGraphReady`),而该开关的语义是「每次资源协调签名变化就丢掉全部 `manuallyPlaced=false` 坐标、按当前资源与拓扑整体重算」;新增一张素材必然改签名,于是既有自动卡全部跟着挪位,用户刚记住的位置就没了。画布上也没有任何显式整理入口(`复位资源视图` 只复位视口)。
- 决策一(默认口径):两个 mode 的 `rederiveAutomaticPositions` 固定 `false`——画布默认只补新卡,不动任何既有坐标(`preserve`)。整张重排改为显式动作:资源工具条动作区新增**独立的**「整理画布」动作按钮(**不在**「资源排列方式」这个 `role="group"` 内——它是一键动作,不是第三种排列方式;真机上第一版塞进排序 tab 组里被验收人指出「整理画布的按钮独立出来,不要塞到那个里面」,已移出;第二轮又被指出「不要放在最右边」,所以**最终位置固定在「生成素材」之后、「管理未完成编辑」之前**(紧邻同类资源动作、在排序组左侧,不做这一行的行尾按钮——行尾会被读成「针对整个工具条」的动作)),调用 hook 新暴露的 `rederiveNow()`,复用既有写队列与 sidecar 写回链路,只把策略换成 `rederive`;用户可见反馈继续用既有 `resourceLayoutNotice`(成功即「布局已保存」)与 `resourceLayoutSaving`,不新增状态位。重算结果与当前坐标一致时**不落盘**(沿用既有 `changed` 门):已经整齐的画布按一下不该白推进一次 CAS / revision,关系图 `producerMappingTruncated` 时更不该把一份来自不完整关系图的自动布局写进 sidecar——既有用例 `keeps trusted truncated-graph depths through the workbench without persisting a flat automatic layout` 就是钉这条。
- 决策二(依赖图首次就绪的那一次):`dependencyDepth` 仍要在关系图就绪后按最终拓扑排一次列。这一层不再靠「让布尔长期为真」,而是按**项目作用域的一次性 flag**(`dependencyRederiveScopeRef`):关系图就绪且该侧 sidecar `ready` 的那一刻调用一次 `rederiveNow()`,之后一律 `preserve`;切排序 tab 不重新武装,切项目才重新记一次。
- 决策三(生成后自动聚焦):新增 `seenManifestAssetIdsRef` + effect,按 `manifest.assets` 的**新增 id**(不是 diff 位置、也不是文件名)把新卡交给既有 `pendingResourceFocusRef` + `advanceFocusGeneration()` 裁决链。首次打开项目 / 切项目只登记基线、不聚焦;重命名不改 id、天然不触发;已有指向同一资源的聚焦意图时不重复挂(显式生成链路在提交时就已挂好)。被搜索条件挡住时继续复用既有的「清除搜索并定位」提示与动作。
- 影响范围:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts`(写意图增 `rederive` 标记、写策略分支、`rederiveNow`)、`.../index.tsx`(两个 hook 配置 + 一次性重派生 effect + 新素材聚焦 effect + 「整理画布」按钮)、`apps/ai-game-creator-shell/tests/resourceCanvasManualLayout.test.tsx`。不改 `manuallyPlaced` 语义、不改 sidecar 的 CAS / revision 协议与字段形状、不改卡片尺寸模型、不改「搜索不重排」合同、不改 Rust 资源图与 `dependencyDepth` 权威。
- 验证方式:新增 `resourceCanvasManualLayout.test.tsx` 8 条:新素材入库后除新卡外坐标逐值不变、依赖侧首次就绪重算一次后同样不再重排、新素材自动聚焦、被搜索挡住走既有提示与动作、「整理画布」按 `rederive` 重算并保留手动坐标且给出一次可见反馈、已经整齐时再按一次不产生第二次落盘、首次打开与切项目都不聚焦。变异验证(均已实测):① type 侧改回 `true`、dependency 侧改回 `resourceGraphReady` → 3 条红;② 去掉新素材基线的首次登记 → 3 条红(既有卡被当成"刚生成"选中);③ 去掉 hook 里的 `rederive` 写策略分支 → 3 条红;④ 把显式整理改成强制写回(去掉 `changed` 门)→ 既有「截断关系图」用例红。
- 关联文档:[踩坑记录](pitfalls.md)、[项目开发工作台 PRD](../../prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md)。
## 2026-09-14 客户端 CI 按门禁组拆成三个 job,AGC 的 web / rust 两段并行
@@ -56,7 +103,7 @@
## 2026-09-11 素材类型(功能分类)重新提供用户入口;资源卡角标改显示资源类型而非媒体类型
- 背景:`6bdc8bbd9` 把「分类与标签」面板收敛为纯标签面板,并明确记下「随之的事实是:**「用户手动设置 `category`」这项能力就此移除**」(本文件 2026-09-11 那条以「用户给出的目标样式截图里,这个面板标题是「编辑素材标签」…」开头的条目,其决策一行即该结论)。用户随后要求「用户可以自己变更素材类型」,并追加要求把资源卡右上角角标从"文件/媒体类型"(图片 / 视频 / 文档 …)改成"资源类型"(功能分类中文名)。现状核实:写入链路本来就是完整的 —— `update_local_project_resource_classification`(`src-tauri/src/commands.rs:2162`)→ `update_manifest_asset_classification_at`(`src-tauri/src/project/manifest.rs:1112`)已接受任意合法 `category` 并校验 6 个合法值,**本次不需要改 Rust**;缺的只有 UI 入口与角标口径。本条**只回收「面板不再编辑分类」这一项**,「面板标题仍是「编辑素材标签」、删除资源入口仍在选中工具条」等其余结论不变。
- 决策一(入口):类型选择器加进**「编辑素材标签」面板**(与标签同一次保存、同一条写入路径,不新增第二条命令、不在工具条另开第二个入口)。选择器复用 `GAME_CREATION_APP_ASSET_CATEGORIES` × `resourceReferenceCategoryLabel`,不新造第二套中文译名。
- 决策一(入口)【已被 2026-09-14「「素材类型」从「编辑素材标签」面板拆成独立入口」取代】:类型选择器加进**「编辑素材标签」面板**(与标签同一次保存、同一条写入路径,不新增第二条命令、不在工具条另开第二个入口)。选择器复用 `GAME_CREATION_APP_ASSET_CATEGORIES` × `resourceReferenceCategoryLabel`,不新造第二套中文译名。
- 决策二(两个口径的分叉,本次核心不变量):选择器**读显示口径** `gameCreationAppAssetCategory`(与画布栏目 `projectResourceAssetCategory` 同源,用户看到的选中项就是他看到的栏目);**写回**用 `categoryChoice` 区分用户是否主动选过 —— `null`(没碰过控件)回传 `gameCreationAppAssetPersistedCategory` 的落盘原值,非 `null` 写用户选的值。这条分叉同时满足"只改标签不漂移分类"与"用户选了就写用户的值",两个方向都有对照用例(见验证方式)。
- 决策三(角标):资源卡右上角角标改为**资源类型**,取值 `categoryLabels[resource.category]`(栏目与筛选共用的同一份文案),因此角标恒等于该卡所在栏目;媒体类型仍由卡面视觉(图片 / 视频 / 音频 / 文档摘要)表达。只改这一处渲染(`index.tsx` 的 `ResourceCard`),三处面(栏目画布卡、「所有资源」展开态卡、总览缩略摞上铺的卡)自动一致;`projectResourceTypeLabel` 保留给「资源管理面板」的「分类 · 类型」小字与总览摞分列,不再用于角标。
- 画布跟随链(核实结论,**无需额外迁移代码**):`useProjectResourceCanvasLayout` 的 `createResourceSignature` 已把 `resource.category` 计入签名,分类变化 → 签名变化 → `reconcileResourceCanvasLayout` 按新的 `section` 归并(`resourceCanvasSectionMapping.resolveResourceCanvasSection`:现行栏目值原样归到资源当前分类,x / y / `manuallyPlaced` 原样保留)→ 需要时写回 sidecar。卡片随分组落到新栏目。
@@ -8679,3 +8726,14 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 决策:复用判据收窄为**同一条写调用链(同一线程)重入**——按锁路径登记真实持锁线程,只有当前线程就是持锁线程时才返回 advisory guard;本进程其它线程的争用继续走有界等待与终态占用。自主游戏构建流水线的并行专家动作豁免保持不变;跨进程占用、残留回收、权限分类、等待预算和错误文案不变。
- 边界:锁定这些不变量的既有用例(`project_tools` / `command_runtime` / `parallel_actions` / `runtime_state` / `response_stream` / `direct_tool_bridge` / `ui_editor::persistence`)不得为了让锁语义通过而改写;用「同线程自持锁」模拟「另一个写者」的两条用例改为**在另一条线程持锁**,断言语义不变。同进程跨线程重入(持锁链在 `await` / `spawn_blocking` 后于其它线程再取锁)仍会等满预算,出现现场时按 2026-08-27 的既有处置改用 `*_locked` 入口,不放宽判据。
- 关联文档:[项目客户端占用锁收敛里程碑](../plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md)、[踩坑记录](pitfalls.md)。
## 2026-09-14 AGC 图片类生成后台化:提交即返回 + 项目内任务账本 + 本地排队
- 背景:栏目画布图片类入口原先是一条同步 IPC `generate_local_project_asset`,一次调用最长等 35 分钟;提交期间两块生成浮层把 × / 遮罩 / Esc /「取消」全部锁死,用户既关不掉面板也看不到进度。远端图片类生成当时共用 single-flight 输出槽(`standalone_platform_art_generation_runtime_context` 按 `outputPath` 派生 `run_id`)——**同一批次内该槽身份已改为精确动作指纹,见本文件 2026-09-14「放开 AGC 手工图片生成的本地并发」条目。**
- 决策(后台化):新增 `start_local_project_asset_generation`(校验入参 → 落 `queued` 记录 → `tauri::async_runtime::spawn` 派发 → 返回任务记录)与 `list_local_project_asset_generations`(读回账本)。生成本身仍转调既有 `generate_platform_art_asset_with_options_at`(幂等账本、计费、下载校验、manifest/revision 登记、本地预览都不复制),同步命令 `generate_local_project_asset` **保留不动**。
- 决策(账本落项目内):`.agent/runtime/asset-generation-tasks/tasks.json`,复用既有 agent runtime sidecar 读写原语(临时文件 + rename);非终态且不在本进程 live 集合里的记录在读取时收口为「上次运行中断」失败,不假装它还在跑;账本上限 50 条按创建时间淘汰。
- 决策(阶段文案归后端):`phaseDetail` 由 Rust 拥有(「排队中。」/「正在生成。」/「生成已完成。」/失败原因),前端面板与「生成任务」面板只渲染该字符串,不拼阶段、不做百分比。后端目前没有可播报的中间阶段(生成通道不暴露 job 的远端 `phaseDetail`),所以不伪造「正在处理。」这类前端文案。
- 决策(本地排队):第二条提交停在**前端本地队列**(`dispatched=false`,不调用提交 IPC,阶段显示本地排队的「排队中。」),第一条终态后由同一条循环自动补发。判据是「存在 `dispatched && 未终态` 的任务时不派发下一条」——**本批仍保留单条在途的前端排队**:AGC 本地槽此时已按精确动作指纹分槽(具备并行能力),但真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以这里的排队是本批的**提交节流**,不再是「后端拒绝并发」的对应实现。
- 决策(面板可关 + 非模态任务面板):两块生成浮层在提交期间放开 × / 遮罩 / Esc,提交按钮旁给「后台运行并关闭」;**关闭 ≠ 取消**(表单的 `await` 挂在该任务的终局上,不是面板生命周期)。新增「生成任务」非模态浮层(不铺遮罩、不做焦点陷阱、**不进** `isResourceCanvasFloatingPanelOpen` / `resourceCanvasHostGenerationPanelOpen` 遮挡判据),入口按钮 `aria-label="生成任务"`;已完成的条目按 `assetId` 复用既有 `pendingResourceFocusRef` 聚焦链定位素材卡。
- 影响范围:新增 `apps/ai-game-creator-shell/src-tauri/src/asset_generation_tasks.rs`(+ `main.rs` 注册)、`src/features/resource-canvas/{resourceCanvasAssetGenerationTaskModel.ts,resourceCanvasAssetGenerationQueue.ts,ResourceCanvasAssetGenerationTasksPanelView.tsx}`;改动 `ResourceCanvasAssetGenerationPanelView.tsx` / `ResourceCanvasGenerationPanelView.tsx` / `src/view/project-development/index.tsx`;测试改动 `tests/{resourceCanvasAssetGenerationBackgroundClose.test.tsx,resourceCanvasAssetGenerationQueue.test.ts,resourceCanvasAssetGenerationTasksPanel.test.tsx}`(新增)与 `tests/appSurface/project-development.suite.ts`(把「每个入口一次 `generate_local_project_asset`」改成 `start_local_project_asset_generation` + `list_...` 轮询桩,载荷断言逐字不变)。**未动**:external v1 / OpenAPI、`packages/`、SpacetimeDB、音频入口的 pending-edit 账本语义、生成参数与 IPC 载荷字段名。
- 关联文档:`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`(§4 / §4a / §8)、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S11a / §7.3)。
@@ -1,5 +1,12 @@
# 踩坑与排障记录
## 2026-09-14 严格 IPC 桩缺登记新命令时,症状可能是「unhandled rejection + 不相干的提示断言」,而不是同一处报错
- **现象**:`ProjectDevelopmentView` 新增「项目打开时读生成任务账本」(`list_local_project_asset_generations`)后,两个**别的关注点**的用例同时红:`resourceCanvasManualLayout.test.tsx` 报 `AssertionError: expected [ Array(1) ] to deeply equal []`(严格桩把新命令记进 `unexpectedCommands`),并伴随 7 条 `Unhandled Rejection: TypeError: Cannot read properties of undefined (reading 'map')`;`appSurface/project-development.suite.ts` 的「布局读时提示」用例则因为新命令被当成 unexpected invoke 抛错、触发了新的提示条,导致 `queryBySelector('.game-resource-live-notice')` 断言失败。
- **原因**:这些用例的 `invoke` 桩是**严格白名单**(未登记即抛错或返回 `undefined`)。新命令在挂载期就被调用,于是:① 桩把未登记命令记进 `unexpectedCommands`/抛错;② 生产代码若对返回值无形状防御,就在 `undefined` 上 `.map` 产生 unhandled rejection。**两条失败都指不到真正的新增调用点**,很容易被误判成各自关注点的回归。
- **处理(现行口径)**:① 渲染 `ProjectDevelopmentView` 的桩统一登记 `list_local_project_asset_generations`(返回**数组**,空账本 `[]`;Rust 侧返回 `Vec<AssetGenerationTaskRecord>`,不是 `{ tasks: [] }`);② `unexpectedCommands` 这类门禁**不要放宽**,只登记合法命令;③ 生产代码对 IPC 返回值做形状防御(`Array.isArray` 归一化),IPC 拒绝走既有提示路径,不产生 unhandled rejection(`resourceCanvasAssetGenerationTaskModel.ts` / `resourceCanvasAssetGenerationQueue.ts` / `index.tsx` 的恢复 effect)。
- **易错点**:① 桩返回**非数组**时用例可能"看着绿"但同时报 unhandled rejection(实测:把 `[]` 误写成 `{ tasks: [] }` 就是 8 passed + 7 unhandled error),所以判"绿"必须同时看 unhandled 计数;② 新增挂载期 IPC 后要一次性 grep 所有 `ProjectDevelopmentView` 的桩,而不是等 CI 逐个炸;③ 提示条类断言(如「无读时提示」)会把「桩抛错」翻译成「多了一条提示」,排查时先看 unhandled,再看断言。
- **关联**:`apps/ai-game-creator-shell/tests/resourceCanvasManualLayout.test.tsx`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、`apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel.ts`。
## 2026-09-14 UI 编辑器返回后资源画布滚轮平移失效
- **现象**:资源管理打开 UI 编辑器再返回后,资源画布滚轮平移/缩放不再响应;返回前同一手势正常。
@@ -26,6 +33,14 @@
- **处理**:`runNativeShellGate` 收窄为 `(group, gate)`,label 回到各门禁执行体里以字面量打印;分组能力与 `--groups=` 语义不变。脚本内已注明"不要把 label 抽成变量",外部壳的 `check-config` 是它的消费者。
- **验证**:`node apps/mobile-shell/scripts/check-config.mjs` exit 0;`node apps/desktop-shell/scripts/check-config.mjs` 已越过第 740–777 行的根脚本断言段(本机随后卡在本机不存在的 Tauri 生成产物目录,与本次改动无关);`--groups=contract`、vitest `scripts/project-ci-workflow.test.ts`、eslint 均通过。修复后 run 2097 六个 job 全绿。
- **关联**:`scripts/check-native-shells.mjs`(`runNativeShellGate`)、`apps/desktop-shell/scripts/check-config.mjs`、`apps/mobile-shell/scripts/check-config.mjs`。
## 2026-09-14 AGC 资源画布不要恢复「无条件重派生」,否则新增一张素材就整张重排
- **现象**:用户生成一张新素材后,画布上既有卡片全部移位,刚摆好的位置失效。
- **原因**:`useProjectResourceCanvasLayout` 的 `rederiveAutomaticPositions=true` 会让**每一次资源协调签名变化**(新增 / 删除素材、改标签、改分类、拓扑签名变化)都丢掉全部 `manuallyPlaced=false` 坐标整体重算。签名里必然包含新素材,所以「新增一张素材」就等于「整张画布重排」;两个 hook 当时都开着它(type 侧无条件 `true`,dependency 侧长期等于 `resourceGraphReady`)。
- **处理(现行口径)**:默认一律 `preserve`(只补新卡)。整张重排只由「整理画布」按钮调用 hook 的 `rederiveNow()` 发起,或由「关系图首次就绪」那一次按项目作用域的一次性 flag 发起。**不要**把 `rederiveAutomaticPositions` 改回长期 `true` / `resourceGraphReady`,也不要为「拓扑变了要立刻重排」再加自动触发点——那正是本条要修掉的行为。
- **易错点**:① 一致性判据是 `reconcileResourceCanvasLayout` 输出里**每个分区按 `(y, x, resourceId)` 排序**后的数组与来源逐项比较,所以手工构造 sidecar 夹具时要按同序写,否则会被判成「变了」而多写一次,用例里会看到意料之外的写回;② 关系图首次就绪那一次重算要等该侧 sidecar `ready` 之后再发(关系图可能先就绪),否则这一次会被吃掉;③ 依赖侧的一次性 flag 按**项目作用域**记账,不要挂到 `resourceGraphReady` 这类会随排序 tab 反复翻转的值上,否则每次切回依赖视图都会重排一次;④ 新素材聚焦按 `manifest.assets` 的新增 id 判定,首次打开 / 切项目必须先登记基线,否则一进工作台就跳到最后的卡上;⑤ 不要为了让「整理画布」按钮"一定有反馈"而把 `changed` 门拿掉——重算结果与当前坐标一致时不写盘是既有合同,关系图 `producerMappingTruncated` 时强行写回等于把一份来自不完整关系图的自动布局持久化(`appSurface.test.ts` 的 `keeps trusted truncated-graph depths through the workbench without persisting a flat automatic layout` 会红)。
- **验证**:`npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasManualLayout.test.tsx`(8 条);把两个 hook 配置改回旧口径会红 3 条。
- **关联**:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`。
## 2026-09-14 客户端 CI 拆分后,选组运行会跳过未选分组,且必须同步分支保护
@@ -6,7 +6,7 @@
## 1. 一句话
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类入口走本地 `generate_local_project_asset`,音频入口复用既有无源生成链路,上传复用 `upload_local_asset`;生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类入口走本地 `start_local_project_asset_generation`(提交即返回、后台生成),音频入口复用既有无源生成链路,上传复用 `upload_local_asset`;生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
## 2. 入口矩阵(事实源)
@@ -31,20 +31,26 @@
## 4. 请求载荷映射
| 入口 | IPC | 载荷要点 |
| --------------------- | ------------------------------- | ------------------------------------------------------------------------------------------- |
| 生成图片 | `generate_local_project_asset` | `kind: 'image'`,比例 / 尺寸可调,默认 `1:1 · 1K` |
| 生成角色形象 | 同上 | `kind: 'character'`,默认 `1:1 · 1K` |
| 生成规范 → 图标规范 | 同上 | `kind: 'icon-spec'`,固定 `1:1 · 1K`;缺权威规范图时 `outputPath: 'assets/art-spec.png'` |
| 生成规范 → 角色规范 | 同上 | `kind: 'spec'`,固定 `1:1 · 1K` |
| 生成规范 → 自定义规范 | 同上 | `kind: 'spec'`,固定 `1:1 · 1K` |
| 生成图标素材 | 同上 | `kind: 'art-spritesheet'`,默认 `1:1 · 1K`;前置:已登记 `assets/art-spec.png` |
| 生成 UI 设计图 | 同上 | `kind: 'ui-prototype'`,默认 `16:9 · 1K`;前置同上 |
| 生成背景音乐 | `derive_local_project_resource` | `editKind: 'background-music'`、`generationMode: 'create'`、`sourceMediaType: 'audio/mpeg'` |
| 生成音效 | `derive_local_project_resource` | `editKind: 'sound-effect'`、其余同上 |
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes }` |
| 入口 | IPC | 载荷要点 |
| --------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- |
| 生成图片 | `start_local_project_asset_generation` | `kind: 'image'`,比例 / 尺寸可调,默认 `1:1 · 1K` |
| 生成角色形象 | 同上 | `kind: 'character'`,默认 `1:1 · 1K` |
| 生成规范 → 图标规范 | 同上 | `kind: 'icon-spec'`,固定 `1:1 · 1K`;缺权威规范图时 `outputPath: 'assets/art-spec.png'` |
| 生成规范 → 角色规范 | 同上 | `kind: 'spec'`,固定 `1:1 · 1K` |
| 生成规范 → 自定义规范 | 同上 | `kind: 'spec'`,固定 `1:1 · 1K` |
| 生成图标素材 | 同上 | `kind: 'art-spritesheet'`,默认 `1:1 · 1K`;前置:已登记 `assets/art-spec.png` |
| 生成 UI 设计图 | 同上 | `kind: 'ui-prototype'`,默认 `16:9 · 1K`;前置同上 |
| 生成背景音乐 | `derive_local_project_resource` | `editKind: 'background-music'`、`generationMode: 'create'`、`sourceMediaType: 'audio/mpeg'` |
| 生成音效 | `derive_local_project_resource` | `editKind: 'sound-effect'`、其余同上 |
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes }` |
`generate_local_project_asset` 的完整参数是 `{ projectPath, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(Rust `src-tauri/src/commands.rs` 的 `generate_local_project_asset`,返回 `{ id, localPath, absolutePath, manifestPath }`)。
`start_local_project_asset_generation` 的完整参数是 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(Rust `src-tauri/src/asset_generation_tasks.rs`),**提交即返回**一条任务记录(`taskId / status / phaseDetail / assetId / error` 等);生成在 `tauri::async_runtime::spawn` 出来的后台任务里跑,账本落在项目内 `.agent/runtime/asset-generation-tasks/tasks.json`,进度由 `list_local_project_asset_generations` 读回。截图面板不再「等生成结束」,所以**点「生成」即同步关闭面板**(不等 IPC),面板内不出现阶段文案;只有「点击瞬间就失败」才带草稿重开(关闭 ≠ 取消)。
## 4a. 本地排队与「生成任务」侧栏
- **本地排队**:AGC 本地 durable 输出槽按**精确动作指纹**分槽(`run_id = slot-<sha256(动作材料)>`,材料为 prompt / outputPath / 比例 / 尺寸 / assetKind / assetLabel / replaceExisting / requireSlices;不同 prompt 或素材名各自独立成槽,不再按 `outputPath` 共用;旧 `{outputPath,requireSlices}` 槽账本由同一精确动作懒迁移)。因此后端已具备并行能力,但**本批前端仍按「同一时刻只派发一条」排队**——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批。所以第二条提交停在**前端本地队列**里(不调用提交 IPC,阶段显示本地排队的「排队中。」),第一条终态后由同一条循环自动补发;判据是「存在 `dispatched && 未终态` 的任务时不派发下一条」。
- **阶段文案只来自后端,且只出现在任务侧栏**:派发之后由任务侧栏渲染后端账本给的 `phaseDetail`(「排队中。」/「正在生成。」/「生成已完成。」/失败原因),提交面板已关闭、不渲染任何阶段文案;前端不拼阶段、不做百分比。
- **「生成任务」是常驻画布的可折叠侧栏**(形态对齐网页端美术画布的任务侧栏):折叠时留贴边把手并在把手上显示在途数量;展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界);位置在画布左侧标题栏之下、**覆盖式**(不 reflow 挤窄画布视口)。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、**不参与** `isResourceCanvasFloatingPanelOpen` / `resourceCanvasHostGenerationPanelOpen` 的遮挡判据),提交受理后自动展开;工具栏入口按钮常驻并显示 `生成任务 · N`。每项的「定位到素材」**每次点击都终局化**(含跨栏目先切栏目、不在投影里给结论、3 秒有界兜底),不允许提示条永久停在「正在定位生成的素材…」。
## 5. 参数口径与复用程度
@@ -71,12 +77,18 @@
npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasBottomToolbar.test.tsx
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasGenerationEntry.test.tsx
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationBackgroundClose.test.tsx
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationQueue.test.ts
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksPanel.test.tsx
npx vitest run apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx
npm run agc:typecheck
npm run check:encoding
git diff --check
cargo test -p genarrative-ai-game-creator-shell asset_generation_task
```
- 模型层:四个栏目的工具集与顺序、未命中栏目返回空、「所有资源」与 `null` 返回空、二级菜单分流、比例 / 尺寸白名单收窄、前置判据、不可用原因、`outputPath` 策略。
- 组件层:工具栏渲染与二级菜单开合、不可用入口可点击 + 原因可关闭、上传回调。
- AppSurface 层:进入栏目出现工具栏 / 回总览与展开态消失、每个图片类入口的 `generate_local_project_asset` 载荷、前置缺失可点击说明且零请求、音频入口走 `derive_local_project_resource`、上传走 `upload_local_asset` + 配对读清单、工具栏与右下 Dock 的几何契约。
- 生成任务层:`resourceCanvasAssetGenerationTaskModel`(状态机 / 在途判据 / 恢复 / 耗时文案)、`resourceCanvasAssetGenerationQueue`(第二条不发提交 IPC、前一条终态后自动补发、失败与账本丢失都能收口)、`ResourceCanvasAssetGenerationTasksPanelView`(非模态、阶段文案来自后端记录、按 `assetId` 定位)、两块生成浮层在提交期间可关闭、「后台运行并关闭」文案。
- Rust 层:`asset_generation_tasks` 的账本落项目内、排队阶段文案由后端拥有、进程重启把无人推进的记录收口为中断失败、账本上限与 task id 边界。
- AppSurface 层:进入栏目出现工具栏 / 回总览与展开态消失、每个图片类入口的 `start_local_project_asset_generation` 载荷、前置缺失可点击说明且零请求、音频入口走 `derive_local_project_resource`、上传走 `upload_local_asset` + 配对读清单、生成面板关闭后任务仍在「生成任务」面板且第二条按本地排队补发、工具栏与右下 Dock 的几何契约。
@@ -51,13 +51,13 @@
| **S9** 选择与多选 | 单击卡片 → `Shift`/`Ctrl`/`Meta` 再点 → 空白处拖拽框选 | 点卡 = 选中并在卡片旁浮出工具条;三种修饰键等效追加;框选可见选框 | L51、L61–62、L318–319 | `data-resource-card-id` + class 含 `is-selected` + `aria-pressed="true"`;浮出容器 `role="toolbar"`(图片 `aria-label="图片工具栏"`,音频 `"素材工具栏"`) | ⚠️ **PRD L61–62 要求的「打开详情」按钮在代码中不存在**(测试显式断言为 `null`),按 Issue #309 C3「取消资源详情面板与全屏编辑路由」判,不要判为缺失;真正入口是 `aria-label="选中资源:<栏目> <名>"` 按钮。**只读信息浮层已回归**(2026-09-11,工具条「信息」动作,见 `decision-log`):**C3 取消的仍只是可编辑详情面板与全屏编辑路由** |
| **S10** 快速编辑(图片) | 选中 PNG/JPEG/WEBP 卡 → 工具条「快速编辑」→ 输入 → 提交(提示词下方另有「AI 润色」) | 先 `normalize_local_project_raster_resource`,再 `derive_local_project_resource(editKind='image-reference')` → 服务端 `/api/editor/images/edits`;**产出一张新素材**,源素材与源文件不变,新资源带 `referenceResourceIds` 血缘 | L123、L193、L476–478、L557;#309 C3/C10 | 提交后 `assets[]` +1、卡片 +1 且 `data-resource-card-id` 为新 id;**源卡仍在且内容未变**;工具条真渲染的只有 `quick-edit` 与 `download`;「AI 润色」`aria-label="AI 润色"` + `aria-busy`,成功回填并出现「恢复原文」,失败保留原文并给「AI 润色失败,可重试」 | ① 服务端白名单 17 项(`editor_project.rs:4120-4141`),`character-animation`/`video`/`audio`/`document`/`code` 等**仍会 400**;② 工具条 7 个动作(重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画)按 opt-in **不渲染**;③ 润色结果超 `resourceEditPromptMaxLength` 会**截断并提示「已按长度上限截断」**;④ 提示词变了(改写或润色回填)必须换 `operationId`/幂等键,否则 Rust `request_fingerprint` 判「已绑定到不同资源编辑请求」 |
| **S11** 画布生成入口(视频) | 画布级入口 → 只呈现视频 → 填名与提示词 → 提交(提示词下方另有「AI 润色」) | 无源新建视频 → 新卡产出并定位(音效 / 背景音乐已移到栏目工具栏,见 S11a) | 飞书 §4(首轮由对话启动)、L122 | 提交后 `assets[]` +1;面板是独立浮层(非在当前面板下方追加);「AI 润色」走 `polish_local_project_prompt` 并带上「这是素材生成提示词(类型)」的场景约束,成功回填、失败保留原文 | ⚠️ **Issue #309 写「画布生成入口未接」,但当前代码已接三类**(`resourceCanvasGenerationModel.ts` 仅 `video`/`sound-effect`/`background-music`);图片等类型会直接报「当前资源类型不支持无源生成」;润色结果超该类型上限(背景音乐 140 / 音效 1900 / 视频 4000)会截断并提示「已按长度上限截断」 |
| **S12** 编辑标签 | 选中已登记素材 → 工具条「编辑标签」→ 增删标签 → 保存 | 面板**只编辑 `manifest.assets[].tags`**;`category` 读一次权威值后**原样回传**;顶部没有分类 chip 排 | L360(2026-09-11 收口)、#309 C2 | 面板标题「编辑素材标签」,底部只有「取消 / 保存标签」;保存后 `assets[n].tags` 变化而 `assets[n].category` **逐字不变** | 面板内的「删除资源」按钮是**唯一**删除入口,保留;「用户手动设置 category」这项能力已移除 |
| **S12** 编辑标签 | 选中已登记素材 → 工具条「编辑标签」→ 增删标签 → 保存 | 面板**只编辑 `manifest.assets[].tags`**;`category` 读一次权威值后**原样回传**;顶部没有分类 chip 排 | L360(2026-09-11 收口)、#309 C2 | 面板标题「编辑素材标签」,底部只有「取消 / 保存标签」;保存后 `assets[n].tags` 变化而 `assets[n].category` **逐字不变** | 面板内的「删除资源」按钮保留(删除入口另见 S15 的选中卡工具条);「用户手动设置 category」不再放在本面板,改为独立入口(工具条「素材类型」与信息浮层「分类 → 设置」,选中即落盘,见 decision-log 2026-09-14) |
| **S13** 重命名 | 工具条「重命名」→ 新文件名 → 确认 | 磁盘文件名与 manifest `localPath` 更新;**asset id 不变**;写盘失败回滚文件 | L362(资源身份稳定)、L515 | 弹窗 `ariaLabel="重命名素材"`;改名后「选中资源:…」按钮的无障碍名同步变化;引用 chip 与 @ 候选列表显示名刷新 | ⚠️ **不改游戏源码里对旧 `assets/<name>` 的引用**,后果由用户自负 |
| **S14** 下载 | 选中可下载卡 → 工具条「下载」 | 弹出**原生保存对话框**选目标路径 → 分块复制;取消即停止 | L507、飞书 §11.1.2 | 出现 `title="保存素材"` 的原生对话框;成功后状态提示含 `已保存 <路径>` | 语义是"另存为",不是静默直下;虚拟版本条目没有文件、不放行 |
| **S15** 删除 | 工具条 / 标签面板「删除」→ 确认弹窗 | 三分支:① 无引用→直接删;② 被引用未勾选→只删素材、版本保留**悬空绑定**、界面不合成幽灵资源卡;③ 勾选→素材与该批版本在**同一次 manifest 写入**内一起删 | L413(版本只追加的唯一例外)、L532–533;#309 C5 | 弹窗 `ariaLabel="确认删除资源"`;被引用时出现 `被 N 个游戏版本使用` + 版本列表 + checkbox `把相关游戏版本一并删除`;无引用时该 body 整块不渲染 | **只摘登记、不删磁盘文件**;读引用命令精确名是 `read_local_project_asset_references` |
| **S15a** 替换素材(含点选替换) | 选中被**当前版本**绑定的素材 → 工具条「替换素材」→ 候选弹窗(弹窗内可筛可选);再点 footer「点选替换」→ 弹窗关闭、进入画布点选态 → 在画布上直接点目标素材 | 弹窗确认与点选是**同一条**写入链路(`replace_local_project_version_resource`,载荷逐字一致):改该版本绑定、不建新版本;点选态下合法目标直接提交并自动退出点选态,非法目标零写入、**留在**点选态并在提示条上说明原因 | PRD §5.3 / §7.8 第 8 条;#309「直接替换」口径 | 点选态判据:候选弹窗 `role="dialog"` 已卸载;画布出现 `.game-resource-canvas-pick-hint`,文案含「在画布上点选要替换成的素材」与「点击空白处不会退出」,并有「取消」按钮;非法目标后提示条仍在且出现 `role="alert"`(「替换素材不兼容:分类不同」/「替换素材与源素材相同」/「替换素材未登记或已被删除」);点选期间卡片 `aria-pressed` 不变(单击只用于点选)、源素材选中与工具条不因点空白或点非法目标而丢;Esc 退出点选(画布全局 Esc 的清选中不随之触发);点选态下滚轮平移与缩放照常可用 | 点选只能点**当前画布上可见**的卡:目标在别的栏目时先切栏目(点选会话跨栏目存活,不因「收起资源」或切栏目结束);空白点击不退出、也不清画布选中 |
| **S11a** 栏目画布底部工具栏 | 进「UI 交互 / 角色与对象 / 场景与环境 / 音频」任一栏目 → 点左下角工具栏里的入口生成 → 「收起资源」回总览看工具栏消失 | 工具栏只在矩阵四个栏目(功能画布)渲染;图片类入口走 `generate_local_project_asset`,音频入口复用既有无源生成链路,上传复用 `upload_local_asset`;生成 / 上传成功后走既有 manifest 刷新与资源定位 | PRD §3.10、§7.9 | DOM 判据 `[data-resource-bottom-toolbar="<category>"]`(资源总览、「所有资源」展开态、文档、待归类、项目版本都**没有**这个节点);工具栏是 `.game-resource-book-manager` 的直接子节点(`closest('.game-resource-book-scene')` 为 `null`);每个入口一次 `generate_local_project_asset`,载荷逐字为 `{ projectPath, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(`kind` 映射见 PRD §7.9 第 3 条);写入命令之后有 `get_local_game_project_revision` + `get_local_game_manifest` 的配对读;缺 `assets/art-spec.png` 时「生成图标素材 / 生成 UI 设计图」仍可点击(`aria-disabled="true"` 但**不是**原生 disabled)并给出含该路径的 `role="alert"` 原因、零生成请求;音频入口面板标题即「生成背景音乐」/「生成音效」且没有类型选择器 | ① 本地通道没有 `model` / `specType` / `replaceExisting` 入参:面板不渲染模型选择器,角色规范与自定义规范共用 `spec` 通道(靠 `assetName` 与提示词区分),「图标规范」在项目已有权威规范图时不再指向 `assets/art-spec.png`(否则会被 Rust 的防覆盖校验硬拒);② 本轮不做生成视频 / 宣发素材 / 生成游戏场景 / 选择工具 / 抓手工具;③ 既有「生成素材」浮层入口只保留生成视频,音频入口只在音频栏目工具栏出现 |
| **S11a** 栏目画布底部工具栏 | 进「UI 交互 / 角色与对象 / 场景与环境 / 音频」任一栏目 → 点左下角工具栏里的入口生成 → 「收起资源」回总览看工具栏消失 | 工具栏只在矩阵四个栏目(功能画布)渲染;图片类入口走 `start_local_project_asset_generation`(提交即返回、生成在后台跑),音频入口复用既有无源生成链路,上传复用 `upload_local_asset`;生成 / 上传成功后走既有 manifest 刷新与资源定位 | PRD §3.10、§7.9 | DOM 判据 `[data-resource-bottom-toolbar="<category>"]`(资源总览、「所有资源」展开态、文档、待归类、项目版本都**没有**这个节点);工具栏是 `.game-resource-book-manager` 的直接子节点(`closest('.game-resource-book-scene')` 为 `null`);每个入口一次 `start_local_project_asset_generation`,载荷逐字为 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(`kind` 映射见 PRD §7.9 第 3 条;`taskId` 是前端每次提交新铸的本地任务 id);该命令提交即返回,之后有 `list_local_project_asset_generations` 轮询与 `get_local_game_project_revision` + `get_local_game_manifest` 的配对读;提交期间点 × / 遮罩 / Esc /「后台运行并关闭」任一都能关面板且请求继续(画布立即恢复可交互),关闭后任务仍出现在「生成任务」面板(入口按钮 `aria-label="生成任务"`,非模态浮层、无 `aria-modal`)并显示后端 `phaseDetail`;第一条未终态时提交第二条 → 第二条显示「排队中。」且生成提交 IPC 次数仍为 1,第一条终态后自动补发(次数变 2);缺 `assets/art-spec.png` 时「生成图标素材 / 生成 UI 设计图」仍可点击(`aria-disabled="true"` 但**不是**原生 disabled)并给出含该路径的 `role="alert"` 原因、零生成请求;音频入口面板标题即「生成背景音乐」/「生成音效」且没有类型选择器 | ① 本地通道没有 `model` / `specType` / `replaceExisting` 入参:面板不渲染模型选择器,角色规范与自定义规范共用 `spec` 通道(靠 `assetName` 与提示词区分),「图标规范」在项目已有权威规范图时不再指向 `assets/art-spec.png`(否则会被 Rust 的防覆盖校验硬拒);② 本轮不做生成视频 / 宣发素材 / 生成游戏场景 / 选择工具 / 抓手工具;③ 既有「生成素材」浮层入口只保留生成视频,音频入口只在音频栏目工具栏出现 |
### D 阶段 · 版本与运行
@@ -101,6 +101,8 @@
先记住状态枚举:`data-preview-kind` ∈ `raster-image | media-image | video | audio | code | document | version | placeholder`;`data-preview-status` **只有 `idle | loading | loaded | failed` 四个值,不存在 `unsupported`**。"不适用"不是状态,表现为**永远 idle**。
棋盘格底另有一条独立判据:`data-preview-has-alpha` **只在预览已加载且这张图真的带 alpha 通道时才写 `'true'`**(原生头部判据:PNG colorType 4/6 或 `tRNS`、WebP alpha 标志;JPEG 恒不透明)。缺属性与不透明同档,卡面退回纯色底。因此「真透明底」与「AI 把棋盘格画进像素里」不再同形:**看 `data-preview-has-alpha` 而不是看卡面有没有棋盘格**。
```js
// ① 全量分布(首选)
[...document.querySelectorAll('.game-resource-card[data-preview-kind]')]
@@ -120,6 +122,11 @@
[...document.querySelectorAll('.game-resource-card[data-preview-status="loaded"]')]
.filter(e => !e.querySelector('.game-resource-card-visual img, .game-resource-card-visual video'))
.map(e => [e.dataset.resourceCardId, e.dataset.previewKind])
// ⑤ 真假透明底:已加载的图片卡按 alpha 分组
// 真透明(true)才应有棋盘格底;AI 画了棋盘格的不透明图这里必须是 undefined
[...document.querySelectorAll('.game-resource-card[data-preview-status="loaded"][data-preview-kind="raster-image"], .game-resource-card[data-preview-status="loaded"][data-preview-kind="media-image"]')]
.reduce((m, e) => { const k = e.dataset.previewHasAlpha ?? '(无 alpha 判据)'; m[k] = (m[k] || 0) + 1; return m; }, {})
```
| 现象 | 说明 | 第一眼做什么 |
@@ -128,6 +135,7 @@
| `failed` + `data-preview-error` | 读取或解码真失败,读 `data-preview-error` 原文 | `retryable=false` 表示永久失败(超尺寸、损坏、类型不支持、不安全 SVG);可见性预取遇 failed 会直接放弃,不会自愈 |
| `loaded` 但无主体节点 | 原生返回的 dataUrl 为空、或解码不出画面 | 这类**不报错**,只能靠脚本 ④ 抓 |
| `placeholder` + `idle` | **不适用**(游戏代码、无法识别的二进制产物等),正常 | 不要当 bug 报 |
| 卡面有棋盘格但 `data-preview-has-alpha` 缺失 | **这张图其实不透明**:棋盘格是图里画进去的(旧构建会额外再铺一层卡面棋盘格,两者叠在一起) | 脚本 ⑤ 分组确认;真透明卡才允许有 `'true'` |
**IPC 侧**:预览只走 4 条命令 `read_local_project_image_preview`、`read_local_project_media_preview`、`read_local_project_text_preview`、`cancel_local_project_resource_preview_scope`。媒体那条的 `category` **只接受 `art` / `audio`**,传栏目值会被原生拒绝,表现是卡片全空、没有缩略图。
@@ -238,7 +246,7 @@ api-server 是否本次重启:□ 是 □ 否
### 7.3 已知未做 / 已取消(不要报成缺陷)
1. **C6 的候选素材 / 替换关系 / 替换队列 / Agent 审核 / 批量提交整条取消**(#309)。PRD §5.3 的 `ProjectVersionResourceReplacement` 与 §3.2 的「创建下一迭代版本」**当前不实现**(2026-09-11 用户 DDL 口径「替换这块先做成直接替换」),但**保留为未来合同**:今天的实现是「改该版本的绑定,不建新版本」,验收见 PRD §7.8。
2. **画布生成入口分为两条**(2026-09-13 起):既有「生成素材」浮层入口只保留视频;音频(背景音乐 / 音效)与图片类(生成图片 / 生成规范 / 生成角色形象 / 生成图标素材 / 生成 UI 设计图)改由栏目画布左下角的底部工具栏承载,走本地 `generate_local_project_asset`。工具栏之外的其它无源生成类型仍会直接报"当前资源类型不支持无源生成",这是设计而非缺陷。见 PRD §3.10 / §7.9 与下面的 S11a。
2. **画布生成入口分为两条**(2026-09-13 起):既有「生成素材」浮层入口只保留视频;音频(背景音乐 / 音效)与图片类(生成图片 / 生成规范 / 生成角色形象 / 生成图标素材 / 生成 UI 设计图)改由栏目画布左下角的底部工具栏承载,走本地 `start_local_project_asset_generation`(提交即返回、生成在后台跑完写回项目,任务账本在项目内 `.agent/runtime/asset-generation-tasks/`)。工具栏之外的其它无源生成类型仍会直接报"当前资源类型不支持无源生成",这是设计而非缺陷。见 PRD §3.10 / §7.9 与下面的 S11a。
3. **工具条 7 个动作按 opt-in 不渲染**:重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画;只有 `快速编辑` 与 `下载` 真渲染(宿主另附加 `UI 编辑器` / `编辑标签` / `重命名`,以及**只在素材被当前版本绑定时**出现的 `替换素材`)。
4. **C7 只做记录层 + UI 层**:切换版本即重载当前预览;版本化资源解析机制未实现。资源替换同样不做运行时资源重映射:改绑定只改「这个版本用哪些素材」的记录,运行画面要按游戏自身引用的资源路径渲染。
5. **「首轮进度投影」无编码级判据**,可见性由底部 Agent 状态栏承载。