From e39d5fb2782a8edfd39fcf9f74fc9dd520bca239 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Wed, 15 Jul 2026 15:47:17 +0800 Subject: [PATCH] storage generated assets metadata in external job payload (only for editor agent source) --- .../api-server/src/editor_agent/reconcile.rs | 10 +- .../src/editor_agent/resp_to_asset.rs | 43 +++- .../src/external_generation_worker.rs | 218 ++++++++++++++++-- 3 files changed, 247 insertions(+), 24 deletions(-) diff --git a/server-rs/crates/api-server/src/editor_agent/reconcile.rs b/server-rs/crates/api-server/src/editor_agent/reconcile.rs index 93768f437..552a2b3f1 100644 --- a/server-rs/crates/api-server/src/editor_agent/reconcile.rs +++ b/server-rs/crates/api-server/src/editor_agent/reconcile.rs @@ -19,7 +19,9 @@ use platform_editor_agent::agent::tools::generate_sound_effect::{ GenerateSoundEffectTool, GenerateSoundEffectToolArgs, }; use platform_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool; -use platform_editor_agent::agent::tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs}; +use platform_editor_agent::agent::tools::generate_video::{ + GenerateVideoTool, GenerateVideoToolArgs, +}; use platform_editor_agent::framework::tool::Tool; use serde_json::Value; use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse}; @@ -108,9 +110,9 @@ fn reconcile_completed_editor_agent_tool_call( let response = result_payload_json .ok_or_else(|| "生成任务缺少结果数据".to_string()) .and_then(|payload| serde_json::from_str::(payload).map_err(|e| e.to_string()))? - .get("response") + .get("editor-agent-tool-call-result") .cloned() - .ok_or_else(|| "生成任务缺少 response 结果".to_string())?; + .ok_or_else(|| "生成任务缺少 Agent 工具调用结果".to_string())?; let tool_call = message .tool_call .as_mut() @@ -153,7 +155,7 @@ fn reconcile_completed_editor_agent_tool_call( context: EditorToolContext::default(), } .format_execute_message(&args, &result); - tool_call.images = vec![resp_to_asset::editor_agent_generated_spritesheet(&result)]; + tool_call.images = resp_to_asset::editor_agent_generated_spritesheet_images(&result); } GenerateVideoTool::NAME => { let args: GenerateVideoToolArgs = parse_reconciled_value(&tool_call.args)?; diff --git a/server-rs/crates/api-server/src/editor_agent/resp_to_asset.rs b/server-rs/crates/api-server/src/editor_agent/resp_to_asset.rs index 117521d8d..014a7d159 100644 --- a/server-rs/crates/api-server/src/editor_agent/resp_to_asset.rs +++ b/server-rs/crates/api-server/src/editor_agent/resp_to_asset.rs @@ -1,9 +1,11 @@ -use serde_json::Value; -use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse}; -use shared_contracts::editor_agent::{EditorAgentGeneratedAudio, EditorAgentGeneratedImage, EditorAgentGeneratedVideo}; use platform_editor_agent::agent::tools::edit_image::EditorImageEditResult; use platform_editor_agent::agent::tools::generate_icon_spritesheet::EditorIconSpritesheetResult; use platform_editor_agent::agent::tools::generate_image::EditorImageGenerationResult; +use serde_json::Value; +use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse}; +use shared_contracts::editor_agent::{ + EditorAgentGeneratedAudio, EditorAgentGeneratedImage, EditorAgentGeneratedVideo, +}; fn value_string(value: Option<&Value>, field: &str) -> Option { value @@ -12,7 +14,9 @@ fn value_string(value: Option<&Value>, field: &str) -> Option { .map(ToOwned::to_owned) } -pub fn editor_agent_generated_image(result: &EditorImageGenerationResult) -> EditorAgentGeneratedImage { +pub fn editor_agent_generated_image( + result: &EditorImageGenerationResult, +) -> EditorAgentGeneratedImage { EditorAgentGeneratedImage { resource_id: value_string(result.resource.as_ref(), "resourceId"), object_key: result.object_key.clone(), @@ -52,7 +56,32 @@ pub fn editor_agent_generated_spritesheet( } } -pub fn editor_agent_generated_video(result: &EditorVideoGenerateResponse) -> EditorAgentGeneratedVideo { +pub fn editor_agent_generated_spritesheet_images( + result: &EditorIconSpritesheetResult, +) -> Vec { + let images = result + .icon_image_srcs + .iter() + .map(|image| EditorAgentGeneratedImage { + resource_id: value_string(image.resource.as_ref(), "resourceId"), + object_key: value_string(image.resource.as_ref(), "objectKey"), + asset_object_id: value_string(image.resource.as_ref(), "assetObjectId"), + image_src: image.image_src.clone(), + thumbnail_src: None, + width: Some(image.width), + height: Some(image.height), + }) + .collect::>(); + if images.is_empty() { + vec![editor_agent_generated_spritesheet(result)] + } else { + images + } +} + +pub fn editor_agent_generated_video( + result: &EditorVideoGenerateResponse, +) -> EditorAgentGeneratedVideo { EditorAgentGeneratedVideo { resource_id: value_string(result.resource.as_ref(), "resourceId"), object_key: result.object_key.clone(), @@ -64,7 +93,9 @@ pub fn editor_agent_generated_video(result: &EditorVideoGenerateResponse) -> Edi } } -pub fn editor_agent_generated_audio(result: &EditorAudioGenerateResponse) -> EditorAgentGeneratedAudio { +pub fn editor_agent_generated_audio( + result: &EditorAudioGenerateResponse, +) -> EditorAgentGeneratedAudio { EditorAgentGeneratedAudio { resource_id: value_string(result.resource.as_ref(), "resourceId"), object_key: result.object_key.clone(), diff --git a/server-rs/crates/api-server/src/external_generation_worker.rs b/server-rs/crates/api-server/src/external_generation_worker.rs index 48327dfa7..1b6915d29 100644 --- a/server-rs/crates/api-server/src/external_generation_worker.rs +++ b/server-rs/crates/api-server/src/external_generation_worker.rs @@ -675,7 +675,9 @@ async fn process_external_generation_job_once( ) .await { - Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await, + Ok(result) => { + complete_editor_generation_job(&state, &worker_id, &job, result.0).await + } Err(error) => { let message = error.body_text(); fail_job(&state, &worker_id, &job, message.clone()).await?; @@ -703,7 +705,9 @@ async fn process_external_generation_job_once( ) .await { - Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await, + Ok(result) => { + complete_editor_generation_job(&state, &worker_id, &job, result.0).await + } Err(error) => { let message = error.body_text(); fail_job(&state, &worker_id, &job, message.clone()).await?; @@ -732,7 +736,9 @@ async fn process_external_generation_job_once( ) .await { - Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await, + Ok(result) => { + complete_editor_generation_job(&state, &worker_id, &job, result.0).await + } Err(error) => { let message = error.body_text(); fail_job(&state, &worker_id, &job, message.clone()).await?; @@ -833,7 +839,9 @@ async fn process_external_generation_job_once( ) .await { - Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await, + Ok(result) => { + complete_editor_generation_job(&state, &worker_id, &job, result.0).await + } Err(response) => { let message = response_error_message(response).await; fail_job(&state, &worker_id, &job, message.clone()).await?; @@ -862,7 +870,9 @@ async fn process_external_generation_job_once( ) .await { - Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await, + Ok(result) => { + complete_editor_generation_job(&state, &worker_id, &job, result.0).await + } Err(response) => { let message = response_error_message(response).await; fail_job(&state, &worker_id, &job, message.clone()).await?; @@ -891,7 +901,9 @@ async fn process_external_generation_job_once( ) .await { - Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await, + Ok(result) => { + complete_editor_generation_job(&state, &worker_id, &job, result.0).await + } Err(response) => { let message = response_error_message(response).await; fail_job(&state, &worker_id, &job, message.clone()).await?; @@ -920,7 +932,9 @@ async fn process_external_generation_job_once( ) .await { - Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await, + Ok(result) => { + complete_editor_generation_job(&state, &worker_id, &job, result.0).await + } Err(response) => { let message = response_error_message(response).await; fail_job(&state, &worker_id, &job, message.clone()).await?; @@ -1025,18 +1039,13 @@ async fn complete_editor_generation_job( state: &AppState, worker_id: &str, job: &ExternalGenerationJobRecord, + response: Value, ) -> Result<(), String> { complete_job( state, worker_id, job, - Some( - json!({ - "sourceModule": job.source_module.clone(), - "sourceEntityId": job.source_entity_id.clone(), - }) - .to_string(), - ), + Some(editor_generation_result_payload_json(job, &response)), ) .await } @@ -1064,6 +1073,16 @@ fn editor_generation_result_payload_json( "sourceModule": job.source_module.clone(), "sourceEntityId": job.source_entity_id.clone(), }); + if is_editor_agent_generation_job(job) + && let Some(object) = payload.as_object_mut() + { + // The Agent needs this compact result to restore its tool-call card. Other jobs keep + // master's metadata-only completion payload to avoid turning the queue into an asset API. + object.insert( + "editor-agent-tool-call-result".to_string(), + compact_editor_generation_result(response.clone()), + ); + } if let Some(warning) = extract_editor_generation_slice_warning(response) && let Some(object) = payload.as_object_mut() { @@ -1072,6 +1091,55 @@ fn editor_generation_result_payload_json( payload.to_string() } +fn is_editor_agent_generation_job(job: &ExternalGenerationJobRecord) -> bool { + serde_json::from_str::(job.request_payload_json.as_str()) + .ok() + .is_some_and(|payload| { + payload + .pointer("/generationInputs/source") + .and_then(Value::as_str) + .is_some_and(|source| source.trim() == "editor-agent-tool-call-result") + }) +} + +fn compact_editor_generation_result(mut result: Value) -> Value { + let Some(object) = result.as_object_mut() else { + return result; + }; + object.remove("project"); + object.remove("asset"); + object.remove("spritesheetAsset"); + for field in ["resource", "spritesheetResource"] { + let Some(resource) = object.get_mut(field).and_then(Value::as_object_mut) else { + continue; + }; + resource + .retain(|key, _| matches!(key.as_str(), "resourceId" | "objectKey" | "assetObjectId")); + } + if let Some(icon_image_srcs) = object + .get_mut("iconImageSrcs") + .and_then(Value::as_array_mut) + { + for icon in icon_image_srcs { + let Some(icon) = icon.as_object_mut() else { + continue; + }; + if let Some(resource) = icon.get_mut("resource").and_then(Value::as_object_mut) { + resource.retain(|key, _| { + matches!(key.as_str(), "resourceId" | "objectKey" | "assetObjectId") + }); + } + icon.retain(|key, _| { + matches!( + key.as_str(), + "name" | "imageSrc" | "width" | "height" | "resource" + ) + }); + } + } + result +} + fn extract_editor_generation_slice_warning(response: &Value) -> Option { let data = response.get("data").unwrap_or(response); let warning = data.get("sliceWarning")?; @@ -1357,6 +1425,128 @@ mod tests { ); assert!(payload.get("spritesheetImageSrc").is_none()); assert!(payload.get("iconImageSrcs").is_none()); + assert!(payload.get("editor-agent-tool-call-result").is_none()); + } + + #[test] + fn editor_agent_result_payload_keeps_compact_response() { + let mut job = external_generation_job_record_fixture(Some("lease-1")); + job.request_payload_json = json!({ + "generationInputs": { "source": "editor-agent-tool-call-result" }, + }) + .to_string(); + let response = json!({ + "imageSrc": "/api/assets/object/generated.png", + "objectKey": "users/user-1/generated.png", + "assetObjectId": "asset-object-1", + "width": 1024, + "height": 1024, + "sourceType": "generated", + "prompt": "castle", + "actualPrompt": null, + "model": "gpt-image-2", + "provider": "VectorEngine", + "taskId": "provider-task-1", + "resource": { + "resourceId": "resource-1", + "objectKey": "users/user-1/generated.png", + "assetObjectId": "asset-object-1", + "imageSrc": "data:image/png;base64,SHOULD_NOT_PERSIST", + }, + "asset": { "assetId": "asset-1" }, + "project": { "projectId": "project-1" }, + }); + + let payload: Value = + serde_json::from_str(&editor_generation_result_payload_json(&job, &response)) + .expect("worker result should be valid JSON"); + + assert_eq!( + payload["editor-agent-tool-call-result"]["imageSrc"], + json!("/api/assets/object/generated.png") + ); + assert_eq!( + payload["editor-agent-tool-call-result"]["resource"], + json!({ + "resourceId": "resource-1", + "objectKey": "users/user-1/generated.png", + "assetObjectId": "asset-object-1", + }) + ); + assert!(payload["editor-agent-tool-call-result"].get("asset").is_none()); + assert!(payload["editor-agent-tool-call-result"].get("project").is_none()); + assert!(!payload.to_string().contains("data:image")); + } + + #[test] + fn editor_agent_spritesheet_result_keeps_all_persisted_slices() { + let mut job = external_generation_job_record_fixture(Some("lease-1")); + job.request_payload_json = json!({ + "generationInputs": { "source": "editor-agent-tool-call-result" }, + }) + .to_string(); + let response = json!({ + "spritesheetImageSrc": "/api/assets/object/sheet.png", + "spritesheetWidth": 512, + "spritesheetHeight": 512, + "taskId": "provider-task-1", + "spritesheetResource": { + "resourceId": "sheet-resource", + "objectKey": "users/user-1/sheet.png", + "assetObjectId": "sheet-object", + }, + "iconImageSrcs": [ + { + "name": "backpack", + "imageSrc": "/api/assets/object/backpack.png", + "width": 64, + "height": 64, + "resource": { + "resourceId": "icon-resource-1", + "objectKey": "users/user-1/backpack.png", + "assetObjectId": "icon-object-1", + "imageSrc": "data:image/png;base64,SHOULD_NOT_PERSIST", + }, + "asset": { "assetId": "icon-asset-1" }, + }, + { + "name": "map", + "imageSrc": "/api/assets/object/map.png", + "width": 64, + "height": 64, + "resource": { + "resourceId": "icon-resource-2", + "objectKey": "users/user-1/map.png", + "assetObjectId": "icon-object-2", + }, + }, + ], + }); + + let payload: Value = + serde_json::from_str(&editor_generation_result_payload_json(&job, &response)) + .expect("worker result should be valid JSON"); + + assert_eq!( + payload["editor-agent-tool-call-result"]["iconImageSrcs"] + .as_array() + .map(Vec::len), + Some(2) + ); + assert_eq!( + payload["editor-agent-tool-call-result"]["iconImageSrcs"][0]["resource"], + json!({ + "resourceId": "icon-resource-1", + "objectKey": "users/user-1/backpack.png", + "assetObjectId": "icon-object-1", + }) + ); + assert!( + payload["editor-agent-tool-call-result"]["iconImageSrcs"][0] + .get("asset") + .is_none() + ); + assert!(!payload.to_string().contains("data:image")); } #[test]