Merge branch 'fix/agc-rust-staging-idempotency' into feat/agc-bundled-resources-m3
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m24s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 53s
Project CI / Native shell tests (pull_request) Failing after 1m19s
Project CI / Frontend tests (pull_request) Failing after 57s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 5m15s
Project CI / Backend tests (pull_request) Successful in 3m50s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m28s
Project CI / Repository checks (pull_request) Successful in 1m55s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m50s

This commit is contained in:
2026-09-28 12:19:31 +08:00
20 changed files with 947 additions and 112 deletions
@@ -2326,6 +2326,30 @@ fn direct_taonier_reference_matches_local_source(
.unwrap_or(false)
}
fn direct_taonier_background_asset_identity(
root: &Path,
expected_reference_source: Option<&DirectTaonierArtAssetIdentity>,
) -> Option<DirectTaonierArtAssetIdentity> {
direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"/api/external/v1/editor/scenes/generations",
"scene",
expected_reference_source,
)
.or_else(|| {
direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"/api/external/v1/editor/images/generations",
"spec",
expected_reference_source,
)
})
}
fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
let Some(art_spec) = direct_taonier_art_asset_identity(
root,
@@ -2337,14 +2361,7 @@ fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
) else {
return false;
};
let Some(background) = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"/api/external/v1/editor/images/generations",
"spec",
Some(&art_spec),
) else {
let Some(background) = direct_taonier_background_asset_identity(root, Some(&art_spec)) else {
return false;
};
let _ = (background, art_spec);
@@ -3127,27 +3144,16 @@ async fn generate_direct_taonier_art_asset_at(
};
let runtime_context =
direct_taonier_art_generation_runtime_context(root, output_path, asset_kind)?;
let generation = if retain_runtime_state {
generate_platform_art_asset_with_retained_runtime_options_at(
root,
prompt,
&[],
&options,
require_slices,
&runtime_context,
)
.await
} else {
generate_platform_art_asset_with_runtime_options_at(
root,
prompt,
&[],
&options,
require_slices,
&runtime_context,
)
.await
};
let generation = generate_art_package_asset_with_runtime_options_at(
root,
prompt,
&[],
&options,
require_slices,
retain_runtime_state,
&runtime_context,
)
.await;
let generated = match generation {
Ok(generated) => generated,
Err(error)
@@ -3325,15 +3331,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
})
.flatten();
let existing_art_spec_and_background = existing_art_spec.as_ref().is_some_and(|art_spec| {
direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"/api/external/v1/editor/images/generations",
"spec",
Some(art_spec),
)
.is_some()
direct_taonier_background_asset_identity(root, Some(art_spec)).is_some()
});
let art_spec = match existing_art_spec {
Some(identity) => identity,
@@ -3409,15 +3407,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
}
};
if mode.regenerates_existing()
|| direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"/api/external/v1/editor/images/generations",
"spec",
Some(&art_spec),
)
.is_none()
|| direct_taonier_background_asset_identity(root, Some(&art_spec)).is_none()
{
emit_direct_game_creator_progress(root, "art.background", "正在生成 16:9 游戏场景背景图");
let outcome = match generate_direct_taonier_art_asset_at(
@@ -8294,8 +8284,8 @@ mod tests {
vec!["taonier-resource-icon-spec".to_string()],
),
GameCreationAppAssetKind::Scene => (
"/api/external/v1/editor/images/generations",
"spec",
"/api/external/v1/editor/scenes/generations",
"scene",
vec!["taonier-resource-icon-spec".to_string()],
),
_ => (
@@ -8679,6 +8669,46 @@ mod tests {
);
}
#[test]
fn legacy_route_background_registration_still_counts_as_valid_base() {
let root = tempfile::tempdir().expect("temp dir");
init_local_game_project_at(root.path(), "legacy-background-route", "旧路由背景登记")
.expect("init project");
std::fs::create_dir_all(root.path().join("assets")).expect("assets dir");
register_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
);
std::fs::write(
root.path().join(DIRECT_CODEX_BACKGROUND_ASSET_PATH),
tiny_visible_png(),
)
.expect("background bytes");
register_local_asset_at(
root.path(),
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"image/png",
"platform-art",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Canvas,
canvas_project_id: Some("taonier-project".to_string()),
resource_id: Some("taonier-resource-game-background".to_string()),
asset_object_id: Some("taonier-object-game-background".to_string()),
task_id: Some("taonier-task-game-background".to_string()),
prompt: None,
model: Some("gpt-image-2".to_string()),
generation_route: Some("/api/external/v1/editor/images/generations".to_string()),
generation_kind: Some("spec".to_string()),
reference_resource_ids: vec!["taonier-resource-icon-spec".to_string()],
},
)
.expect("register legacy-route background");
assert!(direct_taonier_art_base_is_valid(root.path()));
}
fn tiny_visible_png() -> Vec<u8> {
let mut bytes = Vec::new();
let image = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
@@ -18,8 +18,7 @@ pub(crate) use canvas_generation::submit_external_generation_request;
pub(in crate::agent) use canvas_generation::{
admit_platform_art_generation_at, commit_prepared_platform_art_asset_at,
commit_prepared_platform_art_asset_strict_slices_at, generate_admitted_platform_art_asset_at,
generate_platform_art_asset_with_retained_runtime_options_at,
generate_platform_art_asset_with_runtime_options_at,
generate_art_package_asset_with_runtime_options_at,
platform_art_generation_error_result_unknown, recover_persisted_visual_generation_options,
register_existing_platform_art_slices_at, request_platform_art_asset_with_runtime_options_at,
restore_platform_art_asset_bytes_at,
@@ -2267,6 +2267,35 @@ fn decode_platform_art_image_bytes_with_limits(
.map_err(|error| format!("{label}无法在安全内存边界内解码:{error}"))
}
fn normalize_art_package_png(download: &mut CanvasResourceDownload) -> Result<(), String> {
if download.bytes.len() > PLATFORM_ART_SPRITESHEET_SINGLE_DOWNLOAD_BYTES {
return Err("美术包图片超过 20 MiB 本地校验上限".to_string());
}
let format = image::guess_format(&download.bytes)
.map_err(|error| format!("美术包图片无法识别格式:{error}"))?;
if !matches!(
format,
image::ImageFormat::Png | image::ImageFormat::Jpeg | image::ImageFormat::WebP
) {
return Err("美术包图片仅支持 PNG、JPEG、WebP".to_string());
}
// PNG 也必须完整解码;保留原字节以兼容既有摘要和恢复记录。
let decoded = decode_platform_art_image_with_limits(download, "美术包图片")?;
if format != image::ImageFormat::Png {
let mut output = std::io::Cursor::new(Vec::new());
decoded
.write_to(&mut output, image::ImageFormat::Png)
.map_err(|error| format!("美术包图片转为 PNG 失败:{error}"))?;
let bytes = output.into_inner();
if bytes.len() > PLATFORM_ART_SPRITESHEET_SINGLE_DOWNLOAD_BYTES {
return Err("美术包图片转为 PNG 后超过 20 MiB 本地校验上限".to_string());
}
download.bytes = bytes;
}
download.media_type = "image/png".to_string();
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::agent) struct ValidatedPlatformArtPng {
pub(in crate::agent) width: u32,
@@ -2555,6 +2584,7 @@ pub(in crate::agent) async fn generate_admitted_platform_art_asset_at(
false,
&context,
Some(admission.guard),
false,
)
.await
}
@@ -2735,20 +2765,19 @@ pub(in crate::agent) async fn generate_platform_art_asset_with_runtime_options_a
false,
runtime_context,
None,
false,
)
.await
}
/// Installs a recovered or newly generated asset while retaining the durable
/// request state. Package-level workflows use this to compensate local files
/// after a later stage fails without losing the accepted operation identity or
/// issuing another billable POST when the package is retried.
pub(in crate::agent) async fn generate_platform_art_asset_with_retained_runtime_options_at(
/// 美术包统一输出 PNG;重生成时保留账本,以便后续阶段失败后复用已付费结果。
pub(in crate::agent) async fn generate_art_package_asset_with_runtime_options_at(
root: &Path,
prompt: &str,
briefs: &[AgentGroupBrief],
options: &PlatformArtAssetGenerationOptions,
require_slices: bool,
retain_runtime_state: bool,
runtime_context: &PlatformArtGenerationRuntimeContext,
) -> Result<GeneratedPlatformArtAsset, String> {
generate_platform_art_asset_with_runtime_options_and_retention_at(
@@ -2757,9 +2786,10 @@ pub(in crate::agent) async fn generate_platform_art_asset_with_retained_runtime_
briefs,
options,
require_slices,
true,
retain_runtime_state,
runtime_context,
None,
true,
)
.await
}
@@ -2835,14 +2865,15 @@ pub(in crate::agent) fn retained_platform_art_generation_runtime_state_matches_d
&& art_spec_asset_type == Some("icon-spec")
}
GameCreationAppAssetKind::Scene => {
snapshot.endpoint == "/api/external/v1/editor/images/generations"
&& snapshot.generation_kind == "spec"
snapshot.endpoint == "/api/external/v1/editor/scenes/generations"
&& snapshot.generation_kind == "scene"
&& platform_art_runtime_references_match_request_contract(
&snapshot.reference_resource_ids,
GameCreationAppAssetKind::Scene,
)
&& request_asset_kind.as_deref() == Some("game-background")
&& art_spec_asset_type == Some("background")
&& json_string_field(&request_body, "sceneContent")
.is_some_and(|value| !value.trim().is_empty())
&& json_string_field(&request_body, "stylePreset").as_deref() == Some("custom")
}
GameCreationAppAssetKind::IconSpritesheet => {
snapshot.endpoint == "/api/external/v1/editor/icon-spritesheets/generations"
@@ -2908,6 +2939,7 @@ async fn generate_platform_art_asset_with_runtime_options_and_retention_at(
retain_runtime_state: bool,
runtime_context: &PlatformArtGenerationRuntimeContext,
admitted_guard: Option<super::external_generation_state::DurablePlatformArtGenerationGuard>,
normalize_package_png: bool,
) -> Result<GeneratedPlatformArtAsset, String> {
if require_slices && options.asset_kind != GameCreationAppAssetKind::IconSpritesheet {
return Err("严格游戏切片生成只允许 icon-spritesheet 资产类型".to_string());
@@ -2979,7 +3011,7 @@ async fn generate_platform_art_asset_with_runtime_options_and_retention_at(
)?;
recover_interrupted_strict_platform_art_transaction_locked_at(root, &recovery_lock)?;
}
let prepared = request_platform_art_asset_with_runtime_options_at(
let mut prepared = request_platform_art_asset_with_runtime_options_at(
root,
prompt,
briefs,
@@ -2987,6 +3019,21 @@ async fn generate_platform_art_asset_with_runtime_options_and_retention_at(
Some(runtime_context),
)
.await?;
if normalize_package_png {
normalize_art_package_png(&mut prepared.download)?;
prepared.extension = "png".to_string();
if options.asset_kind == GameCreationAppAssetKind::IconSpritesheet
&& prepared.download.bytes.len()
+ prepared
.slices
.iter()
.map(|slice| slice.download.bytes.len())
.sum::<usize>()
> PLATFORM_ART_SPRITESHEET_TOTAL_DOWNLOAD_BYTES
{
return Err("美术包图集转码后与切片累计超过 32 MiB,已拒绝同步".to_string());
}
}
prepared.validate_frozen_session()?;
let lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
@@ -3248,6 +3295,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
GameCreationAppAssetKind::Image => "image",
GameCreationAppAssetKind::Character => "character",
GameCreationAppAssetKind::PublicationMaterial => "publication-material",
GameCreationAppAssetKind::Scene => "scene",
_ => "spec",
};
let is_canonical_art_spritesheet =
@@ -3291,6 +3339,25 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
},
}),
)
} else if options.asset_kind == GameCreationAppAssetKind::Scene {
(
"/api/external/v1/editor/scenes/generations",
serde_json::json!({
"sceneContent": generation_prompt,
"stylePreset": "custom",
"customStyle": prompt_text!("media.scene_style"),
"aspectRatio": options.aspect_ratio,
"imageSize": options.image_size,
"assetLabel": options.asset_label,
"projectId": canvas_context.project_id,
"assetFolderId": canvas_context.asset_folder_id,
"referenceImageSrcs": references.ordered.clone(),
"canvasCompletion": {
"title": options.asset_label,
"placeholder": external_canvas_placeholder(&options.aspect_ratio),
},
}),
)
} else {
(
"/api/external/v1/editor/images/generations",
@@ -10301,6 +10368,81 @@ mod canvas_generation_tests {
rgba_test_png_with_quality(alpha, CompressionType::Fast, FilterType::Adaptive)
}
fn art_package_test_image(format: image::ImageFormat) -> CanvasResourceDownload {
let image = image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(
2,
2,
image::Rgb([12, 34, 56]),
));
let mut output = std::io::Cursor::new(Vec::new());
image.write_to(&mut output, format).expect("encode fixture");
CanvasResourceDownload {
bytes: output.into_inner(),
media_type: format.to_mime_type().to_string(),
}
}
#[test]
fn art_package_png_normalization_preserves_png_bytes_and_real_alpha() {
let mut png = rgba_test_png(120);
let original = png.bytes.clone();
png.media_type = "application/octet-stream".to_string();
normalize_art_package_png(&mut png).expect("validate existing PNG");
assert_eq!(png.bytes, original);
assert_eq!(png.media_type, "image/png");
let mut webp = std::io::Cursor::new(Vec::new());
image::load_from_memory(&original)
.unwrap()
.write_to(&mut webp, image::ImageFormat::WebP)
.unwrap();
let mut webp = CanvasResourceDownload {
bytes: webp.into_inner(),
media_type: "image/webp".to_string(),
};
normalize_art_package_png(&mut webp).unwrap();
let validated = validate_platform_art_png_bytes_with_limits(&webp.bytes, "test").unwrap();
assert!(validated.has_transparent_pixels && validated.has_visible_pixels);
assert_eq!(
validated.pixel_sha256,
validate_platform_art_png_bytes_with_limits(&original, "test")
.unwrap()
.pixel_sha256
);
let mut jpeg = art_package_test_image(image::ImageFormat::Jpeg);
normalize_art_package_png(&mut jpeg).unwrap();
let (transparent, visible, _) = platform_art_spritesheet_alpha_contract(&jpeg).unwrap();
assert!(!transparent, "JPEG 转码不能补造透明像素");
assert!(visible);
}
#[test]
fn art_package_png_normalization_rejects_invalid_or_oversized_images() {
let mut oversized = Vec::new();
PngEncoder::new(&mut oversized)
.write_image(
&vec![0; (PLATFORM_ART_SPRITESHEET_MAX_DIMENSION as usize + 1) * 4],
PLATFORM_ART_SPRITESHEET_MAX_DIMENSION + 1,
1,
ColorType::Rgba8.into(),
)
.unwrap();
for bytes in [
b"GIF89a".to_vec(),
b"\x89PNG\r\n\x1a\n".to_vec(),
vec![0xff, 0xd8, 0xff],
oversized,
vec![0; PLATFORM_ART_SPRITESHEET_SINGLE_DOWNLOAD_BYTES + 1],
] {
let mut download = CanvasResourceDownload {
bytes,
media_type: "image/png".to_string(),
};
assert!(normalize_art_package_png(&mut download).is_err());
}
}
fn rgba_test_png_with_quality(
alpha: u8,
compression: CompressionType,
@@ -10986,6 +11128,16 @@ mod canvas_generation_tests {
#[tokio::test]
async fn retained_runtime_generation_retries_a_completed_stage_without_posting_again() {
for format in [
image::ImageFormat::Png,
image::ImageFormat::Jpeg,
image::ImageFormat::WebP,
] {
assert_art_package_recovers_completed_stage(format).await;
}
}
async fn assert_art_package_recovers_completed_stage(format: image::ImageFormat) {
let temporary = tempfile::tempdir().expect("create retained runtime project");
let root = temporary.path();
init_local_game_project_at(root, "retained-runtime", "整包阶段恢复")
@@ -10993,12 +11145,14 @@ mod canvas_generation_tests {
let listener =
std::net::TcpListener::bind("127.0.0.1:0").expect("bind retained download fixture");
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
let image_url = format!("{base_url}/retained.png");
let extension = format.extensions_str()[0];
let image_url = format!("{base_url}/retained.{extension}");
let signed_image_url = image_url.clone();
let png = rgba_test_png(u8::MAX).bytes;
let source = art_package_test_image(format);
let source_bytes = source.bytes.clone();
let (request_sender, request_receiver) = std::sync::mpsc::channel();
let server = std::thread::spawn(move || {
for _ in 0..4 {
for request_index in 0..6 {
let (mut stream, _) = listener.accept().expect("accept retained download");
let request = read_test_http_request(&mut stream);
request_sender
@@ -11017,12 +11171,19 @@ mod canvas_generation_tests {
)
.expect("write retained signed URL");
} else {
// 首次下载有合法签名但内容损坏,重试必须仍使用同一已完成账本。
let bytes = if request_index == 1 {
&source.bytes[..16]
} else {
&source.bytes
};
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
png.len()
"HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
source.media_type,
bytes.len()
)
.and_then(|_| stream.write_all(&png))
.and_then(|_| stream.write_all(bytes))
.expect("write retained image");
}
}
@@ -11081,7 +11242,7 @@ mod canvas_generation_tests {
"resource": {
"resourceId": "retained-resource",
"projectId": "retained-canvas-project",
"objectKey": "retained/spec.png",
"objectKey": format!("retained/spec.{extension}"),
"imageSrc": image_url
},
"taskId": "retained-task",
@@ -11097,21 +11258,76 @@ mod canvas_generation_tests {
"retained-runtime-key".to_string(),
),
async {
for _ in 0..2 {
generate_platform_art_asset_with_retained_runtime_options_at(
let ledger_before =
read_platform_art_generation_runtime_state(root, &runtime_context)
.unwrap()
.unwrap();
let failure = generate_art_package_asset_with_runtime_options_at(
root,
prompt,
&[],
&options,
false,
false,
&runtime_context,
)
.await;
assert!(failure.is_err());
assert!(!root.join("assets/art-spec.png").exists());
assert!(read_manifest_for_project(root).unwrap().assets.is_empty());
let ledger_after =
read_platform_art_generation_runtime_state(root, &runtime_context)
.unwrap()
.unwrap();
assert_eq!(
platform_art_generation_runtime_idempotency_key(&ledger_before),
platform_art_generation_runtime_idempotency_key(&ledger_after)
);
assert_eq!(
platform_art_generation_runtime_retained_result(&ledger_before),
platform_art_generation_runtime_retained_result(&ledger_after)
);
let mut installed_bytes = None;
let mut installed_asset_id = None;
for retain in [true, false] {
generate_art_package_asset_with_runtime_options_at(
root,
prompt,
&[],
&options,
false,
retain,
&runtime_context,
)
.await
.expect("recover retained completed stage");
assert!(
let bytes = fs::read(root.join("assets/art-spec.png")).unwrap();
let validated =
validate_platform_art_png_bytes_with_limits(&bytes, "recovered").unwrap();
assert_eq!((validated.width, validated.height), (2, 2));
if format == image::ImageFormat::Png {
assert_eq!(bytes, source_bytes);
}
if let Some(previous) = installed_bytes.replace(bytes) {
assert_eq!(installed_bytes.as_ref().unwrap(), &previous);
}
let manifest = read_manifest_for_project(root).unwrap();
assert_eq!(manifest.assets.len(), 1);
let asset = &manifest.assets[0];
assert_eq!(asset.local_path, "assets/art-spec.png");
assert_eq!(asset.media_type, "image/png");
assert_eq!(
asset.source.resource_id.as_deref(),
Some("retained-resource")
);
if let Some(previous) = installed_asset_id.replace(asset.id.clone()) {
assert_eq!(asset.id, previous);
}
assert_eq!(
read_platform_art_generation_runtime_state(root, &runtime_context)
.expect("read retained stage ledger")
.is_some()
.is_some(),
retain
);
}
},
@@ -11124,13 +11340,13 @@ mod canvas_generation_tests {
.ok()
})
.collect::<Vec<_>>();
assert_eq!(requests.len(), 4);
assert_eq!(requests.len(), 6);
assert_eq!(
requests
.iter()
.filter(|request| request.starts_with("GET /retained.png "))
.filter(|request| request.starts_with(&format!("GET /retained.{extension} ")))
.count(),
2
3
);
assert!(requests.iter().all(|request| request.starts_with("GET ")));
assert!(requests.iter().all(|request| !request.starts_with("POST ")));
@@ -13272,14 +13488,13 @@ mod canvas_generation_tests {
let background = write_retained_stage(
root,
"run-background-canonical-and-user-references",
"/api/external/v1/editor/images/generations",
"/api/external/v1/editor/scenes/generations",
serde_json::json!({
"prompt": "保留账本参考合同",
"kind": "spec",
"assetKind": "game-background",
"sceneContent": "保留账本参考合同",
"stylePreset": "custom",
"customStyle": "原创横屏 Web 游戏场景背景",
"projectId": "test-canvas-project",
"assetFolderId": "test-asset-folder",
"generationInputs": { "artSpec": { "assetType": "background" } },
"referenceImageSrcs": ["resource-icon-spec", "user-reference-1"],
}),
);
@@ -735,6 +735,30 @@ pub(super) fn platform_art_generation_runtime_request_snapshot(
.collect::<Result<Vec<_>, _>>()?;
(generation_kind, reference_resource_ids)
}
"/api/external/v1/editor/scenes/generations" => {
if json_string_field(&request_body, "sceneContent")
.is_none_or(|value| value.trim().is_empty())
{
return Err("External Editor 场景生成账本请求缺少 sceneContent".to_string());
}
let reference_resource_ids = request_body
.get("referenceImageSrcs")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| {
"External Editor 场景生成账本请求缺少 referenceImageSrcs".to_string()
})?
.iter()
.map(|value| {
value
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| "External Editor 场景生成账本引用资源 ID 无效".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
("scene".to_string(), reference_resource_ids)
}
"/api/external/v1/editor/icon-spritesheets/generations" => {
let reference_resource_id = json_string_field(&request_body, "referenceId")
.or_else(|| json_string_field(&request_body, "referenceImageSrc"))
@@ -555,6 +555,7 @@ fn platform_art_generation_request_snapshot_is_valid(payload: &serde_json::Value
endpoint,
Some(
"/api/external/v1/editor/images/generations"
| "/api/external/v1/editor/scenes/generations"
| "/api/external/v1/editor/icon-spritesheets/generations"
)
);