AGC 壳把资源 kind 的登记与解析边界统一到严格口径
- assets.rs:新增 `registration_asset_kind()`,空白入参落中性 `image`、非空认不出严格收口成 `Unknown` 并留痕;`import_canvas_asset_at` 改走该函数。 - commands.rs:`imported_platform_asset_kind` 与 `register_local_asset` 一并委托给 `registration_asset_kind()`,删掉两处各写一遍的 trim/严格解析分支。 - agent/generation/canvas_generation.rs:切片残留登记判据改为只看 `assets/art-spritesheet-slices/` 路径——历史切片刻的 kind 是非 canonical 值,严格解析后收口成 `unknown`,再带 kind 判据会漏掉半成品登记并静默跳过回填;补平台词汇映射用例钉住别名与未映射值落点。 - project/resource_editor.rs:栅格归一化的源 subtype 收口到图片族成员(音频/视频/文档/字体等一律按图片登记),Agent 回执派生物写回 `Document` 而不再主动写 `Unknown`;补派生 kind 用例。 - ui_editor/workflow.rs:新增 `same_sprite_asset_identity()`,比较 sprite 身份时忽略随 kind 派生的 `metadata.asset_type`,避免对既有 State 重跑工作流误报资源冲突。
This commit is contained in:
@@ -7013,11 +7013,14 @@ pub(in crate::agent) fn register_existing_platform_art_slices_at(
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| "旧项目图集源资源缺少 sourceResourceId".to_string())?;
|
||||
// 判据只看路径:`assets/art-spritesheet-slices/` 是切片专用目录,任何落在这里的登记都是
|
||||
// 切片登记。不能再要求 `kind == Icon`——历史项目里切片刻的 kind 是非 canonical 值
|
||||
// (`art-spritesheet-slice` 等),严格解析后收口成 `unknown`,加上 kind 判据就会漏掉这些
|
||||
// 半成品登记,让下面的回填被静默跳过。
|
||||
let has_partial_or_invalid_registration = manifest.assets.iter().any(|asset| {
|
||||
asset.kind == GameCreationAppAssetKind::Icon
|
||||
&& asset
|
||||
.local_path
|
||||
.starts_with("assets/art-spritesheet-slices/")
|
||||
asset
|
||||
.local_path
|
||||
.starts_with("assets/art-spritesheet-slices/")
|
||||
});
|
||||
|
||||
let receipt_path =
|
||||
@@ -7908,6 +7911,51 @@ mod canvas_generation_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 平台请求词汇 → manifest kind 的**唯一**映射点:逐条别名、trim 与未映射值的落点都必须钉住。
|
||||
///
|
||||
/// 未映射值不能悄悄变成某个具体 kind:它走严格解析,认不出就是 `Unknown`(→「待归类」),
|
||||
/// 同时由 reporter 把原始串写进日志——这条用例保证"不猜"。
|
||||
#[test]
|
||||
fn platform_art_asset_manifest_kind_maps_platform_vocabulary_explicitly() {
|
||||
use GameCreationAppAssetKind as Kind;
|
||||
for (platform_kind, expected) in [
|
||||
("image", Kind::Image),
|
||||
("game-art", Kind::Image),
|
||||
("character", Kind::Character),
|
||||
("character-art", Kind::Character),
|
||||
("spec", Kind::IconSpec),
|
||||
("icon-spec", Kind::IconSpec),
|
||||
("ui-prototype", Kind::UiDesign),
|
||||
("ui-design", Kind::UiDesign),
|
||||
("art-spritesheet", Kind::IconSpritesheet),
|
||||
("icon-spritesheet", Kind::IconSpritesheet),
|
||||
("publication-material", Kind::PublicationMaterial),
|
||||
("game-background", Kind::Scene),
|
||||
("scene", Kind::Scene),
|
||||
// 平台词汇这一步会 trim(与 `normalize_platform_art_asset_generation_kind` 同口径)。
|
||||
(" ui-prototype ", Kind::UiDesign),
|
||||
] {
|
||||
assert_eq!(
|
||||
platform_art_asset_manifest_kind(platform_kind),
|
||||
expected,
|
||||
"{platform_kind} 的映射必须显式钉住"
|
||||
);
|
||||
}
|
||||
// canonical 成员即使不在上面的平台别名表里也照常解析(严格等值)。
|
||||
assert_eq!(
|
||||
platform_art_asset_manifest_kind("font"),
|
||||
GameCreationAppAssetKind::Font
|
||||
);
|
||||
// 认不出的词汇不猜、不兜底成具体 kind。
|
||||
for platform_kind in ["", "UI", "ui", "mystery", "unknown-kind"] {
|
||||
assert_eq!(
|
||||
platform_art_asset_manifest_kind(platform_kind),
|
||||
GameCreationAppAssetKind::Unknown,
|
||||
"{platform_kind} 不在映射表内,必须落 unknown 而不是被猜成某个 kind"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generation_kind_catalog_binds_art_spritesheet_to_a_spec_board_prompt() {
|
||||
// 放行 art-spritesheet 必须真的走到图集提示词与图集请求合同,
|
||||
|
||||
@@ -538,6 +538,22 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> GameCreationAppAsse
|
||||
}
|
||||
}
|
||||
|
||||
/// 登记边界的 kind 解析:空白入参按「没有信息」处理,落中性 `image`(→「待归类」);
|
||||
/// 非空但认不出的原值严格收口成 `Unknown`(同样「待归类」)并把原始串交给 reporter 留痕。
|
||||
///
|
||||
/// 三条外部登记边界必须同口径:平台导入(`commands::imported_platform_asset_kind`)、
|
||||
/// 画板导入(`sync_canvas_project_assets_at`)、外部 `register_local_asset`。
|
||||
/// 只做严格等值匹配——不 trim 归一、不查别名、不做迁移。
|
||||
pub(crate) fn registration_asset_kind(
|
||||
value: &str,
|
||||
context: &'static str,
|
||||
) -> GameCreationAppAssetKind {
|
||||
if value.trim().is_empty() {
|
||||
return GameCreationAppAssetKind::Image;
|
||||
}
|
||||
GameCreationAppAssetKind::parse_with_context(value, context)
|
||||
}
|
||||
|
||||
pub(crate) fn upload_local_asset_at(
|
||||
root: &Path,
|
||||
file_name: &str,
|
||||
@@ -686,7 +702,7 @@ 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 = GameCreationAppAssetKind::parse_with_context(kind.trim(), "canvas.asset_import");
|
||||
let kind = registration_asset_kind(kind, "canvas.asset_import");
|
||||
|
||||
register_local_asset_at(
|
||||
root,
|
||||
|
||||
@@ -2167,7 +2167,7 @@ pub(crate) fn register_local_asset(
|
||||
register_local_asset_at(
|
||||
root,
|
||||
local_path.trim(),
|
||||
GameCreationAppAssetKind::parse_with_context(kind.trim(), "asset.register"),
|
||||
crate::assets::registration_asset_kind(&kind, "asset.register"),
|
||||
media_type.trim(),
|
||||
source_kind.trim(),
|
||||
GameCreationAppAssetSource {
|
||||
@@ -4189,13 +4189,13 @@ pub(crate) fn import_local_project_assets_for_agent(
|
||||
///
|
||||
/// kind 只有一条口径:严格解析,不做别名归一、不做迁移(`Kind.ALL` 是唯一词汇表)。
|
||||
fn imported_platform_asset_kind(platform_kind: Option<&str>) -> GameCreationAppAssetKind {
|
||||
match platform_kind.map(str::trim).filter(|kind| !kind.is_empty()) {
|
||||
// 平台是外部系统,但 kind 口径**只有一条**:严格解析 + 非 canonical 原值留痕
|
||||
// (回调已由壳层注册成 `app_log!`,见 `main.rs`)。这里不再做别名收口,
|
||||
// 认不出的值就是 `Unknown` → 「待归类」,原值只出现在日志里。
|
||||
Some(kind) => GameCreationAppAssetKind::parse_with_context(kind, "platform.asset_kind"),
|
||||
None => GameCreationAppAssetKind::Image,
|
||||
}
|
||||
// 平台是外部系统,但 kind 口径**只有一条**:严格解析 + 非 canonical 原值留痕
|
||||
// (回调已由壳层注册成 `app_log!`,见 `main.rs`);缺失 / 空白按「没有信息」落
|
||||
// 中性 `image`。这里不再做别名收口,认不出的值就是 `Unknown` → 「待归类」,
|
||||
// 原值只出现在日志里。
|
||||
platform_kind
|
||||
.map(|kind| crate::assets::registration_asset_kind(kind, "platform.asset_kind"))
|
||||
.unwrap_or(GameCreationAppAssetKind::Image)
|
||||
}
|
||||
|
||||
/// 按账户素材 `assetId` 查询权威素材、换签下载并登记到当前项目。模型只提交
|
||||
|
||||
@@ -338,7 +338,12 @@ impl ResourceEditSourceKind {
|
||||
fn manifest_kind(self) -> GameCreationAppAssetKind {
|
||||
match self {
|
||||
Self::Manifest(kind) => kind,
|
||||
Self::ProjectVersion | Self::AgentResultText => GameCreationAppAssetKind::Unknown,
|
||||
// Agent 回执派生物是 markdown 文本,与 `Text => Document` 同口径:写盘只写正式成员,
|
||||
// 不能主动写 `Unknown`(那会把"派生物是文档"的事实换成"认不出的资源")。
|
||||
Self::AgentResultText => GameCreationAppAssetKind::Document,
|
||||
// 版本编辑不走 manifest 资产写回通道(版本有独立提交路径),此分支不可达;
|
||||
// 真走到这里也只表示"没有 manifest kind"。
|
||||
Self::ProjectVersion => GameCreationAppAssetKind::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3849,8 +3854,10 @@ pub(crate) fn normalize_local_project_raster_resource_at(
|
||||
.map_err(|error| format!("序列化源图片身份失败:{error}"))?;
|
||||
let identity_hash = sha256_hex(&identity_material);
|
||||
let asset_id = format!("normalized-{}", &identity_hash[..24]);
|
||||
// 该接口只把栅格源登记成 manifest 图片资产:源 subtype 只接受正式枚举成员,
|
||||
// 未声明或非图片族成员时按图片收口。
|
||||
// 该接口只把栅格源登记成 manifest 图片资产:源 subtype 只接受**图片族**成员,
|
||||
// 未声明、认不出或非图片族成员(音频 / 视频 / 文档 / 字体 / 代码…)时按图片收口。
|
||||
// 只排除 `Unknown` 是不够的:`source_subtype` 是外部入参,传 `background-music` 就会把
|
||||
// 一张 PNG 登记成音频 kind,和本函数的存在理由直接冲突。
|
||||
let source_kind = input
|
||||
.source_subtype
|
||||
.as_deref()
|
||||
@@ -3858,6 +3865,19 @@ pub(crate) fn normalize_local_project_raster_resource_at(
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| GameCreationAppAssetKind::parse_with_context(value, "resource-edit.normalize"))
|
||||
.filter(|kind| *kind != GameCreationAppAssetKind::Unknown)
|
||||
.filter(|kind| {
|
||||
matches!(
|
||||
kind,
|
||||
GameCreationAppAssetKind::Image
|
||||
| GameCreationAppAssetKind::Scene
|
||||
| GameCreationAppAssetKind::Character
|
||||
| GameCreationAppAssetKind::CharacterAnimation
|
||||
| GameCreationAppAssetKind::Icon
|
||||
| GameCreationAppAssetKind::IconSpritesheet
|
||||
| GameCreationAppAssetKind::IconSpec
|
||||
| GameCreationAppAssetKind::UiDesign
|
||||
)
|
||||
})
|
||||
.unwrap_or(GameCreationAppAssetKind::Image);
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: asset_id.clone(),
|
||||
@@ -6279,6 +6299,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 派生资源写回 manifest 的 kind 必须是正式成员:Agent 回执派生物是 markdown 文本,
|
||||
/// 与 `Text => Document` 同口径,不许主动写 `Unknown`。
|
||||
#[test]
|
||||
fn derived_asset_manifest_kind_is_never_unknown_for_text_derivatives() {
|
||||
assert_eq!(
|
||||
ResourceEditSourceKind::AgentResultText.manifest_kind(),
|
||||
GameCreationAppAssetKind::Document
|
||||
);
|
||||
assert_eq!(
|
||||
ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video).manifest_kind(),
|
||||
GameCreationAppAssetKind::Video
|
||||
);
|
||||
// 版本谱系没有 manifest kind,且不走资产写回通道(版本有独立提交路径)。
|
||||
assert_eq!(
|
||||
ResourceEditSourceKind::ProjectVersion.manifest_kind(),
|
||||
GameCreationAppAssetKind::Unknown
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_media_upload_ticket_uses_legal_private_editor_namespace() {
|
||||
let directory = tempfile::tempdir().expect("create resource editor fixture");
|
||||
|
||||
@@ -854,6 +854,23 @@ fn ensure_page_ui_resource(
|
||||
Ok(ui_asset)
|
||||
}
|
||||
|
||||
/// 比较 sprite 资源身份时忽略 `metadata.asset_type`。
|
||||
///
|
||||
/// `asset_type` 是随 canonical kind 派生的展示串(`SpriteAssetMetadata` 参与 `PartialEq`),
|
||||
/// 而迁移前的 State 里存的是当时的 manifest 原始 kind(`ui-prototype` / `art-spritesheet` 等)。
|
||||
/// 它参与比较会让"对既有 State 重跑工作流"误报「与已有 State 资源冲突」,所以身份判定只看
|
||||
/// 真正影响渲染的字段;展示串随 kind 派生、不构成资源身份。
|
||||
fn same_sprite_asset_identity(current: &SpriteAsset, next: &SpriteAsset) -> bool {
|
||||
if current.metadata.asset_type == next.metadata.asset_type {
|
||||
return current == next;
|
||||
}
|
||||
let mut current = current.clone();
|
||||
let mut next = next.clone();
|
||||
current.metadata.asset_type = String::new();
|
||||
next.metadata.asset_type = String::new();
|
||||
current == next
|
||||
}
|
||||
|
||||
fn install_page_component_assets(
|
||||
root: &Path,
|
||||
state: &mut crate::ui_editor::state::State,
|
||||
@@ -885,7 +902,7 @@ fn install_page_component_assets(
|
||||
};
|
||||
sprite.path = asset.local_path.clone();
|
||||
match state.sprite_assets.get(&asset_id) {
|
||||
Some(current) if current != &sprite => {
|
||||
Some(current) if !same_sprite_asset_identity(current, &sprite) => {
|
||||
return Err(format!(
|
||||
"UI 独立图片/图标 {} 与已有 State 资源冲突",
|
||||
asset.id
|
||||
|
||||
Reference in New Issue
Block a user