拒绝栅格资源 subtype 静默降级
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (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 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled

非图片族和未知 sourceSubtype 改为明确报错
补充栅格正规化回归测试并同步规范
This commit is contained in:
2026-09-17 23:06:30 +08:00
parent 3d4c7929c3
commit 72cd0747a4
2 changed files with 41 additions and 17 deletions
@@ -3872,19 +3872,25 @@ 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 只接受**图片族**成员
// 未声明认不出或非图片族成员(音频 / 视频 / 文档 / 字体 / 代码…)时按图片收口。
// 只排除 `Unknown` 是不够的:`source_subtype` 是外部入参,传 `background-music` 就会把
// 一张 PNG 登记成音频 kind,和本函数的存在理由直接冲突。
let source_kind = input
// 该接口只把栅格源登记成 manifest 图片资产:源 subtype 只接受**图片族**成员
// 未声明/空白时按没有类型信息处理为 `image`;认不出或传入非图片族成员
// (音频 / 视频 / 文档 / 字体 / 代码…)直接失败,避免静默掩盖调用方错误。
let source_kind = match input
.source_subtype
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| GameCreationAppAssetKind::parse_with_context(value, "resource-edit.normalize"))
.filter(|kind| *kind != GameCreationAppAssetKind::Unknown)
.filter(|kind| {
matches!(
{
None => GameCreationAppAssetKind::Image,
Some(value) => {
let kind =
GameCreationAppAssetKind::parse_with_context(value, "resource-edit.normalize");
if kind == GameCreationAppAssetKind::Unknown {
return Err(format!(
"sourceSubtype 必须是图片族 kind,无法识别:{value}"
));
}
if !matches!(
kind,
GameCreationAppAssetKind::Image
| GameCreationAppAssetKind::Scene
@@ -3894,9 +3900,15 @@ pub(crate) fn normalize_local_project_raster_resource_at(
| GameCreationAppAssetKind::IconSpritesheet
| GameCreationAppAssetKind::IconSpec
| GameCreationAppAssetKind::UiDesign
)
})
.unwrap_or(GameCreationAppAssetKind::Image);
) {
return Err(format!(
"sourceSubtype 必须是图片族 kind,不能使用:{}",
kind.as_str()
));
}
kind
}
};
let asset = GameCreationAppAssetManifestEntry {
id: asset_id.clone(),
kind: source_kind,
@@ -9112,7 +9124,7 @@ mod tests {
source_resource_id: format!("task:{task_id}:assets/task-hero.png"),
source_path: "assets/task-hero.png".to_string(),
source_media_type: "image/png".to_string(),
source_subtype: Some("task-artifact".to_string()),
source_subtype: None,
producer_task_id: task_id.clone(),
};
@@ -9144,6 +9156,18 @@ mod tests {
assert_eq!(replay.asset.id, first.asset.id);
assert_eq!(replay.manifest.assets.len(), 1);
for invalid_subtype in ["background-music", "legacy-raster-kind"] {
let error = normalize_local_project_raster_resource_at(
NormalizeLocalProjectRasterResourceInput {
expected_project_revision: replay.committed_project_revision,
source_subtype: Some(invalid_subtype.to_string()),
..request.clone()
},
)
.expect_err("non-image or unknown source subtype must fail");
assert!(error.contains("sourceSubtype 必须是图片族 kind"), "{error}");
}
let rejected =
normalize_local_project_raster_resource_at(NormalizeLocalProjectRasterResourceInput {
expected_project_revision: replay.committed_project_revision,
@@ -9181,7 +9205,7 @@ mod tests {
source_resource_id: format!("task:{task_id}:assets/task-hero.png"),
source_path: "assets/task-hero.png".to_string(),
source_media_type: "image/png".to_string(),
source_subtype: Some("task-artifact".to_string()),
source_subtype: None,
producer_task_id: task_id,
};
let first = normalize_local_project_raster_resource_at(request.clone())
@@ -9244,7 +9268,7 @@ mod tests {
source_resource_id: format!("task:{task_id}:assets/task-hero.png"),
source_path: "assets/task-hero.png".to_string(),
source_media_type: "image/png".to_string(),
source_subtype: Some("task-artifact".to_string()),
source_subtype: None,
producer_task_id: task_id,
};
let first = normalize_local_project_raster_resource_at(request.clone())
@@ -8,14 +8,14 @@
- **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 必须与内容相符**`normalize_local_project_raster_resource_at` 的源 subtype 只接受图片族成员(`image / scene / character / character-animation / icon / icon-spritesheet / icon-spec / ui-design`,音频、视频、文档、字体、代码等一律按 `image` 登记——`source_subtype` 是公开入参,只排除 `Unknown` 会让一张 PNG 被登记成 `background-music`。派生资源写回 manifest 时不得主动写 `Unknown`Agent 回执派生物是 markdown 文本,按 `Text => Document` 同口径落 `document`
- **登记内容的 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 资源冲突」。
- **TS 侧判据也只留一份**`isGameCreationAppUiDesignDocAsset(asset)` 是「`kind == ui-design-doc``mediaType == application/json`」的唯一定义,资源画布入口、UI 编辑器桥接、资源引用缩略图三处统一调用它。
- **严格口径的已知代价**:既有项目 manifest 里若存的是 legacy kind`"UI"``"ui-prototype"``"art-spritesheet"``"game-background"`…),读入后就是 `unknown`,依赖 kind 等值比较的运行门禁会按「缺少该资源」处理。这是「不迁移、不 fallback」的必然结果,由项目决策接受;若将来要迁就存量数据,必须单独立项(一次性迁移或读侧白名单),不得把别名表加回解析路径。
- **留痕走 `app_log!`**`shared-contracts` 不再依赖 `tracing``kind-observability` feature 删除),改为暴露可注册回调 `set_non_canonical_asset_kind_reporter()`AGC 壳在 `main()` 里注册成 `app_log!`,日志同时含**原始输入串**与调用上下文,用于回查还有谁在写 legacy kind。TS 侧对应 `parseGameCreationAppAssetKind()``console.warn`
- **分类映射**`GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND` 改为按 `GameCreationAppAssetKind` 变体穷举(含 `unknown → unclassified`),与 `GameCreationAppAssetKind::ALL` 的对齐由单测 `asset_category_mapping_covers_every_kind` 守住;TS 侧同表按生成 union 穷举。
- **平台/画板词汇表**`AGENT_RUNTIME_CANVAS_ASSET_KINDS``platform_art_asset_manifest_kind()` 仍是「画板词汇 → manifest kind」的唯一映射点(保留),但认不出的原值改为留痕收口;对外 API 的 `EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS` 白名单保持原样,属 API 兼容面,不代表客户端 kind 归一规则。
- **画布生成 kind 共用枚举**`canvas.asset_generate``assetKind` 白名单直接复用 `GameCreationAppAssetKind::CANVAS_ASSET_KINDS`(当前为 `image / icon-spec / ui-design / icon-spritesheet`),工具 schema、prompt 和执行前校验均从该枚举派生,不再维护 `&[&str]` 字符串目录。平台适配层若仍需要旧的外部请求字面值,只能在发送边界显式映射;manifest 始终写入枚举的 canonical 值。对外 API 的 `EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS` 白名单保持原样,属 API 兼容面,不代表客户端 kind 归一规则。
- **版本素材替换**`subtypeEqual` 改为枚举相等(不再经别名字符串归一),`categoryEqual` 仍用 `game_creation_app_asset_effective_category` 的读时口径。
## 2026-09-15 GameCreationApp 资源 kind 枚举化(当前权威口径)