From 5d20f5bfeb732a1791b57cec57b15ee5501ba7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 19 Sep 2026 02:38:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AGC=20=E8=B5=84=E6=BA=90?= =?UTF-8?q?=20kind=20=E6=9E=9A=E4=B8=BE=E5=8C=96=E5=90=8E=E7=9A=84?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E4=B8=8E=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=9B=9E?= =?UTF-8?q?=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - canvas_generation:参考素材合同四个 helper 改收 GameCreationAppAssetKind,删除 ui-prototype/game-background/art-spritesheet 字符串比较 - direct_runtime:删除 game-background / art-spritesheet 的参考合同字符串映射,直接传枚举 - resource_editor:音效/BGM 编辑类型判据改为比较 ResourceEditSourceKind::Manifest(BackgroundMusic),不再拼字符串 haystack - assets/commands:画板导入 kind 解析上移到 Tauri 命令边界,import_canvas_asset_at 只接收枚举 - 测试与批量标签脚手架:kind 参数由字符串改为 GameCreationAppAssetKind,恢复编译期穷举保护 - 技术方案:登记边界同口径改为描述命令边界解析 --- .../src-tauri/src/agent/direct_runtime/mod.rs | 7 +- .../src/agent/generation/canvas_generation.rs | 113 +++++++++------ .../src-tauri/src/assets.rs | 6 +- .../src-tauri/src/commands.rs | 4 +- .../project/manifest/classification_tests.rs | 134 ++++++++++++++---- .../src-tauri/src/project/resource_editor.rs | 19 ++- .../src-tauri/src/tests/project.rs | 43 ++++-- .../src-tauri/src/tests/provider.rs | 4 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 9 files changed, 224 insertions(+), 108 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 052faac32..5c4746b3b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -2587,15 +2587,10 @@ fn direct_taonier_art_asset_identity( }; // 参考集合先按该 kind 的请求合同收口:根素材(规范图)允许用户参考(icon-spec 没有 // 规范前置,参考只是风格输入),派生素材仍必须按合同携带规范前置。 - let reference_contract_kind = match expected_kind { - GameCreationAppAssetKind::Scene => "game-background", - GameCreationAppAssetKind::IconSpritesheet => "art-spritesheet", - kind => kind.as_str(), - }; let references_match_contract = crate::agent::platform_art_runtime_references_match_request_contract( &identity.reference_resource_ids, - reference_contract_kind, + expected_kind, ); let lineage_matches = match expected_reference_source { Some(source) => direct_taonier_reference_matches_local_source(root, source, &identity), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index f06d1cc8f..d59c7a493 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -529,16 +529,22 @@ pub(crate) fn normalize_platform_art_asset_generation_kind( /// 需要规范图前置的生成类型:这些请求必须解析出当前账号的规范图引用,用户参考最多 /// [`PLATFORM_ART_MAX_USER_REFERENCE_IMAGES_WITH_CANONICAL_SPEC`] 张。 -pub(crate) fn platform_art_asset_kind_requires_canonical_spec_reference(asset_kind: &str) -> bool { +pub(crate) fn platform_art_asset_kind_requires_canonical_spec_reference( + asset_kind: GameCreationAppAssetKind, +) -> bool { matches!( asset_kind, - "ui-prototype" | "game-background" | "art-spritesheet" + GameCreationAppAssetKind::UiDesign + | GameCreationAppAssetKind::Scene + | GameCreationAppAssetKind::IconSpritesheet ) } /// 只有单规范引用的图集操作不接受用户参考:非法参考必须在原生提交处**拒绝**,不能静默丢弃。 -pub(crate) fn platform_art_asset_kind_accepts_user_reference_assets(asset_kind: &str) -> bool { - asset_kind != "art-spritesheet" +pub(crate) fn platform_art_asset_kind_accepts_user_reference_assets( + asset_kind: GameCreationAppAssetKind, +) -> bool { + asset_kind != GameCreationAppAssetKind::IconSpritesheet } /// 收口参考素材 id 入参:trim、去重(保持给出顺序),并拒绝路径 / 远端资源 ID / 跨项目身份。 @@ -546,7 +552,7 @@ pub(crate) fn platform_art_asset_kind_accepts_user_reference_assets(asset_kind: /// 这里只做**形状与数量**校验;「是不是当前项目已登记图片素材」由 /// [`manifest_asset_remote_reference_at`] 用 manifest 身份与图片解码证明,不靠命名猜测。 pub(crate) fn normalize_platform_art_reference_asset_ids( - asset_kind: &str, + asset_kind: GameCreationAppAssetKind, asset_ids: &[String], ) -> Result, String> { if !platform_art_asset_kind_accepts_user_reference_assets(asset_kind) && !asset_ids.is_empty() { @@ -2121,11 +2127,11 @@ async fn resolve_platform_art_generation_references_at( options: &PlatformArtAssetGenerationOptions, ) -> Result { let user_reference_asset_ids = normalize_platform_art_reference_asset_ids( - &options.asset_kind, + options.asset_kind, &options.reference_asset_ids, )?; let requires_canonical = - platform_art_asset_kind_requires_canonical_spec_reference(&options.asset_kind); + platform_art_asset_kind_requires_canonical_spec_reference(options.asset_kind); // 预检:本次请求要用到的所有参考(规范图 + 用户参考)先在本地全部验证一遍, // 任何一个不合格都必须在**任何上传之前**失败,避免「第一张参考已上传、第二张坏图才失败」。 if requires_canonical || !user_reference_asset_ids.is_empty() { @@ -2708,9 +2714,9 @@ pub(in crate::agent) async fn generate_platform_art_asset_with_retained_runtime_ /// 这里判的是「形状」,不是具体身份:规范图到底是什么由调用方用当前账号的解析结果单独比对。 /// 提交侧的顺序合同固定为「规范图前置在最前,用户参考按给定顺序追加在后」,所以: /// -/// - `art-spritesheet`:只接受唯一规范引用,多一个用户参考都不算同合同; -/// - `ui-prototype` / `game-background`:必须有规范图前置(至少 1 项),总数不超过总上限; -/// - 其余放行 kind(`icon-spec` 等):没有规范前置,可以零参考,也可以全是用户参考; +/// - `IconSpritesheet`:只接受唯一规范引用,多一个用户参考都不算同合同; +/// - `UiDesign` / `Scene`:必须有规范图前置(至少 1 项),总数不超过总上限; +/// - 其余放行 kind(`IconSpec` 等):没有规范前置,可以零参考,也可以全是用户参考; /// - 不在目录里的 kind 一律判为不符合,失败关闭,不做兜底猜测。 /// /// 提交侧的收口在 [`normalize_platform_art_reference_asset_ids`] 与 @@ -2718,20 +2724,19 @@ pub(in crate::agent) async fn generate_platform_art_asset_with_retained_runtime_ /// 是不是该 kind 的合法形状」,两边必须共用同一套上限,否则恢复校验会误判合法请求。 pub(crate) fn platform_art_runtime_references_match_request_contract( reference_resource_ids: &[String], - expected_asset_kind: impl AsRef, + expected_asset_kind: GameCreationAppAssetKind, ) -> bool { - let expected_asset_kind = expected_asset_kind.as_ref(); if reference_resource_ids .iter() .any(|reference| reference.trim().is_empty()) { return false; } - // `game-background` 只由 Direct 运行时直接构造 options,不走 kind 归一目录,所以单独放行。 - if expected_asset_kind != "game-background" + // `Scene` 只由 Direct 运行时直接构造 options,不走 GUI kind 目录,所以单独放行。 + if expected_asset_kind != GameCreationAppAssetKind::Scene && !PLATFORM_ART_ASSET_GENERATION_KINDS .iter() - .any(|kind| kind.as_str() == expected_asset_kind) + .any(|kind| *kind == expected_asset_kind) { return false; } @@ -2769,7 +2774,7 @@ pub(in crate::agent) fn retained_platform_art_generation_runtime_state_matches_d && snapshot.generation_kind == "spec" && platform_art_runtime_references_match_request_contract( &snapshot.reference_resource_ids, - "icon-spec", + GameCreationAppAssetKind::IconSpec, ) && request_asset_kind.as_deref() == Some("icon-spec") && art_spec_asset_type == Some("icon-spec") @@ -2779,7 +2784,7 @@ pub(in crate::agent) fn retained_platform_art_generation_runtime_state_matches_d && snapshot.generation_kind == "spec" && platform_art_runtime_references_match_request_contract( &snapshot.reference_resource_ids, - "game-background", + GameCreationAppAssetKind::Scene, ) && request_asset_kind.as_deref() == Some("game-background") && art_spec_asset_type == Some("background") @@ -2789,7 +2794,7 @@ pub(in crate::agent) fn retained_platform_art_generation_runtime_state_matches_d && snapshot.generation_kind == "icon-spritesheet" && platform_art_runtime_references_match_request_contract( &snapshot.reference_resource_ids, - "art-spritesheet", + GameCreationAppAssetKind::IconSpritesheet, ) && request_asset_kind.is_none() && art_spec_asset_type == Some("art") @@ -12608,7 +12613,7 @@ mod canvas_generation_tests { import_canvas_asset_at( root, AGENT_RUNTIME_ART_SPEC_PATH, - "icon-spec", + GameCreationAppAssetKind::IconSpec, "image/png", "historical-canvas-project", Some("historical-resource".to_string()), @@ -12853,8 +12858,11 @@ mod canvas_generation_tests { }; // 去重保持给出顺序,空白项直接丢弃。 assert_eq!( - normalize_platform_art_reference_asset_ids("icon-spec", &ids(&[" b ", "a", "b", " "])) - .expect("normalize icon-spec references"), + normalize_platform_art_reference_asset_ids( + GameCreationAppAssetKind::IconSpec, + &ids(&[" b ", "a", "b", " "]) + ) + .expect("normalize icon-spec references"), ids(&["b", "a"]) ); // 路径、跨项目远端资源 ID 与非法字符都不是可接受的素材身份。 @@ -12866,7 +12874,11 @@ mod canvas_generation_tests { &"a".repeat(PLATFORM_ART_REFERENCE_ASSET_ID_MAX_CHARS + 1), ] { assert!( - normalize_platform_art_reference_asset_ids("icon-spec", &ids(&[rejected])).is_err(), + normalize_platform_art_reference_asset_ids( + GameCreationAppAssetKind::IconSpec, + &ids(&[rejected]) + ) + .is_err(), "{rejected} 不能被当成参考素材 id" ); } @@ -12881,30 +12893,36 @@ mod canvas_generation_tests { 5 ); assert!(normalize_platform_art_reference_asset_ids( - "icon-spec", + GameCreationAppAssetKind::IconSpec, &ids(&["a", "b", "c", "d", "e", "f"]) ) .is_err()); assert_eq!( - normalize_platform_art_reference_asset_ids("ui-prototype", &ids(&["a", "b", "c", "d"])) - .expect("four user references with a canonical spec") - .len(), + normalize_platform_art_reference_asset_ids( + GameCreationAppAssetKind::UiDesign, + &ids(&["a", "b", "c", "d"]) + ) + .expect("four user references with a canonical spec") + .len(), 4 ); assert!(normalize_platform_art_reference_asset_ids( - "ui-prototype", + GameCreationAppAssetKind::UiDesign, &ids(&["a", "b", "c", "d", "e"]) ) .is_err()); // 图集只接受单规范引用:额外参考必须被拒绝,不能静默丢弃。 - assert!( - normalize_platform_art_reference_asset_ids("art-spritesheet", &ids(&["a"])).is_err() - ); - assert!( - normalize_platform_art_reference_asset_ids("art-spritesheet", &[]) - .expect("spritesheet without user references") - .is_empty() - ); + assert!(normalize_platform_art_reference_asset_ids( + GameCreationAppAssetKind::IconSpritesheet, + &ids(&["a"]) + ) + .is_err()); + assert!(normalize_platform_art_reference_asset_ids( + GameCreationAppAssetKind::IconSpritesheet, + &[] + ) + .expect("spritesheet without user references") + .is_empty()); } /// 恢复侧与提交侧必须共用同一套参考上限,否则合法账本会被判成身份不符。 @@ -12918,18 +12936,21 @@ mod canvas_generation_tests { // 根素材(icon-spec):没有规范前置,可以零参考,也可以全是用户参考。 assert!(platform_art_runtime_references_match_request_contract( &[], - "icon-spec" + GameCreationAppAssetKind::IconSpec )); assert!(platform_art_runtime_references_match_request_contract( &references(5), - "icon-spec" + GameCreationAppAssetKind::IconSpec )); assert!(!platform_art_runtime_references_match_request_contract( &references(6), - "icon-spec" + GameCreationAppAssetKind::IconSpec )); // 有规范前置:规范图必须在场,总量仍不超过 5 张(含规范图)。 - for kind in ["ui-prototype", "game-background"] { + for kind in [ + GameCreationAppAssetKind::UiDesign, + GameCreationAppAssetKind::Scene, + ] { assert!( !platform_art_runtime_references_match_request_contract(&[], kind), "{kind} 必须有规范图前置" @@ -12950,32 +12971,32 @@ mod canvas_generation_tests { // 图集:恰好一项,多一项都不算同一份请求合同。 assert!(!platform_art_runtime_references_match_request_contract( &[], - "art-spritesheet" + GameCreationAppAssetKind::IconSpritesheet )); assert!(platform_art_runtime_references_match_request_contract( &references(1), - "art-spritesheet" + GameCreationAppAssetKind::IconSpritesheet )); assert!(!platform_art_runtime_references_match_request_contract( &references(2), - "art-spritesheet" + GameCreationAppAssetKind::IconSpritesheet )); // 普通图片类生成与规范图共用总上限;空白项与未知 kind 一律拒绝。 assert!(platform_art_runtime_references_match_request_contract( &references(5), - "image" + GameCreationAppAssetKind::Image )); assert!(!platform_art_runtime_references_match_request_contract( &references(6), - "image" + GameCreationAppAssetKind::Image )); assert!(!platform_art_runtime_references_match_request_contract( &[" ".to_string()], - "icon-spec" + GameCreationAppAssetKind::IconSpec )); assert!(!platform_art_runtime_references_match_request_contract( &references(1), - "unknown-kind" + GameCreationAppAssetKind::Unknown )); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/assets.rs b/apps/ai-game-creator-shell/src-tauri/src/assets.rs index 7b2e2e443..0710c3351 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/assets.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/assets.rs @@ -697,8 +697,6 @@ pub(crate) fn import_canvas_asset_at( if resource_id.is_none() && asset_object_id.is_none() { return Err("画板资源 ID 和资产对象 ID 至少需要一个".to_string()); } - let kind = registration_asset_kind(kind, "canvas.asset_import"); - register_local_asset_at( root, local_path, @@ -2331,7 +2329,7 @@ mod tests { let plain = register_local_asset_entry( root, "assets/plain.png", - "image", + GameCreationAppAssetKind::Image, "image/png", "platform-art", canvas_source(), @@ -2345,7 +2343,7 @@ mod tests { register_local_asset_entry( root, "assets/hero.png", - "image", + GameCreationAppAssetKind::Image, "image/png", "platform-art", canvas_source(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 28c32c6c1..d26f479f1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -2476,7 +2476,7 @@ pub(crate) fn import_canvas_asset( import_canvas_asset_at( root, local_path.trim(), - kind.trim(), + registration_asset_kind(kind.trim(), "canvas.asset_import"), media_type.trim(), canvas_project_id.trim(), trim_optional_string(resource_id), @@ -4958,7 +4958,7 @@ pub(crate) fn prepare_local_project_asset_generation( // 参考入参只接受当前项目 manifest 素材 id:路径、远端 resourceId 与超限在这里就被拒绝, // 不把校验推迟到远端(远端只该收到当前账号绑定下的 resource ID)。 let reference_asset_ids = - normalize_platform_art_reference_asset_ids(asset_kind.as_str(), reference_asset_ids)?; + normalize_platform_art_reference_asset_ids(asset_kind, reference_asset_ids)?; // GUI 完成登记层参数:入口栏目与生成 kind 不是同一套词汇,只有调用方显式给出目标分类 // 才能把产物原位落回入口栏目。非法值(含 `version` / `all` 这类栏目伪值)直接失败关闭。 let target_category = normalize_platform_art_target_category(target_category)?; diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/manifest/classification_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/manifest/classification_tests.rs index eb747e2de..78ca10a5d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/manifest/classification_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/manifest/classification_tests.rs @@ -561,7 +561,12 @@ fn asset_classification_audit_failure_is_reported_and_never_faked() { } /// 批量标签测试的公共脚手架:登记素材、读原始字节、数审计记录。 -fn register_batch_tag_asset(root: &Path, local_path: &str, kind: &str, media_type: &str) -> String { +fn register_batch_tag_asset( + root: &Path, + local_path: &str, + kind: GameCreationAppAssetKind, + media_type: &str, +) -> String { register_local_asset_entry( root, local_path, @@ -587,7 +592,7 @@ fn register_batch_tag_asset(root: &Path, local_path: &str, kind: &str, media_typ fn project_with_batch_tag_assets( test_name: &str, - specs: &[(&str, &str, &str)], + specs: &[(&str, GameCreationAppAssetKind, &str)], ) -> (PathBuf, Vec) { let root = classification_test_root(test_name); init_local_game_project_at(&root, "batch-tag-project", "批量标签测试") @@ -595,7 +600,7 @@ fn project_with_batch_tag_assets( let ids = specs .iter() .map(|(local_path, kind, media_type)| { - register_batch_tag_asset(&root, local_path, kind, media_type) + register_batch_tag_asset(&root, local_path, *kind, media_type) }) .collect::>(); (root, ids) @@ -647,9 +652,21 @@ fn batch_tags_append_keeps_existing_tags_and_categories() { let (root, ids) = project_with_batch_tag_assets( "batch-append", &[ - ("assets/hero.png", "character", "image/png"), - ("assets/theme.mp3", "background-music", "audio/mpeg"), - ("assets/untouched.png", "image", "image/png"), + ( + "assets/hero.png", + GameCreationAppAssetKind::Character, + "image/png", + ), + ( + "assets/theme.mp3", + GameCreationAppAssetKind::BackgroundMusic, + "audio/mpeg", + ), + ( + "assets/untouched.png", + GameCreationAppAssetKind::Image, + "image/png", + ), ], ); let project_id = batch_tag_project_id(&root); @@ -739,8 +756,16 @@ fn batch_tags_dedupe_asset_ids_and_tags() { let (root, ids) = project_with_batch_tag_assets( "batch-dedupe", &[ - ("assets/hero.png", "image", "image/png"), - ("assets/theme.mp3", "background-music", "audio/mpeg"), + ( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + ), + ( + "assets/theme.mp3", + GameCreationAppAssetKind::BackgroundMusic, + "audio/mpeg", + ), ], ); let project_id = batch_tag_project_id(&root); @@ -781,8 +806,16 @@ fn batch_tags_repeat_is_a_noop_without_write_audit_or_revision() { let (root, ids) = project_with_batch_tag_assets( "batch-noop", &[ - ("assets/hero.png", "image", "image/png"), - ("assets/theme.mp3", "background-music", "audio/mpeg"), + ( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + ), + ( + "assets/theme.mp3", + GameCreationAppAssetKind::BackgroundMusic, + "audio/mpeg", + ), ], ); let project_id = batch_tag_project_id(&root); @@ -833,8 +866,16 @@ fn batch_tags_mixed_noop_and_change_audits_only_changed_assets() { let (root, ids) = project_with_batch_tag_assets( "batch-mixed", &[ - ("assets/hero.png", "image", "image/png"), - ("assets/theme.mp3", "background-music", "audio/mpeg"), + ( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + ), + ( + "assets/theme.mp3", + GameCreationAppAssetKind::BackgroundMusic, + "audio/mpeg", + ), ], ); let project_id = batch_tag_project_id(&root); @@ -907,8 +948,16 @@ fn batch_tags_missing_target_writes_nothing() { let (root, ids) = project_with_batch_tag_assets( "batch-missing", &[ - ("assets/hero.png", "image", "image/png"), - ("assets/theme.mp3", "background-music", "audio/mpeg"), + ( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + ), + ( + "assets/theme.mp3", + GameCreationAppAssetKind::BackgroundMusic, + "audio/mpeg", + ), ], ); let project_id = batch_tag_project_id(&root); @@ -947,8 +996,16 @@ fn batch_tags_rejects_merged_tag_limit_and_single_tag_length() { let (root, ids) = project_with_batch_tag_assets( "batch-limits", &[ - ("assets/hero.png", "image", "image/png"), - ("assets/theme.mp3", "background-music", "audio/mpeg"), + ( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + ), + ( + "assets/theme.mp3", + GameCreationAppAssetKind::BackgroundMusic, + "audio/mpeg", + ), ], ); let project_id = batch_tag_project_id(&root); @@ -1035,8 +1092,14 @@ fn batch_tags_rejects_merged_tag_limit_and_single_tag_length() { /// 空批次与空标签都不接受:空白 `assetId` 整批拒绝(不静默跳过),全空标签归一后拒绝。 #[test] fn batch_tags_rejects_empty_batch_and_empty_tags() { - let (root, ids) = - project_with_batch_tag_assets("batch-empty", &[("assets/hero.png", "image", "image/png")]); + let (root, ids) = project_with_batch_tag_assets( + "batch-empty", + &[( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + )], + ); let project_id = batch_tag_project_id(&root); let revision_before = batch_tag_revision(&root); let bytes_before = batch_tag_manifest_bytes(&root); @@ -1098,7 +1161,12 @@ fn batch_tags_accepts_two_hundred_assets_and_rejects_two_hundred_one() { .expect("initialize batch bound project"); let ids = (0..=ASSET_BATCH_TAG_MAX_ASSETS) .map(|index| { - register_batch_tag_asset(&root, &format!("assets/a{index}.png"), "image", "image/png") + register_batch_tag_asset( + &root, + &format!("assets/a{index}.png"), + GameCreationAppAssetKind::Image, + "image/png", + ) }) .collect::>(); let project_id = batch_tag_project_id(&root); @@ -1139,8 +1207,14 @@ fn batch_tags_accepts_two_hundred_assets_and_rejects_two_hundred_one() { /// 项目身份与 revision CAS 沿用单素材口径:身份不符 / 版本冲突都在写之前失败。 #[test] fn batch_tags_keeps_project_identity_and_revision_cas() { - let (root, ids) = - project_with_batch_tag_assets("batch-cas", &[("assets/hero.png", "image", "image/png")]); + let (root, ids) = project_with_batch_tag_assets( + "batch-cas", + &[( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + )], + ); let project_id = batch_tag_project_id(&root); let revision_before = batch_tag_revision(&root); let bytes_before = batch_tag_manifest_bytes(&root); @@ -1201,7 +1275,11 @@ fn batch_tags_input_rejects_unknown_fields() { fn batch_tags_command_requires_asset_register_permission() { let (root, ids) = project_with_batch_tag_assets( "batch-permission", - &[("assets/hero.png", "image", "image/png")], + &[( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + )], ); let project_id = batch_tag_project_id(&root); let revision_before = batch_tag_revision(&root); @@ -1234,8 +1312,16 @@ fn batch_tags_audit_failure_reports_written_batch() { let (root, ids) = project_with_batch_tag_assets( "batch-audit-failure", &[ - ("assets/hero.png", "image", "image/png"), - ("assets/theme.mp3", "background-music", "audio/mpeg"), + ( + "assets/hero.png", + GameCreationAppAssetKind::Image, + "image/png", + ), + ( + "assets/theme.mp3", + GameCreationAppAssetKind::BackgroundMusic, + "audio/mpeg", + ), ], ); let project_id = batch_tag_project_id(&root); diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs index 0e22209dc..7cee0b18a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs @@ -1135,15 +1135,14 @@ fn source_asset_canonical_resource_id(asset: &GameCreationAppAssetManifestEntry) format!("local-asset:{}", asset.id) } -fn resource_edit_audio_kind(asset_kind: &str, path: &str) -> LocalProjectResourceEditKind { - let haystack = format!( - "{} {}", - asset_kind.to_ascii_lowercase(), - path.to_ascii_lowercase() - ); - if ["background", "bgm", "music", "theme"] - .iter() - .any(|marker| haystack.contains(marker)) +fn resource_edit_audio_kind( + asset_kind: ResourceEditSourceKind, + path: &str, +) -> LocalProjectResourceEditKind { + if asset_kind == ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::BackgroundMusic) + || ["background", "bgm", "music", "theme"] + .iter() + .any(|marker| path.to_ascii_lowercase().contains(marker)) { LocalProjectResourceEditKind::BackgroundMusic } else { @@ -1478,7 +1477,7 @@ fn resolve_resource_edit_source( if matches!( input.edit_kind, LocalProjectResourceEditKind::SoundEffect | LocalProjectResourceEditKind::BackgroundMusic - ) && resource_edit_audio_kind(asset_kind.as_str(), &path) != input.edit_kind + ) && resource_edit_audio_kind(asset_kind, &path) != input.edit_kind { return Err("音频资源的音效/BGM 编辑类型与源资源用途不一致".to_string()); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index 00c8e9fbe..7beeb6825 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -1351,7 +1351,10 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po // 只接受单规范引用的图集不接受用户参考,且必须明确拒绝而不是静默丢弃。 let error = reject( &root, - options("art-spritesheet", vec!["user-asset".to_string()]), + options( + GameCreationAppAssetKind::IconSpritesheet, + vec!["user-asset".to_string()], + ), ) .await; assert!(error.contains("透明美术图集只接受规范图引用"), "{error}"); @@ -1362,7 +1365,14 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po "..\\hero.png", "https://example.com/hero.png", ] { - let error = reject(&root, options("icon-spec", vec![rejected.to_string()])).await; + let error = reject( + &root, + options( + GameCreationAppAssetKind::IconSpec, + vec![rejected.to_string()], + ), + ) + .await; assert!(error.contains("不接受路径或远端资源 ID"), "{error}"); } @@ -1370,7 +1380,7 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po let error = reject( &root, options( - "icon-spec", + GameCreationAppAssetKind::IconSpec, vec!["editor-resource-from-older-account".to_string()], ), ) @@ -1395,7 +1405,7 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po let error = reject( &root, options( - "icon-spec", + GameCreationAppAssetKind::IconSpec, vec![manifest_asset_id(&root, "assets/document.json")], ), ) @@ -1411,14 +1421,18 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po let missing_asset_id = manifest_asset_id(&root, "assets/missing-reference.png"); fs::remove_file(root.join("assets/missing-reference.png")) .expect("remove missing reference fixture file"); - let error = reject(&root, options("icon-spec", vec![missing_asset_id])).await; + let error = reject( + &root, + options(GameCreationAppAssetKind::IconSpec, vec![missing_asset_id]), + ) + .await; assert!(error.contains("不存在;请重新登记后再引用"), "{error}"); // 超过总上限:无规范前置最多 5 张。 let error = reject( &root, options( - "icon-spec", + GameCreationAppAssetKind::IconSpec, (0..6).map(|index| format!("reference-{index}")).collect(), ), ) @@ -1450,7 +1464,7 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po let error = reject( &root, options( - "icon-spec", + GameCreationAppAssetKind::IconSpec, vec![plain_reference_asset_id.clone(), vector_asset_id], ), ) @@ -1476,7 +1490,7 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po let error = reject( &root, options( - "icon-spec", + GameCreationAppAssetKind::IconSpec, vec![plain_reference_asset_id.clone(), mislabeled_asset_id], ), ) @@ -1498,7 +1512,7 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po let error = reject( &root, options( - "icon-spec", + GameCreationAppAssetKind::IconSpec, vec![plain_reference_asset_id.clone(), broken_asset_id], ), ) @@ -1516,7 +1530,10 @@ async fn reference_selection_rejects_unsupported_inputs_before_any_generation_po let differential = request_platform_art_asset_with_options_for_test( &root, "参考素材门禁", - &options("icon-spec", vec![plain_reference_asset_id.clone()]), + &options( + GameCreationAppAssetKind::IconSpec, + vec![plain_reference_asset_id.clone()], + ), ) .await; let mut upload_ticket_attempted = false; @@ -2881,7 +2898,7 @@ fn import_canvas_asset_registers_canvas_source_metadata() { let result = import_canvas_asset_at( &root, "assets/canvas-hero.png", - "character", + GameCreationAppAssetKind::Character, "image/png", "canvas-project-1", Some("resource-1".to_string()), @@ -2927,7 +2944,7 @@ fn import_canvas_asset_requires_traceable_canvas_ids() { let missing_project = import_canvas_asset_at( &root, "assets/canvas-hero.png", - "character", + GameCreationAppAssetKind::Character, "image/png", "", Some("resource-1".to_string()), @@ -2942,7 +2959,7 @@ fn import_canvas_asset_requires_traceable_canvas_ids() { let missing_asset = import_canvas_asset_at( &root, "assets/canvas-hero.png", - "character", + GameCreationAppAssetKind::Character, "image/png", "canvas-project-1", None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs index dca1fec8e..835d2ce16 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs @@ -7270,7 +7270,7 @@ fn local_asset_prompt_context_summarizes_uploaded_and_canvas_assets() { import_canvas_asset_at( &root, "assets/canvas-hero.png", - "character", + GameCreationAppAssetKind::Character, "image/png", "canvas-project-1", Some("resource-1".to_string()), @@ -7316,7 +7316,7 @@ fn local_asset_prompt_context_discovers_unregistered_media_without_formal_identi import_canvas_asset_at( &root, "assets/registered.png", - "ui-design", + GameCreationAppAssetKind::UiDesign, "image/png", "canvas-project-registered", Some("resource-registered".to_string()), diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index c003d3483..a1c5a66d0 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -7,7 +7,7 @@ - **唯一词汇表**:kind 的变体、线上值(kebab-case)、`as_str()`、`ALL`、严格解析与 ts-rs 绑定全部由 `server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs` 的声明表派生。`GAME_CREATION_APP_CANONICAL_ASSET_KINDS`、`canonical_game_creation_app_asset_kind()`(含 `font → document` 特例与 legacy 别名表)已删除;TS 侧同名的 `GAME_CREATION_APP_CANONICAL_ASSET_KINDS`、`GameCreationAppCanonicalAssetKind`、`GAME_CREATION_APP_LEGACY_ASSET_KINDS`、`canonicalGameCreationAppAssetKind()` 一并删除,仓库里不再有第二份 kind 列表。 - **TS 生成路径**:`GameCreationAppAssetKind` 由 ts-rs 生成到 `packages/shared/src/contracts/generated/GameCreationAppAssetKind.ts`(不再是 `apps/ai-game-creator-shell/src/contracts/generated/`);`packages/shared/src/contracts/gameCreationApp.ts` 直接 re-export 该 union,运行期 kind 列表只有一份 `GAME_CREATION_APP_ASSET_KINDS`(穷举 `Record` 保证不会与生成 union 分叉)。重新生成:`cargo test --locked -p shared-contracts --features ts-bindings export_bindings --manifest-path server-rs/Cargo.toml`,之后 `git diff` 必须为空。两张生成目录的忽略规则同步登记在 `.prettierignore` 与 `.eslintrc.cjs`。 - **严格解析只有一个入口**:`GameCreationAppAssetKind::parse_with_context(value, context)` 是唯一公开解析入口(`FromStr`、serde、所有外部边界都走它),内部严格匹配是私有 `match_canonical()`;先前那批易混名字(`from_str_lossy()`、公开的 `from_str_or_unknown()`)都已删除,认不出 canonical 值只有这一处收口 + 留痕。口径是等值匹配——不 trim、不 lowercase、不查别名、不迁移;`"UI"`、`"ui"`、`" image "`、`"art-spritesheet-slice"` 一律收口成 `unknown`(分类落 `unclassified`)。**接受 unknown 是显式决定**:留痕日志给出原始串与上下文,供回查仍在写 legacy kind 的代码;不写兼容、不做迁移。 -- **登记边界同口径**:外部登记 kind(`commands::register_local_asset`、`import_canvas_asset_at`、`sync_canvas_project_assets_at`、平台导入)统一走 `assets::registration_asset_kind()`——空白入参按「没有信息」落中性 `image`(→「待归类」),非空但认不出的值走严格解析收口成 `Unknown` 并留痕。三条边界不再各写一份 trim/兜底分支。 +- **登记边界同口径**:外部登记 kind(Tauri 命令 `commands::register_local_asset`、`commands::import_canvas_asset`、平台导入)统一走 `assets::registration_asset_kind()`——空白入参按「没有信息」落中性 `image`(→「待归类」),非空但认不出的值走严格解析收口成 `Unknown` 并留痕。三条边界不再各写一份 trim/兜底分支;解析只发生在命令边界,`import_canvas_asset_at` 等内部函数直接接收 `GameCreationAppAssetKind`,不再接受字符串 kind。 - **登记内容的 kind 必须与内容相符**:`normalize_local_project_raster_resource_at` 的源 subtype 只接受图片族成员(`image / scene / character / character-animation / icon / icon-spritesheet / icon-spec / ui-design`)。未提供或空白时按没有类型信息落 `image`;音频、视频、文档、字体、代码等非图片族值,以及未知字符串,均明确报错,不再静默改成 `image`,以暴露调用方错误。派生资源写回 manifest 时不得主动写 `Unknown`:Agent 回执派生物是 markdown 文本,按 `Text => Document` 同口径落 `document`。 - **切片残留判据只看路径**:`register_existing_platform_art_slices_at` 判定「已有半成品切片登记」时只看 `assets/art-spritesheet-slices/` 前缀,不再附带 `kind == Icon`——历史切片刻写的是非 canonical kind,严格解析后是 `unknown`,再带 kind 判据会漏掉这些登记并静默跳过回填。 - **sprite 身份不含展示串**:UI 工作流比较 sprite 资源身份时忽略 `metadata.asset_type`(它随 canonical kind 派生),只看真正影响渲染的字段;否则对既有 State 重跑工作流会误报「与已有 State 资源冲突」。