修复 AGC 前端资源 kind 判据与用例的 canonical 残留
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m1s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m6s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m14s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m52s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m50s
Project CI / Repository checks (pull_request) Successful in 4m44s
Project CI / Frontend tests (pull_request) Successful in 6m20s
Project CI / Backend tests (pull_request) Successful in 8m27s
Project CI / Native shell tests (pull_request) Successful in 8m15s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m10s

- resource-canvas/resourceCanvasAssetGenerationReferenceModel.ts:「图集不接受用户参考」判据改用 canonical icon-spritesheet,删除残留平台词的字符串清单。
- view/project-development/index.tsx:美术摘要的图片产出判据显式排除 ui-design,候选 UI 原型图不再顶掉「仅完成计划」。
- tests/appSurface/project-development.suite.ts:generateCall 支持按素材名定位,规范 / 图集 / UI 设计图入口断言改用 canonical kind。
- tests/appSurface/project-assets.suite.ts:/asset-register 与开发面板用例改用 canonical kind,面板默认值按 unknown 断言。
- tests/resourceCanvasAssetGenerationReferences.test.tsx:夹具与提交断言改用 icon-spritesheet / ui-design。
- docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md:载荷表与规范通道说明改用 canonical kind,并写明平台请求词汇只留在 source.generationKind。
- docs/project-memory/shared-memory/pitfalls.md:记录前端 kind 判据与 fixture 必须一起按 canonical 成员重写。
This commit is contained in:
2026-09-19 03:21:37 +08:00
parent 603650fbf4
commit 47f7cb2bf1
7 changed files with 72 additions and 45 deletions
@@ -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';
}
/**
@@ -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'),
@@ -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: '',
@@ -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,
@@ -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: [],
});
});
@@ -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` 不为空)。
@@ -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-<sha256(动作材料)>`,材料为 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. 音频入口收敛