修复 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
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:
@@ -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)?;
|
||||
|
||||
+110
-24
@@ -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/兜底分支。
|
||||
- **登记边界同口径**:外部登记 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 资源冲突」。
|
||||
|
||||
Reference in New Issue
Block a user