From f9bc2d917c3bcf5b0e29c03a4823d01090b2de27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 16 Sep 2026 19:07:58 +0800 Subject: [PATCH] =?UTF-8?q?AGC=20=E8=B5=84=E6=BA=90=20kind=20=E9=9B=86?= =?UTF-8?q?=E5=90=88=E6=94=B9=E4=B8=BA=E4=BB=8E=E7=94=9F=E6=88=90=20union?= =?UTF-8?q?=20=E7=A9=B7=E4=B8=BE=E6=B4=BE=E7=94=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assetKind.ts:新增 `GAME_CREATION_APP_ASSET_KIND_MEMBERS: Record`,生成 union 增删成员时会直接编译报错 - assetKind.ts:`GAME_CREATION_APP_ASSET_KINDS` 由该表派生,不再手写第二份字面量列表 - assetKind.ts:新增 `isGameCreationAppAssetKind` 类型守卫,`parseGameCreationAppAssetKind` 复用它 - view/project-development/index.tsx:资源引用的 `sourceSubtype` 只传正式 kind 成员,任务产物等非 manifest 来源传 `null` - tests/assetKindCanonicalMapping.test.ts:两侧 kind 目录比对前先展开 Rust 常量引用(`ui-design-doc` 是常量),修正此前恒失败的两条;删除测试体里游离的 `'font',` 表达式 --- .../src/contracts/assetKind.ts | 64 ++++++++++++------- .../src/view/project-development/index.tsx | 11 +++- .../tests/assetKindCanonicalMapping.test.ts | 40 +++++++++--- 3 files changed, 82 insertions(+), 33 deletions(-) diff --git a/apps/ai-game-creator-shell/src/contracts/assetKind.ts b/apps/ai-game-creator-shell/src/contracts/assetKind.ts index c4ec744e4..58ce50634 100644 --- a/apps/ai-game-creator-shell/src/contracts/assetKind.ts +++ b/apps/ai-game-creator-shell/src/contracts/assetKind.ts @@ -2,36 +2,54 @@ import type { GameCreationAppAssetKind } from './generated/GameCreationAppAssetK export type { GameCreationAppAssetKind } from './generated/GameCreationAppAssetKind'; -export const GAME_CREATION_APP_ASSET_KINDS = [ - 'image', - 'scene', - 'character', - 'character-animation', - 'icon', - 'icon-spritesheet', - 'icon-spec', - 'ui-design', - 'ui-design-doc', - 'publication-material', - 'spec', - 'video', - 'audio', - 'sound-effect', - 'background-music', - 'font', - 'document', - 'code', - 'unknown', -] as const satisfies readonly GameCreationAppAssetKind[]; +/** + * 穷举生成的 union:这里多写或少写一个成员都会直接报类型错误, + * 避免再维护一份与 `GameCreationAppAssetKind.ts` 分叉的手写列表。 + */ +const GAME_CREATION_APP_ASSET_KIND_MEMBERS: Record< + GameCreationAppAssetKind, + true +> = { + image: true, + scene: true, + character: true, + 'character-animation': true, + icon: true, + 'icon-spritesheet': true, + 'icon-spec': true, + 'ui-design': true, + 'ui-design-doc': true, + 'publication-material': true, + spec: true, + video: true, + audio: true, + 'sound-effect': true, + 'background-music': true, + font: true, + document: true, + code: true, + unknown: true, +}; + +export const GAME_CREATION_APP_ASSET_KINDS: readonly GameCreationAppAssetKind[] = + Object.keys( + GAME_CREATION_APP_ASSET_KIND_MEMBERS, + ) as GameCreationAppAssetKind[]; const KNOWN_ASSET_KINDS = new Set(GAME_CREATION_APP_ASSET_KINDS); +export function isGameCreationAppAssetKind( + raw: unknown, +): raw is GameCreationAppAssetKind { + return typeof raw === 'string' && KNOWN_ASSET_KINDS.has(raw); +} + export function parseGameCreationAppAssetKind( raw: unknown, context: string, ): GameCreationAppAssetKind { - if (typeof raw === 'string' && KNOWN_ASSET_KINDS.has(raw)) { - return raw as GameCreationAppAssetKind; + if (isGameCreationAppAssetKind(raw)) { + return raw; } const rawValue = typeof raw === 'string' ? raw : String(raw); 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 790552ead..41cc55ff4 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 @@ -96,7 +96,10 @@ import { isFloatingOverlayWheelEvent, useImageCanvasFloatingOptionDismiss, } from '../../../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss'; -import { parseGameCreationAppAssetKind } from '../../contracts/assetKind'; +import { + isGameCreationAppAssetKind, + parseGameCreationAppAssetKind, +} from '../../contracts/assetKind'; import { DesignWorkspacePanel } from '../../features/project-workspace/DesignWorkspacePanel'; import { LocalGamePreviewFrame, @@ -6096,7 +6099,11 @@ export default function ProjectDevelopmentView({ sourceResourceId: resource.id, sourcePath: resource.path, sourceMediaType: canonicalProjectedResourceMediaType(resource), - sourceSubtype: resource.subtype, + // sourceSubtype 只接受 manifest 的正式 kind;任务产物等非 manifest + // 来源不通过该字段传递。 + sourceSubtype: isGameCreationAppAssetKind(resource.subtype) + ? resource.subtype + : null, producerTaskId: resource.producerTaskId ?? '', }, }), diff --git a/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts b/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts index 12d9fc4c0..7584ff535 100644 --- a/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts +++ b/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts @@ -44,7 +44,9 @@ function rustAssetKindAliases(): Map { /** 从 Rust 侧解析 canonical kind 常量目录。 */ function rustCanonicalKinds(): string[] { - const source = readFileSync(RUST_CONTRACT_PATH, 'utf8'); + const source = expandRustAssetKindConsts( + readFileSync(RUST_CONTRACT_PATH, 'utf8'), + ); const start = source.indexOf( 'pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS', ); @@ -53,6 +55,27 @@ function rustCanonicalKinds(): string[] { return Array.from(body.matchAll(/"([^"]+)"/g)).map((match) => match[1]!); } +/** + * 把 Rust 侧 `pub const ..._ASSET_KIND: &str = "..."` 的常量引用展开成字符串字面量。 + * + * 共享契约里 `ui-design-doc` 等 kind 是常量(如 + * `GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND`)而不是裸字面量,不展开时下面的正则解析会 + * 漏掉这些条目,两侧目录比对就会假红。 + */ +function expandRustAssetKindConsts(source: string): string { + const consts = new Map(); + for (const decl of source.matchAll( + /pub const (\w*ASSET_KIND\w*)\s*:\s*&str\s*=\s*"([^"]+)";/g, + )) { + consts.set(decl[1]!, decl[2]!); + } + let expanded = source; + for (const [name, value] of consts) { + expanded = expanded.replaceAll(name, `"${value}"`); + } + return expanded; +} + /** Rust 的 `GameCreationAppAssetCategory` 变体名 → kebab-case 线上值。 */ function rustCategorySocketName(variant: string) { return variant.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); @@ -60,7 +83,9 @@ function rustCategorySocketName(variant: string) { /** 从 Rust 侧 `GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND` 里解析出 kind → 栏目表。 */ function rustAssetCategoryByKind(): Map { - const source = readFileSync(RUST_CONTRACT_PATH, 'utf8'); + const source = expandRustAssetKindConsts( + readFileSync(RUST_CONTRACT_PATH, 'utf8'), + ); const start = source.indexOf( 'pub const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND', ); @@ -120,10 +145,9 @@ const OBSERVED_ALIAS_INPUTS = [ describe('资源 kind 别名表跨语言一致性', () => { test('两侧 canonical kind 目录一致', () => { - 'font', - expect(rustCanonicalKinds()).toEqual([ - ...GAME_CREATION_APP_CANONICAL_ASSET_KINDS, - ]); + expect(rustCanonicalKinds()).toEqual([ + ...GAME_CREATION_APP_CANONICAL_ASSET_KINDS, + ]); }); test('两侧别名映射逐条一致', () => { @@ -311,9 +335,9 @@ describe('写侧 kind 字面量 → 分类的端到端口径', () => { 'apps/ai-game-creator-shell/src-tauri/src/commands.rs', 'utf8', ); - // 字体上传的写入侧:`register_local_asset_entry(root, path, "font", …)`。 + // 字体上传的写入侧直接传正式枚举成员:`register_local_asset_entry(root, path, GameCreationAppAssetKind::Font, …)`。 expect(commandsSource).toMatch( - /register_local_asset_entry\(\s*\n\s*root,\s*\n\s*&relative_path,\s*\n\s*"font",/, + /register_local_asset_entry\(\s*\n\s*root,\s*\n\s*&relative_path,\s*\n\s*GameCreationAppAssetKind::Font,/, ); expect(gameCreationAppAssetCategoryForKind('font')).toBe('document'); });