From dea6576f899be6d15f20ef9a7e5f53890a60c953 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sat, 8 Aug 2026 12:26:41 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9F=B3=E9=A2=91=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=93=8D=E5=BA=94=E6=B3=84=E9=9C=B2=20Provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 收紧音频公开 DTO 与前端响应类型 统一清理 inline 和 Agent 队列结果中的 Provider 覆盖嵌套快照脱敏与历史 Agent 回填兼容 --- .../api-server/src/editor_agent/tool.rs | 1 - .../crates/api-server/src/editor_project.rs | 24 +--- .../generation.rs | 136 +++++++++++++----- .../sound_effect_worker.rs | 5 +- .../crates/shared-contracts/src/assets.rs | 10 +- .../ImageCanvasGenerationLayerModel.test.ts | 1 - ...anvasGenerationSubmissionWorkflow.test.tsx | 1 - .../useImageCanvasGenerationWorkflow.test.tsx | 1 - .../image-editor/editorProjectClient.test.ts | 3 - .../image-editor/editorProjectClient.ts | 1 - 10 files changed, 113 insertions(+), 70 deletions(-) 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 c1be56e2c..cb61ab1dc 100644 --- a/server-rs/crates/api-server/src/editor_agent/tool.rs +++ b/server-rs/crates/api-server/src/editor_agent/tool.rs @@ -1572,7 +1572,6 @@ mod tests { "prompt": "按钮点击声", "actualPrompt": "A short button click", "model": "eleven_text_to_sound_v2", - "provider": "elevenlabs", "taskId": "task-1", "priceMudPoints": 5, "audioKind": "sound-effect" diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 68d9f7319..d53ec71d8 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -1130,21 +1130,12 @@ fn serialize_atomic_editor_generation_job_result( } /// 队列结果落库前的通用紧凑化。原子提交与 worker 直接 complete 两条路径共用同一份实现, -/// 避免其中一条单独演进后丢掉音频豁免(历史上就出现过:豁免只加在 worker 副本里, -/// 原子化改造新写的副本没带上,导致 Agent 音效成功却回填失败)。 +/// 避免任何 consumer 把内部 provider 带入可重放的结果快照。 pub(crate) fn compact_editor_generation_result(mut result: Value) -> Value { let Some(object) = result.as_object_mut() else { return result; }; - // 音频 Agent 使用完整音频 DTO 回填,必须保留其必填 provider;其它生成结果继续隐藏 - // 供应商和内部后处理实现。正常的用户可见模型仍然保留,以便卡片恢复原有展示。 - let is_audio_result = object - .get("audioKind") - .and_then(Value::as_str) - .is_some_and(|kind| matches!(kind, "sound-effect" | "background-music")); - if !is_audio_result { - object.remove("provider"); - } + object.remove("provider"); if object .get("model") .and_then(Value::as_str) @@ -19708,11 +19699,9 @@ mod tests { } } - /// 画布 Agent 音频回填读的是完整 `EditorAudioGenerateResponse`,其 `provider` 是必填。 - /// 紧凑化对 External API 消费者隐藏 provider(见上一条),但对 Agent 消费者必须保留, - /// 否则 ElevenLabs、扣费、OSS 与画布写回全部成功之后,卡片仍会在四次回填尝试后显示失败。 + /// 画布 Agent 音频回填只读取稳定产物引用;provider 不属于工具卡片或回填 DTO。 #[test] - fn atomic_agent_audio_result_keeps_provider_for_tool_call_backfill() { + fn atomic_agent_audio_result_hides_provider_and_stays_backfillable() { for audio_kind in ["sound-effect", "background-music"] { let mut job = atomic_editor_generation_job_fixture(); job.request_payload_json = json!({ @@ -19748,13 +19737,12 @@ mod tests { .expect("agent audio compact payload should be JSON"); let result = &payload["editor-agent-tool-call-result"]; - assert_eq!(result["provider"], "elevenlabs", "{audio_kind}"); - // 真正的锁:生产端紧凑化后的 payload 必须仍能被消费端的 DTO 反序列化。 + assert!(result.get("provider").is_none(), "{audio_kind}"); + // 生产端紧凑化后的 payload 必须仍能被消费端 DTO 反序列化并完成回填。 let backfilled: EditorAudioGenerateResponse = serde_json::from_value(result.clone()) .unwrap_or_else(|error| { panic!("{audio_kind} agent payload must stay backfillable: {error}") }); - assert_eq!(backfilled.provider, "elevenlabs"); assert_eq!(backfilled.audio_kind, audio_kind); } } diff --git a/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs b/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs index f394044ce..8571fcc42 100644 --- a/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs +++ b/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs @@ -414,7 +414,10 @@ pub(crate) async fn generate_editor_sound_effect_for_owner( .await .map_err(&error_response)?; - Ok(json_success_body(Some(&request_context), response)) + Ok(json_success_body( + Some(&request_context), + sanitize_editor_audio_generation_response_for_user(response), + )) } struct ProductionSoundEffectWorkerBilling<'a> { @@ -966,7 +969,6 @@ fn build_editor_background_music_generate_response( prompt: normalized.gpt_description_prompt.clone(), actual_prompt: Some(normalized.gpt_description_prompt), model: platform_audio::SUNO_DEFAULT_MODEL.to_string(), - provider: generated.provider, task_id: generated.task_id, price_mud_points: normalized.price_mud_points, audio_kind: "background-music".to_string(), @@ -979,6 +981,41 @@ fn build_editor_background_music_generate_response( } } +/// 音频结果同时服务于同步用户响应和 Agent 任务回填。顶层公开 DTO 已不含 provider, +/// 但 project/resource/asset 是兼容历史的 JSON 快照,仍须在用户边界递归剥离,避免 +/// 内部持久化字段经嵌套快照重新暴露。 +fn sanitize_editor_audio_generation_response_for_user( + mut response: assets::EditorAudioGenerateResponse, +) -> assets::EditorAudioGenerateResponse { + for snapshot in [ + &mut response.project, + &mut response.resource, + &mut response.asset, + ] { + if let Some(snapshot) = snapshot.as_mut() { + strip_editor_audio_provider_fields(snapshot); + } + } + response +} + +fn strip_editor_audio_provider_fields(value: &mut Value) { + match value { + Value::Object(fields) => { + fields.remove("provider"); + for nested in fields.values_mut() { + strip_editor_audio_provider_fields(nested); + } + } + Value::Array(items) => { + for item in items { + strip_editor_audio_provider_fields(item); + } + } + _ => {} + } +} + pub(crate) async fn generate_editor_background_music_for_owner( state: AppState, request_context: RequestContext, @@ -1083,22 +1120,24 @@ pub(crate) async fn generate_editor_background_music_for_owner( Ok(json_success_body( Some(&request_context), - build_editor_background_music_generate_response( - audio_src.clone(), - shared_contracts::creation_audio::GeneratedAudioAssetResponse { - kind: AudioAssetSlot::BackgroundMusic.creation_contract_kind(), - task_id: persisted.provider_task_id, - provider: persisted.provider, - status: "completed".to_string(), - asset_object_id: Some(persisted.asset_object_id), - object_key: Some(persisted.object_key), - asset_kind: Some("editor_background_music".to_string()), - audio_src: Some(audio_src), - }, - normalized, - persisted.project, - persisted.resource, - persisted.asset, + sanitize_editor_audio_generation_response_for_user( + build_editor_background_music_generate_response( + audio_src.clone(), + shared_contracts::creation_audio::GeneratedAudioAssetResponse { + kind: AudioAssetSlot::BackgroundMusic.creation_contract_kind(), + task_id: persisted.provider_task_id, + provider: persisted.provider, + status: "completed".to_string(), + asset_object_id: Some(persisted.asset_object_id), + object_key: Some(persisted.object_key), + asset_kind: Some("editor_background_music".to_string()), + audio_src: Some(audio_src), + }, + normalized, + persisted.project, + persisted.resource, + persisted.asset, + ), ), )) } @@ -2049,27 +2088,38 @@ mod tests { #[test] fn editor_background_music_inline_response_keeps_canonical_prompt_equal() { let representative = background_music_prompt_fixture("representative-complex"); - let response = build_editor_background_music_generate_response( - "/generated-editor-audios/background-music.wav".to_string(), - shared_contracts::creation_audio::GeneratedAudioAssetResponse { - kind: - shared_contracts::creation_audio::CreationAudioGenerationKind::BackgroundMusic, - task_id: "task-background-music-1".to_string(), - provider: platform_audio::VECTOR_ENGINE_PROVIDER.to_string(), - status: "completed".to_string(), - asset_object_id: Some("asset-object-1".to_string()), - object_key: Some("generated/editor/background-music.wav".to_string()), - asset_kind: Some("editor_background_music".to_string()), - audio_src: Some("/generated-editor-audios/background-music.wav".to_string()), - }, - NormalizedEditorBackgroundMusicRequest { - gpt_description_prompt: representative.prompt.clone(), - make_instrumental: true, - price_mud_points: 5, - }, - Some(serde_json::json!({ "projectId": "project-1" })), - Some(serde_json::json!({ "resourceId": "resource-1" })), - Some(serde_json::json!({ "assetId": "asset-1" })), + let response = super::sanitize_editor_audio_generation_response_for_user( + build_editor_background_music_generate_response( + "/generated-editor-audios/background-music.wav".to_string(), + shared_contracts::creation_audio::GeneratedAudioAssetResponse { + kind: + shared_contracts::creation_audio::CreationAudioGenerationKind::BackgroundMusic, + task_id: "task-background-music-1".to_string(), + provider: platform_audio::VECTOR_ENGINE_PROVIDER.to_string(), + status: "completed".to_string(), + asset_object_id: Some("asset-object-1".to_string()), + object_key: Some("generated/editor/background-music.wav".to_string()), + asset_kind: Some("editor_background_music".to_string()), + audio_src: Some("/generated-editor-audios/background-music.wav".to_string()), + }, + NormalizedEditorBackgroundMusicRequest { + gpt_description_prompt: representative.prompt.clone(), + make_instrumental: true, + price_mud_points: 5, + }, + Some(serde_json::json!({ + "projectId": "project-1", + "resources": [{ "provider": "vector-engine" }] + })), + Some(serde_json::json!({ + "resourceId": "resource-1", + "provider": "vector-engine" + })), + Some(serde_json::json!({ + "assetId": "asset-1", + "provider": "vector-engine" + })), + ), ); assert_eq!(response.prompt, representative.prompt); @@ -2081,6 +2131,14 @@ mod tests { serde_json::to_value(response).expect("inline response should serialize to JSON"); assert_eq!(serialized["prompt"], serialized["actualPrompt"]); assert_eq!(serialized["prompt"], representative.prompt); + assert!(serialized.get("provider").is_none()); + assert!( + serialized + .pointer("/project/resources/0/provider") + .is_none() + ); + assert!(serialized.pointer("/resource/provider").is_none()); + assert!(serialized.pointer("/asset/provider").is_none()); } #[test] diff --git a/server-rs/crates/api-server/src/vector_engine_audio_generation/sound_effect_worker.rs b/server-rs/crates/api-server/src/vector_engine_audio_generation/sound_effect_worker.rs index 7f6b5f8ef..dd9cdca0f 100644 --- a/server-rs/crates/api-server/src/vector_engine_audio_generation/sound_effect_worker.rs +++ b/server-rs/crates/api-server/src/vector_engine_audio_generation/sound_effect_worker.rs @@ -159,7 +159,6 @@ where prompt: input.user_prompt, actual_prompt: Some(actual_prompt), model: input.model, - provider: platform_audio::ELEVENLABS_PROVIDER.to_string(), task_id: input.task_id, price_mud_points: input.price_mud_points, audio_kind: "sound-effect".to_string(), @@ -513,7 +512,9 @@ mod tests { Some("Bright metallic coin pickup.") ); assert_eq!(response.model, assets::EDITOR_SOUND_EFFECT_MODEL); - assert_eq!(response.provider, platform_audio::ELEVENLABS_PROVIDER); + let response_payload = + serde_json::to_value(&response).expect("sound effect response should serialize"); + assert!(response_payload.get("provider").is_none()); assert_eq!(response.task_id, "job-sfx-t6"); assert_eq!(response.duration_seconds, Some(7.42)); assert_eq!(response.loop_enabled, Some(loop_enabled)); diff --git a/server-rs/crates/shared-contracts/src/assets.rs b/server-rs/crates/shared-contracts/src/assets.rs index 723f6d8a3..f06c23d9c 100644 --- a/server-rs/crates/shared-contracts/src/assets.rs +++ b/server-rs/crates/shared-contracts/src/assets.rs @@ -770,7 +770,6 @@ pub struct EditorAudioGenerateResponse { #[serde(skip_serializing_if = "Option::is_none")] pub actual_prompt: Option, pub model: String, - pub provider: String, pub task_id: String, pub price_mud_points: u32, pub audio_kind: String, @@ -1684,7 +1683,6 @@ mod tests { prompt: "金币掉落叮当声".to_string(), actual_prompt: Some("金币掉落叮当声".to_string()), model: EDITOR_SOUND_EFFECT_MODEL.to_string(), - provider: "elevenlabs".to_string(), task_id: "sound-task-1".to_string(), price_mud_points: 10, audio_kind: "sound-effect".to_string(), @@ -1717,11 +1715,17 @@ mod tests { assert_eq!(response_payload["audioKind"], json!("sound-effect")); assert_eq!(response_payload["durationSeconds"], json!(7.42)); assert_eq!(response_payload["loop"], json!(true)); - assert_eq!(response_payload["provider"], json!("elevenlabs")); + assert!(response_payload.get("provider").is_none()); assert_eq!( response_payload["asset"]["assetKind"], json!("sound-effect") ); + + let mut legacy_payload = response_payload; + legacy_payload["provider"] = json!("elevenlabs"); + let parsed: EditorAudioGenerateResponse = serde_json::from_value(legacy_payload) + .expect("legacy audio results with provider should remain readable"); + assert_eq!(parsed.audio_kind, "sound-effect"); } #[test] diff --git a/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts b/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts index c676c3638..10e8464cd 100644 --- a/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts @@ -462,7 +462,6 @@ describe('ImageCanvasGenerationLayerModel', () => { prompt: '金币掉落叮当声', actualPrompt: '金币掉落叮当声', model: 'audio1.0', - provider: 'VectorEngine', taskId: 'sound-task-1', priceMudPoints: 10, audioKind: 'sound-effect', diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx index b257abc4e..49c22dd88 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx @@ -148,7 +148,6 @@ function createAudioGenerated(overrides = {}) { prompt: '金币掉落叮当声', actualPrompt: '金币掉落叮当声', model: 'audio1.0', - provider: 'VectorEngine', taskId: 'task-audio-generated', priceMudPoints: 5, audioKind: 'sound-effect' as const, diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx index aaece8ec3..30c9a8dbb 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx @@ -1657,7 +1657,6 @@ describe('useImageCanvasGenerationWorkflow', () => { prompt: '金币跳出音', actualPrompt: 'A bright coin pop sound', model: 'eleven_text_to_sound_v2', - provider: 'elevenlabs', taskId: 'task-sound', audioKind: 'sound-effect', durationSeconds: 7.42, diff --git a/src/services/image-editor/editorProjectClient.test.ts b/src/services/image-editor/editorProjectClient.test.ts index 59d5feddf..3d0a3bfb0 100644 --- a/src/services/image-editor/editorProjectClient.test.ts +++ b/src/services/image-editor/editorProjectClient.test.ts @@ -1813,7 +1813,6 @@ describe('editorProjectClient', () => { prompt: '金币掉落叮当声', actualPrompt: 'A bright coin pickup chime', model: 'eleven_text_to_sound_v2', - provider: 'elevenlabs', taskId: 'sound-task-1', audioKind: 'sound-effect', asset: { @@ -1867,7 +1866,6 @@ describe('editorProjectClient', () => { prompt: '按钮确认短促音', actualPrompt: 'A short confirmation click', model: 'eleven_text_to_sound_v2', - provider: 'elevenlabs', taskId: 'sound-task-null', audioKind: 'sound-effect', }); @@ -1969,7 +1967,6 @@ describe('editorProjectClient', () => { prompt: canonicalPrompt, actualPrompt: canonicalPrompt, model: 'chirp-v4', - provider: 'VectorEngine', taskId: 'music-task-1', audioKind: 'background-music', }); diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index e5c016d26..f7e90bbd9 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -596,7 +596,6 @@ export type EditorAudioGenerationResult = { prompt: string; actualPrompt?: string | null; model: string; - provider: string; taskId: string; priceMudPoints: number; audioKind: 'sound-effect' | 'background-music';