storage generated assets metadata in external job payload (only for editor agent source)

This commit is contained in:
2026-07-15 15:47:17 +08:00
parent fda0931716
commit e39d5fb278
3 changed files with 247 additions and 24 deletions
@@ -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::<Value>(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)?;
@@ -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<String> {
value
@@ -12,7 +14,9 @@ fn value_string(value: Option<&Value>, field: &str) -> Option<String> {
.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<EditorAgentGeneratedImage> {
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::<Vec<_>>();
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(),
@@ -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::<Value>(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<Value> {
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]