AGC 壳把资源 kind 留痕接到 app_log! 并把各解析边界统一为严格口径
- main.rs:新增 register_non_canonical_asset_kind_reporter(),把 shared-contracts 的原始串与上下文落到 app_log!,按 (原始串, 上下文) 去重;main() 启动时注册。 - commands.rs:imported_platform_asset_kind 改为直接 parse_with_context,不再先经别名表收口;用例改为断言 UI / ui / mystery / game-art / ui-prototype 等非 canonical 值一律落 unknown。 - assets.rs:画板/上传等用例的 kind 断言改用 GameCreationAppAssetKind::ALL 穷举口径。 - project/manifest.rs:读 manifest 的留痕去重键由「只有原始 kind」改成「原始 kind + 资产身份」,避免第二个写旧值的资产被静默吞掉。 - project/manifest.rs:from_str_lossy 调用点改名 from_str_or_unknown。 - agent/generation/canvas_generation.rs:画板 assetKind 的兜底分支改为 parse_with_context,非 canonical 值留痕收口为 unknown。 - project/version_resource_replacement.rs:subtypeEqual 改为 canonical kind 直接比较,去掉中间归一。
This commit is contained in:
@@ -490,7 +490,10 @@ pub(crate) fn platform_art_asset_manifest_kind(asset_kind: &str) -> GameCreation
|
||||
"art-spritesheet" | "icon-spritesheet" => GameCreationAppAssetKind::IconSpritesheet,
|
||||
"publication-material" => GameCreationAppAssetKind::PublicationMaterial,
|
||||
"game-background" | "scene" => GameCreationAppAssetKind::Scene,
|
||||
_ => GameCreationAppAssetKind::Unknown,
|
||||
// 画板/平台词汇表(`AGENT_RUNTIME_CANVAS_ASSET_KINDS`)到 manifest kind 的**唯一**
|
||||
// 映射点:认不出的原值收口成 `Unknown` 并留痕(回调接到壳层 `app_log!`),
|
||||
// manifest 侧不再有第二张别名表。
|
||||
_ => GameCreationAppAssetKind::parse_with_context(asset_kind, "canvas.asset_kind"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2235,8 +2235,7 @@ mod tests {
|
||||
for layer_type in ["序列", "音频", "角色", "场景", "UI", "图层", "其他"] {
|
||||
let kind = infer_canvas_export_asset_kind(&layer(layer_type), "layer.png");
|
||||
assert!(
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_CANONICAL_ASSET_KINDS
|
||||
.contains(&kind.as_str()),
|
||||
GameCreationAppAssetKind::ALL.contains(&kind),
|
||||
"非 canonical kind: {kind}(layer_type={layer_type})"
|
||||
);
|
||||
for legacy in ["ui", "animation", "asset"] {
|
||||
@@ -2276,12 +2275,7 @@ mod tests {
|
||||
assert_ne!(kind, "uploaded", "{file_name} 写回了来源词");
|
||||
assert_ne!(kind, "ui", "{file_name} 写回了非 canonical 的 `ui`");
|
||||
assert!(
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_CANONICAL_ASSET_KINDS
|
||||
.contains(&kind.as_str())
|
||||
|| matches!(
|
||||
kind,
|
||||
GameCreationAppAssetKind::Font | GameCreationAppAssetKind::Unknown
|
||||
),
|
||||
GameCreationAppAssetKind::ALL.contains(&kind),
|
||||
"非 canonical kind: {kind}({file_name})"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3245,8 +3245,11 @@ mod agent_asset_import_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 平台导入(账户素材库 / 网页项目画布)落盘的 `kind`:**有真实类型就用真实类型**,
|
||||
/// 判不出才退回中性 `image`(→ 「待归类」),**永远不许回退成常量 `ui`**。
|
||||
/// 平台导入(账户素材库 / 网页项目画布)落盘的 `kind`:**只接受 canonical 值**,
|
||||
/// 认不出的原值收口成 `unknown`(→ 「待归类」)并留痕原始串,**永远不许回退成常量 `ui`**。
|
||||
///
|
||||
/// kind 没有别名表:平台侧写 `"UI"` / `"ui"` / `"mystery"` 时判据是"这不是合法 kind",
|
||||
/// 而不是"帮它归一成 `ui-design`";缺失/空白才退回中性的 `image`。
|
||||
///
|
||||
/// 变异验证:把 `imported_platform_asset_kind` 改成忽略入参、返回 `"ui"` 的实现,
|
||||
/// 本用例必须变红(`character` / `scene` / 空值 / 未知值四组断言都会失败)。
|
||||
@@ -3266,20 +3269,31 @@ mod agent_asset_import_tests {
|
||||
imported_platform_asset_kind(Some("icon-spec")),
|
||||
Kind::IconSpec
|
||||
);
|
||||
// 平台确实说它是 UI 设计稿时,才允许落 `ui-design`(→ 「UI 交互」);
|
||||
// 大写 `UI` 与旧值 `ui` 都要归一到同一口径,但**不允许由我们替它默认**。
|
||||
assert_eq!(imported_platform_asset_kind(Some("UI")), Kind::UiDesign);
|
||||
assert_eq!(imported_platform_asset_kind(Some("ui")), Kind::UiDesign);
|
||||
// 缺失 / 空白 / 未知值一律落 canonical `image` 兜底(→ 「待归类」)。
|
||||
// 平台确实说它是 UI 设计稿时(`ui-design`)才落 `UiDesign`(→ 「UI 交互」)。
|
||||
assert_eq!(
|
||||
imported_platform_asset_kind(Some("ui-design")),
|
||||
Kind::UiDesign
|
||||
);
|
||||
// 缺失 / 空白退回中性的 `image`(→ 「待归类」)。
|
||||
assert_eq!(imported_platform_asset_kind(None), Kind::Image);
|
||||
assert_eq!(imported_platform_asset_kind(Some(" ")), Kind::Image);
|
||||
assert_eq!(imported_platform_asset_kind(Some("mystery")), Kind::Image);
|
||||
// `kind` 是外部输入,原型链键必须走 `image` 兜底,不能取到原型上的值。
|
||||
assert_eq!(imported_platform_asset_kind(Some("__proto__")), Kind::Image);
|
||||
assert_eq!(
|
||||
imported_platform_asset_kind(Some("constructor")),
|
||||
Kind::Image
|
||||
);
|
||||
// 非 canonical 原值(含大小写变体、旧别名与原型链键)一律收口成 `unknown`:
|
||||
// 判据落在「这不是合法 kind」,原值只出现在留痕日志里。
|
||||
for raw in [
|
||||
"UI",
|
||||
"ui",
|
||||
"mystery",
|
||||
"game-art",
|
||||
"ui-prototype",
|
||||
"__proto__",
|
||||
"constructor",
|
||||
] {
|
||||
assert_eq!(
|
||||
imported_platform_asset_kind(Some(raw)),
|
||||
Kind::Unknown,
|
||||
"{raw} 不是 canonical kind,不得被归一"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 反查门禁:两处平台素材导入必须继续用 `imported_platform_asset_kind` 解析 `kind`,
|
||||
@@ -4164,30 +4178,22 @@ pub(crate) fn import_local_project_assets_for_agent(
|
||||
}
|
||||
|
||||
/// 平台素材导入落盘的 manifest `kind`:**有真实类型就用真实类型**,判不出才退回中性的
|
||||
/// `image`(派生 `unclassified` → 「待归类」)。
|
||||
/// `image`(派生 `unclassified` → 「待归类」),认不出的原值落 `unknown`(同样「待归类」)
|
||||
/// 并在日志里留下原始串。
|
||||
///
|
||||
/// 这里绝不允许回退成 `ui`:账户素材库记录与网页项目画布响应本来就带 `assetKind`
|
||||
/// (`AgentEditorAssetRecord::asset_kind` / 响应字段 `assetKind`),把它换成常量等于丢掉
|
||||
/// 唯一一条"这东西是什么"的事实,并把角色立绘、怪物、道具、场景图一并钉进「UI 交互」栏
|
||||
/// (`ui → ui-design → ui-interaction`)。落盘 `category` 一旦是非 `unclassified` 值,
|
||||
/// 读时自愈永远救不回来,所以宁可写 `image`(待归类)也不能写假 `ui`。
|
||||
///
|
||||
/// kind 只有一条口径:严格解析,不做别名归一、不做迁移(`Kind.ALL` 是唯一词汇表)。
|
||||
fn imported_platform_asset_kind(platform_kind: Option<&str>) -> GameCreationAppAssetKind {
|
||||
match platform_kind.map(str::trim).filter(|kind| !kind.is_empty()) {
|
||||
// 平台是外部系统,它的 `assetKind` 词汇(含大写 `UI` 这类变体)由共享契约统一收口到
|
||||
// canonical 值;这里只把收口结果转成正式枚举,写入侧不再产出裸字符串。
|
||||
Some(kind) => {
|
||||
let canonical =
|
||||
shared_contracts::game_creation_app::canonical_game_creation_app_asset_kind(kind);
|
||||
if canonical != kind {
|
||||
// 收口会抹掉原值,而 kind 本身是严格解析(不 fallback、不迁移),
|
||||
// 所以非 canonical 的原值必须在这里留痕:这是后续定位"平台侧谁还在写
|
||||
// legacy kind"的唯一线索。
|
||||
app_log!(
|
||||
"平台 assetKind 不是 canonical 值,已按共享契约收口:rawKind={kind} canonicalKind={canonical}"
|
||||
);
|
||||
}
|
||||
GameCreationAppAssetKind::parse_with_context(canonical, "platform.asset_kind")
|
||||
}
|
||||
// 平台是外部系统,但 kind 口径**只有一条**:严格解析 + 非 canonical 原值留痕
|
||||
// (回调已由壳层注册成 `app_log!`,见 `main.rs`)。这里不再做别名收口,
|
||||
// 认不出的值就是 `Unknown` → 「待归类」,原值只出现在日志里。
|
||||
Some(kind) => GameCreationAppAssetKind::parse_with_context(kind, "platform.asset_kind"),
|
||||
None => GameCreationAppAssetKind::Image,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +240,36 @@ macro_rules! app_log {
|
||||
}};
|
||||
}
|
||||
|
||||
/// 把 `shared-contracts` 的「非 canonical 资源 kind」留痕接到壳层日志上。
|
||||
///
|
||||
/// 解析边界(含 manifest 反序列化、画板 assetKind、平台 assetKind)认不出 canonical 值时,
|
||||
/// 会把**原始输入串**与调用上下文交回来;这里落到 `app_log!` → AppData 日志里,
|
||||
/// 以便回查"还有谁在写 legacy kind"。kind 本身只接受严格值,不做别名、不做迁移。
|
||||
///
|
||||
/// 按 `(原始串, 上下文)` 去重:manifest 读取极频繁,每次都落一行会把其它诊断刷掉;
|
||||
/// 去重后"有哪些 legacy 值、分别从哪个边界进来"仍然完整。需要具体资产身份时看
|
||||
/// `log_non_canonical_manifest_asset_kinds`(按资产去重,带 assetId / localPath)。
|
||||
fn register_non_canonical_asset_kind_reporter() {
|
||||
static REPORTED: std::sync::OnceLock<std::sync::Mutex<std::collections::HashSet<String>>> =
|
||||
std::sync::OnceLock::new();
|
||||
shared_contracts::game_creation_app::set_non_canonical_asset_kind_reporter(
|
||||
|raw_kind: &str, context: &str| {
|
||||
let key = format!("{context}\u{1}{raw_kind}");
|
||||
let first_seen = REPORTED
|
||||
.get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()))
|
||||
.lock()
|
||||
.map(|mut reported| reported.insert(key))
|
||||
.unwrap_or(true);
|
||||
if !first_seen {
|
||||
return;
|
||||
}
|
||||
app_log!(
|
||||
"GameCreationApp 资源 kind 不是 canonical 值,已按严格口径收口为 unknown:rawKind={raw_kind} context={context}"
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 调试落盘模块(保存 LLM 原始输出 / 失败输入,排查截断、空返回等)放在 debug_drafts.rs。
|
||||
// 用 #[cfg] 编译期门控:仅开发(debug)且非测试构建编入;生产 release 与 cargo test 下整体剔除。
|
||||
mod agent;
|
||||
@@ -2428,6 +2458,7 @@ mod async_runtime_stack_tests {
|
||||
#[cfg(not(test))]
|
||||
fn main() {
|
||||
install_agent_runtime_async_runtime_with_deep_stack();
|
||||
register_non_canonical_asset_kind_reporter();
|
||||
let mut args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
if let Some(exit_code) = run_direct_tools_mcp_if_requested(&args) {
|
||||
std::process::exit(exit_code);
|
||||
|
||||
@@ -1951,14 +1951,18 @@ pub(crate) fn read_manifest(path: &Path) -> Result<GameCreationAppManifest, Stri
|
||||
/// 就丢了,所以这里在读到的原始 JSON 里回捞一遍,把原值连同资产 ID、路径写进应用日志
|
||||
/// (`diagnostics/application.log`),供后续定位"谁还在写 legacy kind"。
|
||||
///
|
||||
/// 同一个原值每次进程只记一次:manifest 读得很频繁,不去重会把其它诊断刷掉。
|
||||
/// 同一份 manifest 里的同一个资产每次进程只记一次:manifest 读得很频繁,不去重会把其它诊断
|
||||
/// 刷掉。去重键**含资产身份**而不是只有原值——只按原值去重会让"第二个还在写 legacy kind 的
|
||||
/// 资产"静默消失(包括另一份项目文件里的资产),那就丢了这条日志唯一的增量信息。
|
||||
fn log_non_canonical_manifest_asset_kinds(payload: &str, source_path: &Path, label: &str) {
|
||||
static LOGGED_RAW_KINDS: OnceLock<Mutex<std::collections::HashSet<String>>> = OnceLock::new();
|
||||
static LOGGED_OFFENDERS: OnceLock<Mutex<std::collections::HashSet<String>>> = OnceLock::new();
|
||||
for (raw_kind, asset_id, local_path) in non_canonical_manifest_asset_kinds(payload) {
|
||||
let logged = LOGGED_RAW_KINDS.get_or_init(|| Mutex::new(std::collections::HashSet::new()));
|
||||
let logged = LOGGED_OFFENDERS.get_or_init(|| Mutex::new(std::collections::HashSet::new()));
|
||||
let first_seen = logged
|
||||
.lock()
|
||||
.map(|mut logged| logged.insert(raw_kind.clone()))
|
||||
.map(|mut logged| {
|
||||
logged.insert(format!("{}\u{1}{asset_id}\u{1}{local_path}", raw_kind))
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if !first_seen {
|
||||
continue;
|
||||
@@ -1985,7 +1989,7 @@ fn non_canonical_manifest_asset_kinds(payload: &str) -> Vec<(String, String, Str
|
||||
continue;
|
||||
};
|
||||
if raw_kind.trim() == GameCreationAppAssetKind::Unknown.as_str()
|
||||
|| GameCreationAppAssetKind::from_str_lossy(raw_kind)
|
||||
|| GameCreationAppAssetKind::from_str_or_unknown(raw_kind)
|
||||
!= GameCreationAppAssetKind::Unknown
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
use shared_contracts::game_creation_app::{
|
||||
canonical_game_creation_app_asset_kind, game_creation_app_asset_effective_category,
|
||||
GameCreationAppAssetManifestEntry,
|
||||
game_creation_app_asset_effective_category, GameCreationAppAssetManifestEntry,
|
||||
};
|
||||
|
||||
/// 版本级资源替换(**直接替换**口径)。
|
||||
@@ -208,7 +207,8 @@ fn version_resource_size_spec_equal(
|
||||
/// 三项兼容性判据(后端是权威判据,前端只做呈现)。
|
||||
///
|
||||
/// - `categoryEqual`:功能分类相等,用**读时自愈**口径(PRD §5.3「分类取值优先级」收口);
|
||||
/// - `subtypeEqual`:canonical `kind` 相等(别名表在 `shared-contracts`)——以上两项是硬门禁;
|
||||
/// - `subtypeEqual`:`kind` 严格相等(kind 没有别名表,非 canonical 原值一律收口成
|
||||
/// `unknown`)——以上两项是硬门禁;
|
||||
/// - `sizeSpecEqual`:见 [`version_resource_size_spec_equal`],只作提示。
|
||||
fn version_resource_compatibility(
|
||||
source: &GameCreationAppAssetManifestEntry,
|
||||
@@ -217,8 +217,7 @@ fn version_resource_compatibility(
|
||||
ProjectVersionResourceCompatibility {
|
||||
category_equal: game_creation_app_asset_effective_category(&source.kind, source.category)
|
||||
== game_creation_app_asset_effective_category(&replacement.kind, replacement.category),
|
||||
subtype_equal: canonical_game_creation_app_asset_kind(&source.kind)
|
||||
== canonical_game_creation_app_asset_kind(&replacement.kind),
|
||||
subtype_equal: source.kind == replacement.kind,
|
||||
size_spec_equal: version_resource_size_spec_equal(source, replacement),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user