diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationReferenceModel.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationReferenceModel.ts index 2f69dd74b..f491cb308 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationReferenceModel.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationReferenceModel.ts @@ -23,21 +23,17 @@ export const RESOURCE_CANVAS_ASSET_GENERATION_MAX_USER_REFERENCES_WITH_SPEC = 4; /** * 不接受用户参考的生成类型。 * - * 目前只有 `art-spritesheet`(图集)按合同只接受单张规范引用:这类入口不呈现用户参考选择器, - * 原生提交也会显式拒绝额外参考(不是静默丢弃)。 + * 目前只有 `icon-spritesheet`(图集)按合同只接受单张规范引用:这类入口不呈现用户参考选择器, + * 原生提交也会显式拒绝额外参考(不是静默丢弃)。判据直接用 manifest 的 canonical kind, + * 与 Rust 侧 `platform_art_asset_kind_accepts_user_reference_assets` 同一个词。 * * 其它入口一律支持用户参考,包括 `icon-spec`(图标规范):它虽然产出权威规范图,但生成时同样 * 可以带参考图,上限与普通生成一致。 */ -const RESOURCE_CANVAS_ASSET_GENERATION_REFERENCE_FREE_KINDS: readonly string[] = - ['art-spritesheet']; - export function resourceCanvasAssetGenerationAcceptsReferences( action: ResourceCanvasAssetToolAction, ): boolean { - return !RESOURCE_CANVAS_ASSET_GENERATION_REFERENCE_FREE_KINDS.includes( - action.assetKind, - ); + return action.assetKind !== 'icon-spritesheet'; } /** diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 348565f02..4d58e5818 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -3938,10 +3938,17 @@ export default function ProjectDevelopmentView({ resourceRenameAsset, ], ); + /** + * 「美术 Agent 是否已产出图片」= 图片类资产里排除待视觉验收的 UI 原型图。 + * + * `ui-design` 是候选界面图(验收通过才成为正式界面图),不是美术图片产出;把它算进来会 + * 让「美术资源计划已完成,尚未生成或登记图片」被一张原型图顶掉。 + */ const hasRegisteredArtImageAssets = manifest.assets.some( (asset) => asset.mediaType.startsWith('image/') && - isGameCreationAppAssetVisualKind(asset.kind), + isGameCreationAppAssetVisualKind(asset.kind) && + asset.kind !== GAME_CREATION_APP_UI_DESIGN_ASSET_KIND, ); const allAgentSummaries = [ summarizeAgent(manifest, 'design', '设计实现 Agent'), diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-assets.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-assets.suite.ts index 33700033d..845617298 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-assets.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-assets.suite.ts @@ -396,14 +396,14 @@ export function registerProjectAssetTests() { ).not.toBeNull(); invoke.mockClear(); - await submitChat('/asset-register assets/hero.png sprite image/png'); + await submitChat('/asset-register assets/hero.png image image/png'); expect( await screen.findByText('asset.register · assets/hero.png'), ).not.toBeNull(); expect( screen.getByText( - '登记 /tmp/authorized-game/assets/hero.png · sprite · image/png', + '登记 /tmp/authorized-game/assets/hero.png · image · image/png', ), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( @@ -419,7 +419,7 @@ export function registerProjectAssetTests() { expect(invoke).toHaveBeenCalledWith('register_local_asset', { projectPath: '/tmp/authorized-game', localPath: 'assets/hero.png', - kind: 'sprite', + kind: 'image', mediaType: 'image/png', sourceKind: 'generated', canvasProjectId: '', @@ -547,7 +547,7 @@ export function registerProjectAssetTests() { ).not.toBeNull(); invoke.mockClear(); - await submitChat('/asset-register assets/hero.png sprite image/png'); + await submitChat('/asset-register assets/hero.png image image/png'); expect( await screen.findByText('准备登记项目资产:assets/hero.png'), @@ -571,7 +571,7 @@ export function registerProjectAssetTests() { expect(invoke).toHaveBeenCalledWith('register_local_asset', { projectPath: '/tmp/authorized-game', localPath: 'assets/hero.png', - kind: 'sprite', + kind: 'image', mediaType: 'image/png', sourceKind: 'generated', canvasProjectId: '', @@ -675,7 +675,9 @@ export function registerProjectAssetTests() { expect(invoke).toHaveBeenCalledWith('register_local_asset', { projectPath: '/tmp/genarrative-ai-game-draft', localPath: 'assets/hero.png', - kind: 'asset', + // 面板默认「资产类型」是 `unknown`(待归类):写入口径允许先落 Unknown, + // 由后续归类决定怎么处理,不在这里编一个非 canonical 的别名。 + kind: 'unknown', mediaType: 'application/octet-stream', sourceKind: 'generated', canvasProjectId: '', diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 8545a0290..46df34801 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -2,6 +2,7 @@ import userEvent from '@testing-library/user-event'; import { createRef } from 'react'; import type { + GameCreationAppAssetKind, ProjectResourceCanvasLayout, ProjectResourceCanvasPosition, } from '../../../../packages/shared/src/contracts/gameCreationApp'; @@ -370,16 +371,27 @@ async function submitBottomToolbarPanel( * * 后台化之后「提交」这一步是 `start_local_project_asset_generation`(入参校验 + 落项目内账本), * 所以载荷断言看的是它;生成本身由 Rust 后台任务跑。 + * + * 三条规范类入口共用同一个 canonical kind(本地 IPC 只有一条规范通道),kind 不足以定位, + * 所以再按 `assetName` 挑一次,避免取到同 kind 的另一条调用。 */ -function generateCall(calls: readonly BottomToolbarInvokeCall[], kind: string) { +function generateCall( + calls: readonly BottomToolbarInvokeCall[], + kind: GameCreationAppAssetKind, + assetName?: string, +) { const call = calls.find( (entry) => entry.command === 'start_local_project_asset_generation' && - invokeStringField(entry.args, 'kind') === kind, + invokeStringField(entry.args, 'kind') === kind && + (assetName === undefined || + invokeStringField(entry.args, 'assetName') === assetName), ); if (!call?.args) { throw new Error( - `missing start_local_project_asset_generation call for ${kind}`, + `missing start_local_project_asset_generation call for ${ + assetName ?? kind + }`, ); } return call.args; @@ -12101,7 +12113,7 @@ export function registerProjectAgentStatusTests() { menu: '生成规范', prompt: '像素月光厨房统一视觉规范', }); - expect(generateCall(calls, 'icon-spec')).toMatchObject({ + expect(generateCall(calls, 'icon-spec', '图标规范')).toMatchObject({ kind: 'icon-spec', prompt: '像素月光厨房统一视觉规范', aspectRatio: '1:1', @@ -12110,13 +12122,14 @@ export function registerProjectAgentStatusTests() { outputPath: 'assets/art-spec.png', }); - // 3) 生成规范 → 自定义规范:规格档固定展示 1:1·1K,不带比例选择器。 + // 3) 生成规范 → 自定义规范:规格档固定展示 1:1·1K,不带比例选择器;与上面那条 + // 共用 `icon-spec` 通道,只有素材名能区分。 await submitBottomToolbarPanel('自定义规范', { menu: '生成规范', prompt: '场景与道具的统一色彩规范', }); - expect(generateCall(calls, 'spec')).toMatchObject({ - kind: 'spec', + expect(generateCall(calls, 'icon-spec', '自定义规范')).toMatchObject({ + kind: 'icon-spec', prompt: '场景与道具的统一色彩规范', aspectRatio: '1:1', imageSize: '1K', @@ -12191,7 +12204,7 @@ export function registerProjectAgentStatusTests() { await submitBottomToolbarPanel('生成图标素材', { prompt: '各种敌人头像:骷髅 哥布林 强盗', }); - expect(generateCall(calls, 'art-spritesheet')).toMatchObject({ + expect(generateCall(calls, 'icon-spritesheet')).toMatchObject({ kind: 'icon-spritesheet', prompt: '各种敌人头像:骷髅 哥布林 强盗', aspectRatio: '1:1', @@ -12204,7 +12217,7 @@ export function registerProjectAgentStatusTests() { await submitBottomToolbarPanel('生成 UI 设计图', { prompt: '横屏单屏界面,含 HUD 与操作控件', }); - expect(generateCall(calls, 'ui-prototype')).toMatchObject({ + expect(generateCall(calls, 'ui-design')).toMatchObject({ kind: 'ui-design', prompt: '横屏单屏界面,含 HUD 与操作控件', aspectRatio: '16:9', @@ -12219,7 +12232,7 @@ export function registerProjectAgentStatusTests() { menu: '生成规范', prompt: '第二版图标规范', }); - expect(generateCall(calls, 'icon-spec')).toMatchObject({ + expect(generateCall(calls, 'icon-spec', '图标规范')).toMatchObject({ kind: 'icon-spec', assetName: '图标规范', outputPath: null, @@ -12241,10 +12254,10 @@ export function registerProjectAgentStatusTests() { menu: '生成规范', prompt: '三角色头身比与服装规范', }); - // 本地 IPC 只有一条规范通道(spec),没有 specType 入参:角色规范与自定义规范共用它, - // 靠素材名与提示词区分。 - expect(generateCall(calls, 'spec')).toMatchObject({ - kind: 'spec', + // 本地 IPC 只有一条规范通道(`icon-spec`),没有 specType 入参:角色规范与自定义规范 + // 共用它,靠素材名与提示词区分。 + expect(generateCall(calls, 'icon-spec', '角色规范')).toMatchObject({ + kind: 'icon-spec', prompt: '三角色头身比与服装规范', assetName: '角色规范', outputPath: null, diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationReferences.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationReferences.test.tsx index 0e4095ae2..08fb245ee 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationReferences.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationReferences.test.tsx @@ -62,13 +62,13 @@ const iconSpecAction = action({ const uiPrototypeAction = action({ id: 'generate-ui-prototype', label: '生成 UI 设计图', - assetKind: 'ui-prototype', + assetKind: 'ui-design', requiresIconSpecReference: true, }); const spritesheetAction = action({ id: 'generate-icon-spritesheet', label: '生成图标素材', - assetKind: 'art-spritesheet', + assetKind: 'icon-spritesheet', requiresIconSpecReference: true, }); @@ -295,7 +295,7 @@ describe('生成面板的参考接线', () => { ); expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit.mock.calls[0]?.[0]).toMatchObject({ - kind: 'art-spritesheet', + kind: 'icon-spritesheet', references: [], }); }); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 3ef639d60..4219729f1 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,12 @@ # 踩坑与排障记录 +## 2026-09-19 资源 kind 词汇收敛后,前端判据与 fixture 必须一起按 canonical 成员重写 + +- **现象**:工具栏入口的 `assetKind` 换成共享 `GameCreationAppAssetKind`(图集从平台词 `art-spritesheet` 改成 `icon-spritesheet`)后,「图集不接受用户参考」的判据仍写在旧的 `['art-spritesheet']` 字符串清单里,判据恒假:生成面板重新给图集渲染参考图选择器,原生提交再按合同显式拒绝多余参考。 +- **同类第二处**:把 `hasRegisteredArtImageAssets` 从 `kind === 'art-spritesheet'` 直接换成「kind ∈ 视觉族」会把候选 UI 原型图(`ui-design`)算成美术图片产出,提前顶掉「美术资源计划已完成,尚未生成或登记图片」;判据必须显式排除 `GAME_CREATION_APP_UI_DESIGN_ASSET_KIND`。 +- **处理(现行口径)**:前端的资源 kind 判据只比较 canonical 成员或共享契约导出的谓词,不再维护第二份字符串清单;词汇收敛时先 grep 旧词在 `src/` 与 `tests/` 两侧的落点,fixture 同步按 canonical 成员重写。 +- **易错点**:fixture 与生产代码写着同一个旧词时,单测会陪着一起变绿,用例证明不了任何事;另外 `tests/appSurface.test.ts` 不是全部同族用例,资源画布 / 画布生成参考等用例散在 `tests/*.test.ts(x)`,词汇收敛必须连这些一起跑。 + ## 2026-09-17 ts-rs 生成物换目录后,忘记同步忽略规则会让「生成物抖动」假装成代码改动 - **现象**:`GameCreationAppAssetKind` 的 ts-rs `export_to` 从 `apps/ai-game-creator-shell/src/contracts/generated/` 换到 `packages/shared/src/contracts/generated/` 后,任何 `cargo build` / `cargo test` 都会重写生成文件;若新目录没进 `.prettierignore` 与 `.eslintrc.cjs` 的 `ignorePatterns`,lint-staged / prettier 会把生成物重新格式化,于是每次提交都出现「生成物被改」,`cargo test export_bindings` 也不再幂等(跑完 `git diff` 不为空)。 diff --git a/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md b/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md index 9314085b3..213442cba 100644 --- a/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md +++ b/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md @@ -31,21 +31,23 @@ ## 4. 请求载荷映射 -| 入口 | 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 }` | +| 入口 | 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: 'icon-spec'`,固定 `1:1 · 1K`;与图标规范 / 自定义规范共用同一条规范通道 | +| 生成规范 → 自定义规范 | 同上 | `kind: 'icon-spec'`,固定 `1:1 · 1K` | +| 生成图标素材 | 同上 | `kind: 'icon-spritesheet'`,默认 `1:1 · 1K`;前置:已登记 `assets/art-spec.png` | +| 生成 UI 设计图 | 同上 | `kind: 'ui-design'`,默认 `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 }` | `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),面板内不出现阶段文案;只有「点击瞬间就失败」才带草稿重开(关闭 ≠ 取消)。 +入参 `kind` 一律是共享 `GameCreationAppAssetKind` 的 canonical 成员(`image` / `character` / `icon-spec` / `icon-spritesheet` / `ui-design` / `publication-material`);`spec` / `art-spritesheet` / `ui-prototype` 这类平台请求词汇只由 Rust 侧派生到 `source.generationKind`,不再出现在 IPC 载荷或 manifest kind 里。 + ## 4a. 本地排队与「生成任务」侧栏 - **本地排队**:AGC 本地 durable 输出槽按**精确动作指纹**分槽(`run_id = slot-`,材料为 prompt / outputPath / 比例 / 尺寸 / assetKind / assetLabel / replaceExisting / requireSlices;不同 prompt 或素材名各自独立成槽,不再按 `outputPath` 共用;旧 `{outputPath,requireSlices}` 槽账本由同一精确动作懒迁移)。因此后端已具备并行能力,但**本批前端仍按「同一时刻只派发一条」排队**——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批。所以第二条提交停在**前端本地队列**里(不调用提交 IPC,阶段显示本地排队的「排队中。」),第一条终态后由同一条循环自动补发;判据是「存在 `dispatched && 未终态` 的任务时不派发下一条」。 @@ -66,7 +68,7 @@ - 判据与 Rust `canonical_art_spec_reference_at` 逐字对齐(`projectHasIconSpecReference`):manifest 里必须存在 `localPath === 'assets/art-spec.png'`、`kind === 'icon-spec'`、`mediaType` 以 `image/` 开头、`source.kind === 'canvas'` 的资产。 - 缺前置时入口**保持可点击**(`aria-disabled="true"`,不是原生 `disabled`),点击后在工具栏上沿给 `role="alert"` 的原因说明:`需要先完成并登记图标规范(assets/art-spec.png);请先用「生成规范 → 图标规范」生成`;此时**不发任何生成请求**。 - 「图标规范」入口在缺前置时把 `outputPath` 指向 `assets/art-spec.png`,让前置条件能由工具栏自身满足;已有权威规范图时 `outputPath` 为 `null`。这条策略对应 Rust 侧硬约束:`prepare_local_project_asset_generation` 的 `replace_existing` 固定为 `false`,指向已存在文件会被拒绝(`图片生成 outputPath 已存在,禁止静默覆盖`)。 -- 已知能力缺口(本轮不扩接口,记账备查):本地 IPC 没有 ① `model` 入参(面板因此不给模型选择)、② `specType` 入参(角色规范与自定义规范共用 `spec` 通道,靠素材名与提示词区分)、③ `replaceExisting` 入参(因此不能重写已存在的权威规范图)。 +- 已知能力缺口(本轮不扩接口,记账备查):本地 IPC 没有 ① `model` 入参(面板因此不给模型选择)、② `specType` 入参(角色规范与自定义规范共用 `icon-spec` 通道,靠素材名与提示词区分)、③ `replaceExisting` 入参(因此不能重写已存在的权威规范图)。 ## 7. 音频入口收敛