diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 698347ba6..c6d594f7b 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -4163,37 +4163,6 @@ pub(crate) fn ensure_editor_image_edit_source_kind_allowed( ) } -fn is_game_creator_resource_editor_generation(generation_inputs: Option<&Value>) -> bool { - generation_inputs - .and_then(|value| value.pointer("/source")) - .and_then(Value::as_str) - .is_some_and(|source| { - // 中文注释:AGC 客户端在发送前会把 source 改写成 `ai-game-creator-client`, - // 两个 source 是同一个 consumer 的别名(见 `from_job` 的 queue consumer 判定), - // 这里必须同口径,否则客户端实际发出的请求永远走不到 AGC 分支。 - matches!( - source.trim(), - GAME_CREATOR_RESOURCE_EDITOR_SOURCE | GAME_CREATOR_CLIENT_GENERATION_SOURCE - ) - }) -} - -fn ensure_editor_image_edit_source_kind_allowed_for_request( - asset_kind: Option<&str>, - media_type: Option<&str>, - generation_inputs: Option<&Value>, -) -> Result<(), AppError> { - let asset_kind = asset_kind.map(str::trim).filter(|value| !value.is_empty()); - let media_type = media_type.map(str::trim).filter(|value| !value.is_empty()); - if is_game_creator_resource_editor_generation(generation_inputs) - && asset_kind == Some("icon") - && matches!(media_type, None | Some("image")) - { - return Ok(()); - } - ensure_editor_image_edit_source_kind_allowed(asset_kind, media_type) -} - pub(crate) const EDITOR_IMAGE_EDIT_QUEUE_PAYLOAD_VERSION: u32 = 1; #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -4379,10 +4348,9 @@ fn ensure_editor_image_edit_target_matches_source( })), ); } - ensure_editor_image_edit_source_kind_allowed_for_request( + ensure_editor_image_edit_source_kind_allowed( target.effective_asset_kind.as_deref(), Some(target.media_type.as_str()), - generation_inputs, ) } @@ -4524,11 +4492,7 @@ async fn resolve_editor_image_edit_source( .map(str::trim) .filter(|value| !value.is_empty()) else { - ensure_editor_image_edit_source_kind_allowed_for_request( - source_asset_kind.as_deref(), - Some("image"), - payload.generation_inputs.as_ref(), - )?; + ensure_editor_image_edit_source_kind_allowed(source_asset_kind.as_deref(), Some("image"))?; return Ok(EditorImageEditResolvedSource { reference_kind, source_reference_id: source_reference_id.to_string(), @@ -20759,96 +20723,29 @@ mod tests { } #[test] - fn game_creator_resource_editor_bypasses_quick_edit_whitelist_only_for_its_source() { - let generation_inputs = json!({ "source": GAME_CREATOR_RESOURCE_EDITOR_SOURCE }); + fn quick_edit_source_kind_policy_ignores_generation_source() { assert!( - ensure_editor_image_edit_source_kind_allowed_for_request( - Some("icon"), - Some("image"), - Some(&generation_inputs), - ) - .is_ok() + 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_for_request( - Some("icon"), - Some("image"), - Some(&json!({ "source": "editor-agent" })), - ) - .is_ok(), - "icon 已是共享契约里的静态图 canonical 类型,与 generationInputs.source 无关" + 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_for_request( - Some("icon"), - Some("video"), - Some(&generation_inputs), - ) - .is_err() - ); - assert!( - ensure_editor_image_edit_source_kind_allowed_for_request( - Some("future-kind"), - Some("image"), - Some(&generation_inputs), - ) - .is_err() + ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image")) + .is_err() ); } - /// AGC 客户端发送前把 `generationInputs.source` 改写成 - /// `ai-game-creator-client`(`resource_editor.rs` 的 `submit_resource_edit_remote`), - /// 服务端必须按队列 consumer 的同口径把它认成同一个 AGC consumer,否则 AGC 分支是死代码。 - #[test] - fn game_creator_client_source_is_recognized_as_game_creator_resource_editor() { - assert!(is_game_creator_resource_editor_generation(Some(&json!({ - "source": GAME_CREATOR_CLIENT_GENERATION_SOURCE - })))); - assert!(is_game_creator_resource_editor_generation(Some(&json!({ - "source": GAME_CREATOR_RESOURCE_EDITOR_SOURCE - })))); - assert!( - is_game_creator_resource_editor_generation(Some(&json!({ - "source": " game-creator-resource-editor " - }))), - "别名判定沿用 trim 口径" - ); - assert!(!is_game_creator_resource_editor_generation(Some(&json!({ - "source": "editor-agent" - })))); - assert!(!is_game_creator_resource_editor_generation(Some( - &json!({}) - ))); - assert!(!is_game_creator_resource_editor_generation(None)); - - // 端到端同口径:AGC 客户端实际发出的 source 必须与 `from_job` 的 consumer 判定一致。 - // 快速编辑端点把 `generationInputs` 对象本身当判定输入,所以这里按 `/generationInputs` 取。 - let mut queued_job = atomic_editor_generation_job_fixture(); - queued_job.dedupe_key = - "game-creator-client-generation:editor_image_generation:fingerprint".to_string(); - queued_job.request_payload_json = json!({ - "generationInputs": { "source": GAME_CREATOR_CLIENT_GENERATION_SOURCE } - }) - .to_string(); - assert_eq!( - EditorGenerationQueueResultContext::from_job(&queued_job).consumer, - EditorGenerationQueueConsumer::GameCreatorResourceEditor - ); - let queued_payload: Value = serde_json::from_str(queued_job.request_payload_json.as_str()) - .expect("fixture payload should be JSON"); - assert!(is_game_creator_resource_editor_generation( - queued_payload.pointer("/generationInputs") - )); - } - - /// 实测 400×5 的回归用例:AGC「图片快速编辑」发出的请求,source 已被改写成 - /// `ai-game-creator-client`,来源资源的 assetKind 是本地 manifest 原始类型 - /// `art-spritesheet` / `ui`(mediaType=image)。这条路径必须放行; + /// 实测 400×5 的回归用例:AGC「图片快速编辑」发出的请求,来源资源的 assetKind 是本地 + /// manifest 原始类型 `art-spritesheet` / `ui`(mediaType=image)。这条路径必须放行; /// 非静态媒体(video/audio/image-sequence)必须继续拒绝。 #[test] fn game_creator_client_quick_edit_accepts_local_manifest_static_image_kinds() { - // 与客户端 `submit_resource_edit_remote` 发送的 JSON 同形:请求体的 - // `generationInputs` 就是这里传入的判定输入。 + // 与客户端 `submit_resource_edit_remote` 发送的 JSON 同形;放行判据只看来源资源的 + // 权威 assetKind 与 mediaType,请求体里的 `generationInputs.source` 不参与。 let agc_payload: Value = serde_json::from_str( r#"{ "prompt": "把这张图改成夜间配色", @@ -20860,9 +20757,13 @@ mod tests { }"#, ) .expect("AGC quick edit payload should be JSON"); - let generation_inputs = agc_payload - .pointer("/generationInputs") - .expect("AGC payload should carry generationInputs"); + assert_eq!( + agc_payload + .pointer("/generationInputs/assetKind") + .and_then(Value::as_str), + Some("art-spritesheet"), + "回归夹具必须保持 AGC 客户端实际发送的请求形状" + ); for (asset_kind, media_type) in [ ("art-spritesheet", "image"), @@ -20873,12 +20774,8 @@ mod tests { ("icon", "image"), ] { assert!( - ensure_editor_image_edit_source_kind_allowed_for_request( - Some(asset_kind), - Some(media_type), - Some(generation_inputs), - ) - .is_ok(), + ensure_editor_image_edit_source_kind_allowed(Some(asset_kind), Some(media_type)) + .is_ok(), "AGC 快速编辑必须放行 {asset_kind}/{media_type}" ); } @@ -20893,24 +20790,17 @@ mod tests { ("future-kind", "image"), ] { assert!( - ensure_editor_image_edit_source_kind_allowed_for_request( - Some(asset_kind), - Some(media_type), - Some(generation_inputs), - ) - .is_err(), + 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_for_request( - Some("future-kind"), - Some("image"), - Some(generation_inputs), - ) - .expect_err("未知类型必须拒绝"); + 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")), @@ -21035,7 +20925,9 @@ mod tests { } let mut forbidden_override = target.clone(); - forbidden_override.effective_asset_kind = Some("icon".to_string()); + // 用仍然被静态图表拒绝的类型,守卫意图不变:目标图层不能借壳成允许来源。 + // `icon` 已是共享契约里的静态图 canonical 类型,不能再当反例。 + forbidden_override.effective_asset_kind = Some("character-animation".to_string()); assert!( ensure_editor_image_edit_target_matches_source( "resource-source",