From 0edefc67ec85590e082d38876f3d31fb5e8f59d4 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sat, 8 Aug 2026 10:26:38 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Agent=20=E9=9F=B3=E9=A2=91?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E8=84=B1=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用外部生成任务的用户可见错误投影 按 Agent 工具名称映射音效任务并在回填前脱敏 补充音效和背景音乐失败回归测试 --- .../api-server/src/editor_agent/reconcile.rs | 68 +++++++++++++++++-- .../api-server/src/editor_agent/tool.rs | 11 +++ .../api-server/src/external_generation.rs | 5 +- 3 files changed, 79 insertions(+), 5 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 5cf310af9..80f6f97ce 100644 --- a/server-rs/crates/api-server/src/editor_agent/reconcile.rs +++ b/server-rs/crates/api-server/src/editor_agent/reconcile.rs @@ -1,5 +1,8 @@ -use crate::http_error::AppError; -use crate::state::AppState; +use crate::{ + editor_agent::tool::editor_agent_audio_generation_job_kind, + external_generation::user_visible_external_generation_error, http_error::AppError, + state::AppState, +}; use platform_editor_agent::agent::tools::context::EditorToolContext; use serde_json::Value; use shared_contracts::editor_agent::{ @@ -87,10 +90,18 @@ pub async fn reconcile_editor_agent_tool_calls( } } "failed" => { + let tool_name = document.messages[message_index] + .tool_call + .as_ref() + .expect("reconcile candidate contains a tool call") + .tool_name + .clone(); mark_job_failed( &mut document.messages[message_index], - job.last_error_message - .unwrap_or_else(|| "生成失败".to_string()), + editor_agent_job_failure_message( + tool_name.as_str(), + job.last_error_message, + ), ); last_reconcile_error = None; reconciled.push(document.messages[message_index].clone()); @@ -124,6 +135,14 @@ fn should_retry_result_reconcile(retry_count: u8) -> bool { retry_count < EDITOR_AGENT_RESULT_RECONCILE_MAX_RETRIES } +fn editor_agent_job_failure_message(tool_name: &str, last_error_message: Option) -> String { + let Some(job_kind) = editor_agent_audio_generation_job_kind(tool_name) else { + return last_error_message.unwrap_or_else(|| "生成失败".to_string()); + }; + user_visible_external_generation_error(job_kind, last_error_message) + .unwrap_or_else(|| "生成失败".to_string()) +} + fn mark_job_failed(message: &mut EditorAgentMessage, error: String) { let tool_call = message .tool_call @@ -219,6 +238,7 @@ pub(crate) fn reconcile_completed_editor_agent_tool_call_for_test( #[cfg(test)] mod tests { use super::*; + use platform_editor_agent::agent::tools::generate_background_music::GenerateBackgroundMusicTool; use platform_editor_agent::agent::tools::generate_sound_effect::GenerateSoundEffectTool; use platform_editor_agent::framework::tool::Tool; use serde_json::json; @@ -310,6 +330,46 @@ mod tests { assert_eq!(tool_call.external_job_id.as_deref(), Some("job-1")); } + #[test] + fn failed_sound_effect_jobs_hide_internal_reason_codes_in_agent_tool_calls() { + for internal_error in [ + "translation_invalid", + "elevenlabs_http_failed", + "ELEVENLABS_BASE_URL 未配置", + ] { + let mut message = pending_tool_message(); + mark_job_failed( + &mut message, + editor_agent_job_failure_message( + GenerateSoundEffectTool::NAME, + Some(internal_error.to_string()), + ), + ); + + let tool_call = message.tool_call.expect("tool call should remain present"); + assert_eq!(tool_call.status, EditorAgentToolCallStatus::Failed); + assert_eq!( + tool_call.error.as_deref(), + Some("音效生成失败,请稍后重试。") + ); + assert!( + !message.text.contains(internal_error), + "Agent 会话文本不应保留内部失败码:{internal_error}" + ); + } + } + + #[test] + fn failed_background_music_jobs_use_the_same_user_visible_error_policy() { + assert_eq!( + editor_agent_job_failure_message( + GenerateBackgroundMusicTool::NAME, + Some("请求 Vector Engine 背景音乐任务失败:connection refused".to_string()), + ), + "背景音乐生成失败,请稍后重试。" + ); + } + #[test] fn completed_legacy_tool_call_with_null_defaults_still_reconciles() { let mut message: EditorAgentMessage = serde_json::from_value(json!({ diff --git a/server-rs/crates/api-server/src/editor_agent/tool.rs b/server-rs/crates/api-server/src/editor_agent/tool.rs index af45fe5a4..c1be56e2c 100644 --- a/server-rs/crates/api-server/src/editor_agent/tool.rs +++ b/server-rs/crates/api-server/src/editor_agent/tool.rs @@ -86,6 +86,17 @@ impl EditorAgentToolError { } } +/// `get_external_generation_job_generated_artifacts` 的精简读取结果不带 `job_kind`; +/// Agent 会话回填只能从已持久化的 tool name 恢复音频任务类型,以复用普通任务读取侧的 +/// 用户可见错误投影。 +pub(crate) fn editor_agent_audio_generation_job_kind(tool_name: &str) -> Option<&'static str> { + match tool_name { + GenerateSoundEffectTool::NAME => Some(EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND), + GenerateBackgroundMusicTool::NAME => Some(EDITOR_BACKGROUND_MUSIC_GENERATION_JOB_KIND), + _ => None, + } +} + impl Display for EditorAgentToolError { fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/server-rs/crates/api-server/src/external_generation.rs b/server-rs/crates/api-server/src/external_generation.rs index 776201f13..83e983c07 100644 --- a/server-rs/crates/api-server/src/external_generation.rs +++ b/server-rs/crates/api-server/src/external_generation.rs @@ -207,7 +207,10 @@ fn map_external_generation_job_status( /// 任务的原始失败文本可能包含内部 provider、分割模型、请求端点或 HTTP 传输层细节,因此 /// 只能在普通用户读取边界替换成稳定文案。 /// 原始值仍留在任务记录、tracing 和后台审计路径中。 -fn user_visible_external_generation_error(job_kind: &str, error: Option) -> Option { +pub(crate) fn user_visible_external_generation_error( + job_kind: &str, + error: Option, +) -> Option { if job_kind == EDITOR_BACKGROUND_REMOVAL_JOB_KIND && error.is_some() { return Some("去除背景失败,请稍后重试。".to_string()); }