合并最新主线并对齐资源类型枚举更新
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m25s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m14s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m38s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m39s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m25s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m6s
Project CI / Repository checks (pull_request) Successful in 8m12s
Project CI / Native shell tests (pull_request) Successful in 10m54s
Project CI / Frontend tests (pull_request) Successful in 13m34s
Project CI / Backend tests (pull_request) Successful in 13m55s
Project CI / AI game creator shell web tests (pull_request) Successful in 8m2s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m25s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m14s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m38s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m39s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m25s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m6s
Project CI / Repository checks (pull_request) Successful in 8m12s
Project CI / Native shell tests (pull_request) Successful in 10m54s
Project CI / Frontend tests (pull_request) Successful in 13m34s
Project CI / Backend tests (pull_request) Successful in 13m55s
Project CI / AI game creator shell web tests (pull_request) Successful in 8m2s
同步 master 的资源类型枚举、共享契约与依赖锁文件更新 保留本分支的窄依赖、集中鉴权和异步追踪实现 通过路由、项目元数据、图片编辑策略和共享契约定向回归
This commit is contained in:
@@ -38,6 +38,7 @@ use shared_contracts::editor_generation::{
|
||||
editor_generation_stable_asset_id, editor_generation_stable_asset_object_id,
|
||||
editor_generation_stable_resource_id,
|
||||
};
|
||||
use shared_contracts::game_creation_app::GameCreationAppAssetKind;
|
||||
use shared_kernel::build_prefixed_uuid_id;
|
||||
use spacetime_client::editor_project::{
|
||||
EditorGenerationResultPersistItemRecordInput, EditorGenerationResultPersistRecord,
|
||||
@@ -4110,54 +4111,19 @@ fn align_editor_image_edit_dimension(value: u32) -> u32 {
|
||||
value.saturating_add(15) / 16 * 16
|
||||
}
|
||||
|
||||
/// 图片快速编辑端点允许的**静态图**来源类型。
|
||||
///
|
||||
/// 口径依据 `packages/shared/src/contracts/gameCreationApp.ts` 的
|
||||
/// `canonicalGameCreationAppAssetKind()`(等价实现见
|
||||
/// `server-rs/crates/shared-contracts/src/game_creation_app.rs`):以「媒体类型为静态图的
|
||||
/// canonical 类型」为权威集合,并额外收下合法 legacy 类型本身(平台还有别的写入口按本地
|
||||
/// manifest 原始类型登记来源资源,客户端不会先做 canonical 归一)。
|
||||
/// `canonicalGameCreationAppAssetKind()` 对未知值回退 `image`,因此两个入口都放行的这些值
|
||||
/// 在共享契约里语义等价,静态图集合对 canonical 归一封闭,不会漏放合法静态图来源。
|
||||
///
|
||||
/// 必须与 `media_type == image` 一起构成 AND 门:视频、音频、序列帧等非静态媒体即使挂着
|
||||
/// 图片类 assetKind 也照旧拒绝。
|
||||
pub(crate) const EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS: [&str; 18] = [
|
||||
// canonical 静态图类型
|
||||
"image",
|
||||
"scene",
|
||||
"character",
|
||||
"icon",
|
||||
"icon-spritesheet",
|
||||
"icon-spec",
|
||||
"ui-design",
|
||||
"publication-material",
|
||||
"spec",
|
||||
// 指向上述 canonical 类型的合法 legacy 类型
|
||||
"game-background",
|
||||
"character-art",
|
||||
"game-art",
|
||||
"illustration",
|
||||
"art-spritesheet",
|
||||
"art-spritesheet-slice",
|
||||
"ui-prototype",
|
||||
// AGC 本地 manifest 等在用的等价静态图类型
|
||||
"ui",
|
||||
// `register_local_asset_entry`(assets.rs)空 kind 兜底写的是 `"asset"`:
|
||||
// 未归类上传的 PNG 会以 `kind: "asset"` 登记,而共享契约把 `"asset"` canonical 化成
|
||||
// `"image"`,漏掉它就等于把「未归类静态图」挡在快速编辑之外(400)。
|
||||
// 白名单必须在 canonical 静态图别名下封闭。
|
||||
"asset",
|
||||
];
|
||||
|
||||
pub(crate) fn ensure_editor_image_edit_source_kind_allowed(
|
||||
asset_kind: Option<&str>,
|
||||
media_type: Option<&str>,
|
||||
) -> Result<(), AppError> {
|
||||
let asset_kind = asset_kind.map(str::trim).filter(|value| !value.is_empty());
|
||||
let asset_kind = asset_kind.filter(|value| !value.trim().is_empty());
|
||||
let media_type = media_type.map(str::trim).filter(|value| !value.is_empty());
|
||||
let asset_kind_allowed = asset_kind
|
||||
.is_none_or(|asset_kind| EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS.contains(&asset_kind));
|
||||
let asset_kind_allowed = asset_kind.is_none_or(|asset_kind| {
|
||||
GameCreationAppAssetKind::parse_with_context(
|
||||
asset_kind,
|
||||
"editor-image-edit.source.asset_kind",
|
||||
)
|
||||
.is_static_image()
|
||||
});
|
||||
let media_type_allowed = matches!(media_type, None | Some("image"));
|
||||
if asset_kind_allowed && media_type_allowed {
|
||||
return Ok(());
|
||||
@@ -20769,7 +20735,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_image_edit_source_kind_whitelist_covers_static_image_kinds_only() {
|
||||
fn editor_image_edit_source_kind_policy_covers_static_image_kinds_only() {
|
||||
let cases = [
|
||||
(None, None, true),
|
||||
(None, Some("image"), true),
|
||||
@@ -20782,16 +20748,6 @@ mod tests {
|
||||
(Some("publication-material"), Some("image"), true),
|
||||
(Some("ui-design"), Some("image"), true),
|
||||
(Some("scene"), Some("image"), true),
|
||||
// `canonicalGameCreationAppAssetKind()` 的合法输入:平台按本地 manifest 原始类型
|
||||
// 登记来源资源时会出现这些 legacy 值,实测 `art-spritesheet` / `ui` 曾整片 400。
|
||||
(Some("art-spritesheet"), Some("image"), true),
|
||||
(Some("art-spritesheet-slice"), Some("image"), true),
|
||||
(Some("ui"), Some("image"), true),
|
||||
(Some("ui-prototype"), Some("image"), true),
|
||||
(Some("game-art"), Some("image"), true),
|
||||
(Some("game-background"), Some("image"), true),
|
||||
(Some("character-art"), Some("image"), true),
|
||||
(Some("illustration"), Some("image"), true),
|
||||
// 非静态媒体与非静态类型必须继续拒绝。
|
||||
(Some("character-animation"), Some("image-sequence"), false),
|
||||
(Some("sound-effect"), Some("audio"), false),
|
||||
@@ -20803,8 +20759,6 @@ mod tests {
|
||||
(Some("future-asset-kind"), Some("image"), false),
|
||||
(None, Some("future-media-type"), false),
|
||||
// mediaType 是 AND 门:图片类 assetKind 挂非静态媒体照旧拒绝。
|
||||
(Some("art-spritesheet"), Some("video"), false),
|
||||
(Some("ui"), Some("audio"), false),
|
||||
(Some("icon"), Some("image-sequence"), false),
|
||||
(Some("scene"), Some("image-sequence"), false),
|
||||
];
|
||||
@@ -20813,13 +20767,13 @@ mod tests {
|
||||
assert_eq!(
|
||||
result.is_ok(),
|
||||
expected_allowed,
|
||||
"unexpected whitelist result for assetKind={asset_kind:?}, mediaType={media_type:?}",
|
||||
"unexpected source-kind policy result for assetKind={asset_kind:?}, mediaType={media_type:?}",
|
||||
);
|
||||
}
|
||||
|
||||
let error =
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("art-spritesheet"), Some("video"))
|
||||
.expect_err("非静态媒体即使挂着图片类 assetKind 也不得支持快速编辑");
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image"))
|
||||
.expect_err("未知 kind 不得作为快速编辑来源");
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("message")),
|
||||
@@ -20932,10 +20886,6 @@ mod tests {
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("icon"), Some("image")).is_ok(),
|
||||
"icon 是共享契约里的静态图 canonical 类型,与 generationInputs.source 无关"
|
||||
);
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("art-spritesheet"), Some("image"))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(ensure_editor_image_edit_source_kind_allowed(Some("icon"), Some("video")).is_err());
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image"))
|
||||
@@ -20943,132 +20893,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 实测 400×5 的回归用例:AGC「图片快速编辑」发出的请求,来源资源的 assetKind 是本地
|
||||
/// manifest 原始类型 `art-spritesheet` / `ui` / `asset`(mediaType=image)。这条路径必须
|
||||
/// 放行;非静态媒体(video/audio/image-sequence)必须继续拒绝。
|
||||
///
|
||||
/// 夹具必须真的走进被测路径,不能是自洽空断言:请求体先反序列化成
|
||||
/// `EditorImageEditRequest`,再从**请求体自己的** `generationInputs.assetKind` 取类型喂给
|
||||
/// 放行判据;并用源码级闸门断言钉住
|
||||
/// `resolve_editor_image_edit_source` / `ensure_editor_image_edit_target_matches_source`
|
||||
/// 两个函数体内不得再出现 `generation_inputs`(历史上 `generationInputs.source` 参与过
|
||||
/// gating,把 AGC 请求整体挡成 400;解析与对账需要 DB/OSS,源码级断言是无需基础设施
|
||||
/// 也能钉住这条回归的唯一位置)。
|
||||
#[test]
|
||||
fn game_creator_client_quick_edit_accepts_local_manifest_static_image_kinds() {
|
||||
// 与客户端 `submit_resource_edit_remote` 发送的 JSON 同形。
|
||||
let agc_payload: EditorImageEditRequest = serde_json::from_str(
|
||||
r#"{
|
||||
"prompt": "把这张图改成夜间配色",
|
||||
"sourceReferenceId": "resource-art-spritesheet",
|
||||
"generationInputs": {
|
||||
"source": "ai-game-creator-client",
|
||||
"assetKind": "art-spritesheet"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("AGC quick edit payload should deserialize into the request DTO");
|
||||
assert_eq!(
|
||||
agc_payload.source_reference_id, "resource-art-spritesheet",
|
||||
"回归夹具必须保持 AGC 客户端实际发送的请求形状"
|
||||
);
|
||||
assert_eq!(
|
||||
agc_payload.target_layer_id, None,
|
||||
"AGC 快速编辑不带 targetLayerId,走的是「主来源即目标」分支"
|
||||
);
|
||||
// 关键:放行判据只吃来源资源的权威 assetKind 与 mediaType。这里用请求体自己声明的
|
||||
// assetKind 驱动判据——请求体里同时带着 `generationInputs.source`,它不得参与 gating。
|
||||
let requested_kind = agc_payload
|
||||
.generation_inputs
|
||||
.as_ref()
|
||||
.and_then(|inputs| inputs.pointer("/assetKind"))
|
||||
.and_then(Value::as_str);
|
||||
assert_eq!(
|
||||
requested_kind,
|
||||
Some("art-spritesheet"),
|
||||
"回归夹具必须保持 AGC 客户端实际发送的请求形状"
|
||||
);
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(
|
||||
normalize_editor_image_edit_resolved_source_kind(requested_kind).as_deref(),
|
||||
Some("image"),
|
||||
)
|
||||
.is_ok(),
|
||||
"AGC 快速编辑请求不得因为 generationInputs.source 被拒"
|
||||
);
|
||||
|
||||
// 源码级闸门:请求解析与目标对账都不得重新引入 generationInputs 门槛。
|
||||
let source = include_str!("editor_project.rs");
|
||||
assert_function_not_contains(
|
||||
source,
|
||||
"async fn resolve_editor_image_edit_source(",
|
||||
"fn ensure_editor_image_edit_source_snapshot_matches(",
|
||||
&["generation_inputs", "generationInputs"],
|
||||
);
|
||||
assert_function_not_contains(
|
||||
source,
|
||||
"fn ensure_editor_image_edit_target_matches_source(",
|
||||
"async fn resolve_editor_image_edit_source(",
|
||||
&["generation_inputs", "generationInputs"],
|
||||
);
|
||||
|
||||
for (asset_kind, media_type) in [
|
||||
("art-spritesheet", "image"),
|
||||
("ui", "image"),
|
||||
("asset", "image"),
|
||||
("ui-prototype", "image"),
|
||||
("game-art", "image"),
|
||||
("game-background", "image"),
|
||||
("icon", "image"),
|
||||
] {
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(Some(asset_kind), Some(media_type))
|
||||
.is_ok(),
|
||||
"AGC 快速编辑必须放行 {asset_kind}/{media_type}"
|
||||
);
|
||||
}
|
||||
|
||||
for (asset_kind, media_type) in [
|
||||
("art-spritesheet", "video"),
|
||||
("ui", "audio"),
|
||||
("asset", "video"),
|
||||
("video", "video"),
|
||||
("sound-effect", "audio"),
|
||||
("background-music", "audio"),
|
||||
("character-animation", "image-sequence"),
|
||||
("future-kind", "image"),
|
||||
] {
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(Some(asset_kind), Some(media_type))
|
||||
.is_err(),
|
||||
"非静态来源必须继续拒绝:{asset_kind}/{media_type}"
|
||||
);
|
||||
}
|
||||
|
||||
// 400 错误体必须带上真实原因,客户端才能诊断(对应 AGC 的
|
||||
// `editor_api_rejection_reason` 取值顺序修正)。
|
||||
let error =
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image"))
|
||||
.expect_err("未知类型必须拒绝");
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("provider")),
|
||||
Some(&json!("editor-image-edit"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("message")),
|
||||
Some(&json!("当前素材类型不支持图片快速编辑"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("assetKind")),
|
||||
Some(&json!("future-kind"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("mediaType")),
|
||||
Some(&json!("image"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_image_edit_target_binding_prefers_object_id_and_fails_closed() {
|
||||
let target = EditorImageEditTargetLayerSource {
|
||||
|
||||
Reference in New Issue
Block a user