diff --git a/server-rs/crates/api-server/src/external_generation.rs b/server-rs/crates/api-server/src/external_generation.rs index 1fba1ce65..679e06c5a 100644 --- a/server-rs/crates/api-server/src/external_generation.rs +++ b/server-rs/crates/api-server/src/external_generation.rs @@ -21,6 +21,7 @@ use spacetime_client::{ use crate::editor_generation_queue::{ EDITOR_BACKGROUND_REMOVAL_JOB_KIND, EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND, + EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND, }; use crate::{ api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError, @@ -202,9 +203,9 @@ fn map_external_generation_job_status( } } -/// 外部任务表保存的失败文本同时服务于 worker 诊断和用户通知。抠图及角色动作透明化任务的 -/// 原始失败文本可能包含内部 provider、分割模型或实现细节,因此只能在普通用户读取边界 -/// 替换成稳定文案。 +/// 外部任务表保存的失败文本同时服务于 worker 诊断和用户通知。抠图、角色动作透明化和音效 +/// 任务的原始失败文本可能包含内部 provider、分割模型、请求端点或 HTTP 传输层细节,因此 +/// 只能在普通用户读取边界替换成稳定文案。 /// 原始值仍留在任务记录、tracing 和后台审计路径中。 fn user_visible_external_generation_error(job_kind: &str, error: Option) -> Option { if job_kind == EDITOR_BACKGROUND_REMOVAL_JOB_KIND && error.is_some() { @@ -213,6 +214,10 @@ fn user_visible_external_generation_error(job_kind: &str, error: Option) if job_kind == EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND && error.is_some() { return Some("角色动作生成失败,请稍后重试。".to_string()); } + // 音效失败文本来自 ElevenLabs / reqwest,会带上请求端点、底层错误链和上游状态码。 + if job_kind == EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND && error.is_some() { + return Some("音效生成失败,请稍后重试。".to_string()); + } error } @@ -470,6 +475,50 @@ mod tests { assert!(!lower.contains("provider")); } + #[test] + fn sound_effect_failure_hides_provider_and_transport_details_from_owner() { + let job = ExternalGenerationJobSummaryRecord { + job_id: "task-sound-effect-failed".to_string(), + job_kind: EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND.to_string(), + owner_user_id: "user-1".to_string(), + source_module: "editor-canvas".to_string(), + source_entity_id: "project-1".to_string(), + request_label: "音效生成".to_string(), + request_prompt: None, + status: "failed".to_string(), + last_error_message: Some( + "请求 ElevenLabs 音效生成失败:error sending request for url \ + (https://api.elevenlabs.io/v1/sound-generation?output_format=mp3_44100_128): \ + connection error: Connection refused (os error 111)" + .to_string(), + ), + created_at: "2026-08-07T08:00:00Z".to_string(), + started_at: Some("2026-08-07T08:00:01Z".to_string()), + completed_at: Some("2026-08-07T08:00:10Z".to_string()), + updated_at: "2026-08-07T08:00:10Z".to_string(), + updated_at_micros: 1_000, + price_mud_points: 5, + refund_ledger_id: None, + notification_acknowledged_at: None, + notification_acknowledged_at_micros: None, + phase: None, + warning_message: None, + }; + + let status = map_external_generation_job_status(job.clone()); + let task = map_external_generation_task_record(job); + + assert_eq!(status.error.as_deref(), Some("音效生成失败,请稍后重试。")); + assert_eq!(task.error.as_deref(), Some("音效生成失败,请稍后重试。")); + for value in [status.error, task.error].into_iter().flatten() { + let lower = value.to_ascii_lowercase(); + assert!(!lower.contains("elevenlabs")); + assert!(!lower.contains("api.elevenlabs.io")); + assert!(!lower.contains("sound-generation")); + assert!(!lower.contains("os error")); + } + } + #[test] fn unrelated_generation_failure_keeps_its_user_visible_message() { let message = user_visible_external_generation_error(