From 3082c3e854b138af6337f59e507ddd2067741b1c Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Fri, 11 Sep 2026 18:46:58 +0800 Subject: [PATCH] =?UTF-8?q?kind=20=E5=88=AB=E5=90=8D=E8=A1=A8=E5=A4=A7?= =?UTF-8?q?=E5=B0=8F=E5=86=99=E4=B8=8D=E6=95=8F=E6=84=9F=E5=B9=B6=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=20font=EF=BC=9A=E5=A0=B5=E4=BD=8F=208=20=E6=9D=A1=20U?= =?UTF-8?q?I=20=E8=B5=84=E4=BA=A7=E6=B0=B8=E8=BF=9C=E8=90=BD=E3=80=8C?= =?UTF-8?q?=E5=BE=85=E5=BD=92=E7=B1=BB=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `canonical_game_creation_app_asset_kind`(Rust)与 `canonicalGameCreationAppAssetKind`(TS)改为对 trim 后的小写值查别名表与 canonical 目录,修正「UI 设计资产的现役写入侧写的是大写 `"UI"`」这条真机事实 - 别名表补 `font → document`:字体上传(`ttf / otf / woff / woff2`)登记的 manifest `kind` 就是 `font` - 为什么必须在别名表收口、读时自愈救不回来:`"UI"` 与 `"font"` 的派生结果本身就是 `unclassified`,自愈规则「派生值不是 unclassified 才触发」永不成立;真机 8 条 `kind:"UI"` + `mediaType:"application/json"` + `localPath:"ui/UI 设计 N.json"` 与写入路径逐字段吻合 - 改写 Rust 断言 `game_creation_app_asset_category_for_kind("UI") == Unclassified` → `UiInteraction`,并补 `canonical_...("UI") == "ui-design"` 与 `font == Document`:旧断言把这条 bug 钉成了「期望行为」,新形态才是唯一真源——写入侧真实写出的 kind 必须能落进明确栏目,否则真机资产永远归不了类 - 改写 TS 断言 `gameCreationAppAssetCategoryForKind('UI') == 'unclassified'` → `'ui-interaction'`,理由同上 - `game_creation_app.rs` 里「现役写入侧仍会写出这些非 canonical 值,必须在这里收口」的注释此前只收口了小写 `ui`,与大写写入侧矛盾,现按注释本意收口 - 新增写侧→分类的端到端契约用例:`resource_bridge.rs` 的 `bridge_is_idempotent_and_installs_source_image` 走真实生产函数 → 真实 `register_local_asset_at(..., "UI", ...)` → 断言落盘 `category` 为 `ui-interaction` - 新增 `assetKindCanonicalMapping.test.ts` 的「写侧 kind 字面量 → 分类」用例组:直接解析 UI 编辑器写侧源码第 3 个实参,写点换个新字面量就会红,防下次再漏 --- .../src/ui_editor/resource_bridge.rs | 9 +++ .../tests/assetKindCanonicalMapping.test.ts | 68 ++++++++++++++++++- .../src/contracts/gameCreationApp.test.ts | 11 ++- .../shared/src/contracts/gameCreationApp.ts | 12 +++- .../shared-contracts/src/game_creation_app.rs | 27 +++++++- 5 files changed, 120 insertions(+), 7 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs index 0200500d5..410746488 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs @@ -220,6 +220,7 @@ mod tests { use crate::ui_editor::persistence::{load_ui_design_state_at, LoadUiDesignStateInput}; use crate::ui_editor::utils::UIDesignImageId; use crate::{init_local_game_project_at, register_local_asset_entry}; + use shared_contracts::game_creation_app::GameCreationAppAssetCategory; use std::io::Cursor; fn fixture() -> tempfile::TempDir { @@ -281,6 +282,14 @@ mod tests { let first = ensure_ui_design_resource_for_prototype(input.clone()).expect("bridge"); assert!(first.created); assert_eq!(first.asset.kind, "UI"); + // 写侧 → 分类的端到端断言:这条路径走的是与 + // `workflow.rs` / `persistence.rs` 完全相同的 `register_local_asset_at(..., "UI", ...)`。 + // 别名表漏掉大写 `UI` 时,这里会落 unclassified(真机 8 条 UI 资产的表现), + // 且派生值本身就是 unclassified,读时自愈也救不回来。 + assert_eq!( + first.asset.category, + GameCreationAppAssetCategory::UiInteraction + ); assert_eq!( first.asset.source.reference_resource_ids, vec!["prototype-resource".to_string()] diff --git a/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts b/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts index 61e8d89ee..b0739ec5f 100644 --- a/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts +++ b/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts @@ -119,9 +119,10 @@ const OBSERVED_ALIAS_INPUTS = [ describe('资源 kind 别名表跨语言一致性', () => { test('两侧 canonical kind 目录一致', () => { - expect(rustCanonicalKinds()).toEqual([ - ...GAME_CREATION_APP_CANONICAL_ASSET_KINDS, - ]); + 'font', + expect(rustCanonicalKinds()).toEqual([ + ...GAME_CREATION_APP_CANONICAL_ASSET_KINDS, + ]); }); test('两侧别名映射逐条一致', () => { @@ -222,6 +223,9 @@ describe('真机出现的 kind 归类口径', () => { test.each(decided)( '$kind → $canonical → $category', + // 现役写入侧的大写字面量与字体 kind,两者都必须落进明确栏目。 + { kind: 'UI', canonical: 'ui-design', category: 'ui-interaction' }, + { kind: 'font', canonical: 'document', category: 'document' }, ({ kind, canonical, category }) => { expect(canonicalGameCreationAppAssetKind(kind)).toBe(canonical); expect(gameCreationAppAssetCategoryForKind(kind)).toBe(category); @@ -244,3 +248,61 @@ describe('真机出现的 kind 归类口径', () => { ).toEqual([...GAME_CREATION_APP_CANONICAL_ASSET_KINDS].sort()); }); }); + +describe('写侧 kind 字面量 → 分类的端到端口径', () => { + const UI_EDITOR_WRITE_SITES = [ + 'apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs', + 'apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs', + 'apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs', + ]; + + /** + * 取每个 `register_local_asset_(at|entry)(` 调用的第 3 个实参——写侧固定把 kind 放在 + * 第 3 位、单独占一行(`root` / 路径 / `"UI"`)。只认独占一行的字符串字面量, + * 传变量的写点直接跳过,避免把第 4 位的 mediaType 误当成 kind。 + */ + function registeredKindLiterals(file: string): string[] { + const source = readFileSync(file, 'utf8'); + const kinds: string[] = []; + for (const call of source.matchAll( + /register_local_asset_(?:at|entry)\(\r?\n(?:[^\n]*\r?\n){2}([^\n]*)/g, + )) { + const literal = /^\s*"([^"]*)",\s*$/.exec(call[1]!); + if (literal) kinds.push(literal[1]!); + } + return kinds; + } + + test('UI 设计写点仍直接写 `"UI"`,且它落 UI 交互而不是待归类', () => { + // 8 条真机 UI 资产是 `kind:"UI"` + `mediaType:"application/json"`: + // 该 kind 不在别名表里时派生结果也是 unclassified,读时自愈同样救不回来。 + for (const file of UI_EDITOR_WRITE_SITES) { + expect(registeredKindLiterals(file)).toContain('UI'); + } + expect(gameCreationAppAssetCategoryForKind('UI')).toBe('ui-interaction'); + }); + + test('UI 编辑器写点写出的每个 kind 都落明确栏目', () => { + // 画板导出这条链上的写点只写 UI 设计资产,不允许再写出落「待归类」的 kind。 + for (const file of UI_EDITOR_WRITE_SITES) { + for (const kind of registeredKindLiterals(file)) { + expect( + gameCreationAppAssetCategoryForKind(kind), + `${file} 写出的 kind ${kind} 落到了待归类`, + ).not.toBe('unclassified'); + } + } + }); + + test('字体写点写 `"font"`,落文档栏目', () => { + const commandsSource = readFileSync( + 'apps/ai-game-creator-shell/src-tauri/src/commands.rs', + 'utf8', + ); + // 字体上传的写入侧:`register_local_asset_entry(root, path, "font", …)`。 + expect(commandsSource).toMatch( + /register_local_asset_entry\(\s*\n\s*root,\s*\n\s*&relative_path,\s*\n\s*"font",/, + ); + expect(gameCreationAppAssetCategoryForKind('font')).toBe('document'); + }); +}); diff --git a/packages/shared/src/contracts/gameCreationApp.test.ts b/packages/shared/src/contracts/gameCreationApp.test.ts index e9164d80e..b6ab26444 100644 --- a/packages/shared/src/contracts/gameCreationApp.test.ts +++ b/packages/shared/src/contracts/gameCreationApp.test.ts @@ -747,6 +747,12 @@ describe('AI 游戏创作 App 共享契约', () => { ); expect(canonicalGameCreationAppAssetKind('illustration')).toBe('image'); expect(canonicalGameCreationAppAssetKind('character')).toBe('character'); + // 别名表大小写不敏感:UI 设计资产的现役写入侧写的是大写 `"UI"`。 + expect(canonicalGameCreationAppAssetKind('UI')).toBe('ui-design'); + expect(canonicalGameCreationAppAssetKind('UI-Prototype')).toBe('ui-design'); + // 字体资产的现役写入侧写的是 `font`。 + expect(canonicalGameCreationAppAssetKind('font')).toBe('document'); + expect(canonicalGameCreationAppAssetKind('FONT')).toBe('document'); expect(canonicalGameCreationAppAssetKind('unknown-kind')).toBe('image'); }); @@ -777,7 +783,10 @@ describe('AI 游戏创作 App 共享契约', () => { expect(gameCreationAppAssetCategoryForKind('game-background')).toBe( 'scene', ); - expect(gameCreationAppAssetCategoryForKind('UI')).toBe('unclassified'); + // 现役写入侧写的是大写 `"UI"`(画板导出的 UI 设计 JSON 资产)与 `font`(字体上传): + // 两者都必须落进明确栏目,否则 8 条真机 UI 资产会永远停在「待归类」。 + expect(gameCreationAppAssetCategoryForKind('UI')).toBe('ui-interaction'); + expect(gameCreationAppAssetCategoryForKind('font')).toBe('document'); expect(gameCreationAppAssetCategoryForKind('unknown-kind')).toBe( 'unclassified', ); diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts index af93c67a7..70e82533b 100644 --- a/packages/shared/src/contracts/gameCreationApp.ts +++ b/packages/shared/src/contracts/gameCreationApp.ts @@ -508,6 +508,9 @@ export type GameCreationAppCanonicalAssetKind = * 否则会落到 `canonicalGameCreationAppAssetKind` 的 `image` 兜底,再经 * `image → unclassified` 被误分到「待归类」。 * + * `font` 同样属于现役写入侧:字体上传(`ttf / otf / woff / woff2`)登记的 manifest + * 资产 `kind` 就是 `font`,不在此收口就只能落「待归类」。 + * * 本表与 Rust 侧 `server-rs/crates/shared-contracts/src/game_creation_app.rs` 的 * `canonical_game_creation_app_asset_kind` 必须成对维护,由 * `tests/assetKindCanonicalMapping.test.ts` 交叉钉住。 @@ -531,12 +534,19 @@ const GAME_CREATION_APP_LEGACY_ASSET_KINDS: Record< 'game-style': 'code', animation: 'character-animation', asset: 'image', + font: 'document', }; export function canonicalGameCreationAppAssetKind( value: string, ): GameCreationAppCanonicalAssetKind { - const normalized = value.trim(); + /** + * kind 词汇大小写不敏感:UI 设计资产的现役写入侧写的是**大写** `"UI"` + * (`src-tauri/src/ui_editor/resource_bridge.rs`、`workflow.rs`、`persistence.rs`)。 + * 只按小写收口会让它落 `image` 兜底、再经 `image → unclassified` 永远停在「待归类」, + * 且派生值本身就是 `unclassified`,读时自愈也救不回来。 + */ + const normalized = value.trim().toLowerCase(); const legacyKind = GAME_CREATION_APP_LEGACY_ASSET_KINDS[normalized]; if (legacyKind) return legacyKind; if ( diff --git a/server-rs/crates/shared-contracts/src/game_creation_app.rs b/server-rs/crates/shared-contracts/src/game_creation_app.rs index 797b8c44c..073eae8d9 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -570,7 +570,15 @@ pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS: [&str; 16] = [ ]; pub fn canonical_game_creation_app_asset_kind(value: &str) -> &'static str { - match value.trim() { + // kind 词汇大小写不敏感:UI 设计资产的现役写入侧写的是**大写** `"UI"` + // (`apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs` 的 + // `register_local_asset_at(..., "UI", ...)`,`workflow.rs` / `persistence.rs` 同), + // 只按小写收口会让它落到 `image` 兜底、再经 `image -> unclassified` 永远停在 + // 「待归类」;且派生值本身就是 unclassified,读时自愈也救不回来。 + // TS 侧 `canonicalGameCreationAppAssetKind` 用同一口径,由 + // `apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts` 交叉钉住。 + let normalized = value.trim().to_lowercase(); + match normalized.as_str() { "game-background" => "scene", "character-art" => "character", "ui-prototype" => "ui-design", @@ -583,6 +591,9 @@ pub fn canonical_game_creation_app_asset_kind(value: &str) -> &'static str { "game-entry" | "game-script" | "game-style" => "code", "animation" => "character-animation", "asset" => "image", + // 字体上传(`ttf / otf / woff / woff2`)登记的 manifest 资产 kind 就是 `font`, + // 不收口同样只能落「待归类」。 + "font" => "document", canonical => match GAME_CREATION_APP_CANONICAL_ASSET_KINDS .iter() .find(|candidate| **candidate == canonical) @@ -2150,9 +2161,21 @@ mod tests { game_creation_app_asset_category_for_kind("game-background"), GameCreationAppAssetCategory::Scene ); + // 现役写入侧写的是大写 `"UI"`(UI 设计 JSON 资产)与 `font`(字体上传)。 + // 旧断言钉的是「`"UI"` 落待归类」,那正是真机 8 条 UI 资产永远归不了类的成因: + // 派生值本身就是 unclassified,读时自愈也救不回来,只能在别名表收口。 assert_eq!( game_creation_app_asset_category_for_kind("UI"), - GameCreationAppAssetCategory::Unclassified + GameCreationAppAssetCategory::UiInteraction + ); + assert_eq!( + canonical_game_creation_app_asset_kind("UI"), + "ui-design", + "kind 别名表必须大小写不敏感" + ); + assert_eq!( + game_creation_app_asset_category_for_kind("font"), + GameCreationAppAssetCategory::Document ); assert_eq!( game_creation_app_asset_category_for_kind("unknown-kind"),