修复 AGC 资源 kind 枚举化后的编译与字符串回退
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 7m9s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 7m20s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 7m24s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 8m10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m39s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m54s
Project CI / Repository checks (pull_request) Failing after 4m43s
Project CI / Frontend tests (pull_request) Failing after 4m51s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

- 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,恢复编译期穷举保护

- 技术方案:登记边界同口径改为描述命令边界解析
This commit is contained in:
2026-09-19 02:38:30 +08:00
parent af38de9fd1
commit 5d20f5bfeb
9 changed files with 224 additions and 108 deletions
@@ -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),
@@ -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<Vec<String>, 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<PlatformArtGenerationReferences, String> {
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<str>,
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
));
}
@@ -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(),
@@ -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)?;
@@ -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<String>) {
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::<Vec<_>>();
(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::<Vec<_>>();
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);
@@ -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());
}
@@ -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,
@@ -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()),
@@ -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<GameCreationAppAssetKind, true>` 保证不会与生成 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/兜底分支。
- **登记边界同口径**:外部登记 kindTauri 命令 `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 资源冲突」。