修复 inline 泄露 provider问题
This commit is contained in:
@@ -20,8 +20,8 @@ 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,
|
||||
EDITOR_BACKGROUND_MUSIC_GENERATION_JOB_KIND, 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,
|
||||
@@ -218,6 +218,9 @@ fn user_visible_external_generation_error(job_kind: &str, error: Option<String>)
|
||||
if job_kind == EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND && error.is_some() {
|
||||
return Some("音效生成失败,请稍后重试。".to_string());
|
||||
}
|
||||
if job_kind == EDITOR_BACKGROUND_MUSIC_GENERATION_JOB_KIND && error.is_some() {
|
||||
return Some("背景音乐生成失败,请稍后重试。".to_string());
|
||||
}
|
||||
|
||||
error
|
||||
}
|
||||
@@ -519,6 +522,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_music_failure_hides_provider_and_transport_details_from_owner() {
|
||||
let message = user_visible_external_generation_error(
|
||||
EDITOR_BACKGROUND_MUSIC_GENERATION_JOB_KIND,
|
||||
Some(
|
||||
"提交 Vector Engine 背景音乐任务失败:error sending request for url \
|
||||
(https://vector-engine.internal/v1/audio/generations): connection error: \
|
||||
Connection refused (os error 111)"
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(message.as_deref(), Some("背景音乐生成失败,请稍后重试。"));
|
||||
let lower = message
|
||||
.expect("失败任务应返回稳定文案")
|
||||
.to_ascii_lowercase();
|
||||
for forbidden in ["vector engine", "vector-engine", "https://", "os error"] {
|
||||
assert!(
|
||||
!lower.contains(forbidden),
|
||||
"普通用户文案不应包含 {forbidden}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_generation_failure_keeps_its_user_visible_message() {
|
||||
let message = user_visible_external_generation_error(
|
||||
|
||||
@@ -314,6 +314,14 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(mut payload) = parse_json_payload(&request_context, payload)?;
|
||||
let owner_user_id = caller.owner_user_id.clone();
|
||||
let error_response = |error| {
|
||||
editor_audio_generation_error_response(
|
||||
&request_context,
|
||||
&caller,
|
||||
"音效生成失败,请稍后重试。",
|
||||
error,
|
||||
)
|
||||
};
|
||||
let target = preflight_editor_billable_generation_target(
|
||||
&state,
|
||||
owner_user_id.as_str(),
|
||||
@@ -321,7 +329,7 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
|
||||
payload.asset_folder_id.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
payload.project_id = target.project_id;
|
||||
payload.asset_folder_id = target.asset_folder_id;
|
||||
let project_id = payload.project_id.clone();
|
||||
@@ -337,16 +345,15 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
|
||||
"message": error.to_string(),
|
||||
}))
|
||||
})
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let normalized = normalize_editor_sound_effect_request_with_pricing(payload.clone(), &pricing)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let request_deadline = request_context.external_call_deadline();
|
||||
let mut settings = require_elevenlabs_audio_settings(&state)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
let mut settings = require_elevenlabs_audio_settings(&state).map_err(&error_response)?;
|
||||
settings.request_deadline = request_deadline;
|
||||
let http_client = platform_audio::build_elevenlabs_audio_http_client(&settings)
|
||||
.map_err(map_platform_audio_error)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let llm_client = state
|
||||
.editor_agent_llm_client()
|
||||
.ok_or_else(|| {
|
||||
@@ -356,18 +363,17 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
|
||||
"message": "音效 Prompt 英文化服务未配置",
|
||||
}))
|
||||
})
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let target = build_editor_audio_target(
|
||||
require_editor_audio_operation(&caller)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?
|
||||
.map_err(&error_response)?
|
||||
.operation_id
|
||||
.as_str(),
|
||||
"sound_effect",
|
||||
"editor_sound_effect",
|
||||
Some(u64::from(normalized.price_mud_points)),
|
||||
);
|
||||
let operation = require_editor_audio_operation(&caller)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
let operation = require_editor_audio_operation(&caller).map_err(&error_response)?;
|
||||
let stages = ProductionSoundEffectWorkerStages {
|
||||
state: &state,
|
||||
caller: &caller,
|
||||
@@ -406,7 +412,7 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
|
||||
Ok(json_success_body(Some(&request_context), response))
|
||||
}
|
||||
@@ -981,6 +987,14 @@ pub(crate) async fn generate_editor_background_music_for_owner(
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(mut payload) = parse_json_payload(&request_context, payload)?;
|
||||
let owner_user_id = caller.owner_user_id.clone();
|
||||
let error_response = |error| {
|
||||
editor_audio_generation_error_response(
|
||||
&request_context,
|
||||
&caller,
|
||||
"背景音乐生成失败,请稍后重试。",
|
||||
error,
|
||||
)
|
||||
};
|
||||
let target = preflight_editor_billable_generation_target(
|
||||
&state,
|
||||
owner_user_id.as_str(),
|
||||
@@ -988,7 +1002,7 @@ pub(crate) async fn generate_editor_background_music_for_owner(
|
||||
payload.asset_folder_id.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
payload.project_id = target.project_id;
|
||||
payload.asset_folder_id = target.asset_folder_id;
|
||||
let project_id = payload.project_id.clone();
|
||||
@@ -1005,14 +1019,13 @@ pub(crate) async fn generate_editor_background_music_for_owner(
|
||||
"message": error.to_string(),
|
||||
}))
|
||||
})
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let normalized = normalize_editor_background_music_request_with_pricing(payload, &pricing)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
let settings = require_vector_engine_audio_settings(&state)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let settings = require_vector_engine_audio_settings(&state).map_err(&error_response)?;
|
||||
let http_client = platform_audio::build_vector_engine_audio_http_client(&settings)
|
||||
.map_err(map_platform_audio_error)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let task = platform_audio::submit_editor_background_music_task(
|
||||
&http_client,
|
||||
&settings,
|
||||
@@ -1024,10 +1037,10 @@ pub(crate) async fn generate_editor_background_music_for_owner(
|
||||
)
|
||||
.await
|
||||
.map_err(map_platform_audio_error)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let target = build_editor_audio_target(
|
||||
require_editor_audio_operation(&caller)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?
|
||||
.map_err(&error_response)?
|
||||
.operation_id
|
||||
.as_str(),
|
||||
"background_music",
|
||||
@@ -1045,7 +1058,7 @@ pub(crate) async fn generate_editor_background_music_for_owner(
|
||||
target,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
let audio_src = generated.audio_src.clone();
|
||||
let persisted = persist_editor_audio_generation(
|
||||
&state,
|
||||
@@ -1066,7 +1079,7 @@ pub(crate) async fn generate_editor_background_music_for_owner(
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
.map_err(&error_response)?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -1158,6 +1171,29 @@ fn build_inline_editor_audio_caller<T: serde::Serialize>(
|
||||
})
|
||||
}
|
||||
|
||||
/// 编辑器音频的 inline 兼容模式会把生成结果直接回给普通用户;provider / 传输诊断只能
|
||||
/// 留在 worker 路径和服务端审计中,不能随这个同步 HTTP 响应泄露出去。
|
||||
///
|
||||
/// 队列 worker 带有 `worker_id`,仍保留原始 `AppError` 响应以便现有 worker 抽取失败文本、
|
||||
/// 写入任务记录并由任务读取边界统一脱敏。4xx 也保持原状,避免丢失用户可操作的校验提示。
|
||||
fn editor_audio_generation_error_response(
|
||||
request_context: &RequestContext,
|
||||
caller: &EditorGenerationCaller,
|
||||
user_message: &'static str,
|
||||
error: AppError,
|
||||
) -> Response {
|
||||
let is_inline_user_request = caller
|
||||
.operation
|
||||
.as_ref()
|
||||
.is_some_and(|operation| operation.worker_id.is_none());
|
||||
if is_inline_user_request && error.status_code().is_server_error() {
|
||||
return AppError::from_status(error.status_code())
|
||||
.with_message(user_message)
|
||||
.into_response_with_context(Some(request_context));
|
||||
}
|
||||
error.into_response_with_context(Some(request_context))
|
||||
}
|
||||
|
||||
fn require_editor_audio_operation(
|
||||
caller: &EditorGenerationCaller,
|
||||
) -> Result<&EditorGenerationOperationContext, AppError> {
|
||||
@@ -1581,6 +1617,7 @@ fn editor_audio_bad_request(message: impl Into<String>) -> AppError {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use axum::{body::to_bytes, http::StatusCode};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::assets;
|
||||
@@ -1590,8 +1627,8 @@ mod tests {
|
||||
CanonicalEditorBackgroundMusicSubmissionPayload, NormalizedEditorBackgroundMusicRequest,
|
||||
NormalizedEditorSoundEffectRequest, build_editor_background_music_generate_response,
|
||||
build_inline_editor_audio_caller, build_sound_effect_job_result_payload,
|
||||
normalize_editor_background_music_request, normalize_editor_sound_effect_request,
|
||||
prepare_editor_background_music_queue_job,
|
||||
editor_audio_generation_error_response, normalize_editor_background_music_request,
|
||||
normalize_editor_sound_effect_request, prepare_editor_background_music_queue_job,
|
||||
};
|
||||
use crate::{
|
||||
editor_generation_queue::{
|
||||
@@ -1599,6 +1636,7 @@ mod tests {
|
||||
serialize_editor_generation_job_payload_for_test,
|
||||
},
|
||||
editor_project::{EditorGenerationCaller, EditorGenerationOperationContext},
|
||||
http_error::AppError,
|
||||
request_context::RequestContext,
|
||||
};
|
||||
|
||||
@@ -1690,6 +1728,112 @@ mod tests {
|
||||
assert!(operation.lease_token.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn inline_audio_server_error_hides_provider_diagnostics_in_both_response_shapes() {
|
||||
for wants_envelope in [false, true] {
|
||||
let request_context = RequestContext::new(
|
||||
format!("request-audio-error-{wants_envelope}"),
|
||||
"POST /api/editor/audios/sound-effects/generations".to_string(),
|
||||
std::time::Duration::ZERO,
|
||||
wants_envelope,
|
||||
);
|
||||
let payload = sound_effect_payload("机械齿轮转动", None, None, false);
|
||||
let caller = build_inline_editor_audio_caller(
|
||||
"user-1".to_string(),
|
||||
&request_context,
|
||||
EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND,
|
||||
&payload,
|
||||
)
|
||||
.expect("inline caller should be prepared");
|
||||
let response = editor_audio_generation_error_response(
|
||||
&request_context,
|
||||
&caller,
|
||||
"音效生成失败,请稍后重试。",
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "elevenlabs",
|
||||
"message": "请求 ElevenLabs 音效生成失败:connection refused",
|
||||
"endpoint": "https://api.elevenlabs.test/v1/sound-generation",
|
||||
"source": "tcp connect error",
|
||||
"status": 502,
|
||||
})),
|
||||
);
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
let bytes = to_bytes(response.into_body(), 64 * 1024)
|
||||
.await
|
||||
.expect("error response body should be readable");
|
||||
let response_payload: Value =
|
||||
serde_json::from_slice(&bytes).expect("error response should be JSON");
|
||||
|
||||
assert_eq!(
|
||||
response_payload["error"]["message"],
|
||||
json!("音效生成失败,请稍后重试。")
|
||||
);
|
||||
assert!(response_payload["error"].get("details").is_none());
|
||||
let serialized = response_payload.to_string().to_ascii_lowercase();
|
||||
for forbidden in [
|
||||
"elevenlabs",
|
||||
"api.elevenlabs.test",
|
||||
"sound-generation",
|
||||
"tcp connect error",
|
||||
"\"endpoint\"",
|
||||
"\"source\"",
|
||||
] {
|
||||
assert!(
|
||||
!serialized.contains(forbidden),
|
||||
"inline 用户响应不应包含 {forbidden}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_audio_server_error_keeps_diagnostics_for_worker_processing() {
|
||||
let request_context = RequestContext::new(
|
||||
"request-audio-worker-error".to_string(),
|
||||
"external-generation-worker".to_string(),
|
||||
std::time::Duration::ZERO,
|
||||
true,
|
||||
);
|
||||
let caller = EditorGenerationCaller {
|
||||
owner_user_id: "user-1".to_string(),
|
||||
audit_subject_user_id: Some("user-1".to_string()),
|
||||
audit_project_id: None,
|
||||
phase_reporter: None,
|
||||
operation: Some(EditorGenerationOperationContext {
|
||||
operation_kind: EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND.to_string(),
|
||||
operation_id: "task-audio-worker-error".to_string(),
|
||||
operation_fingerprint: "fingerprint-audio-worker-error".to_string(),
|
||||
worker_id: Some("worker-audio-1".to_string()),
|
||||
lease_token: Some("lease-audio-1".to_string()),
|
||||
queue_result_context: None,
|
||||
}),
|
||||
};
|
||||
let response = editor_audio_generation_error_response(
|
||||
&request_context,
|
||||
&caller,
|
||||
"音效生成失败,请稍后重试。",
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "elevenlabs",
|
||||
"endpoint": "https://api.elevenlabs.test/v1/sound-generation",
|
||||
"source": "tcp connect error",
|
||||
})),
|
||||
);
|
||||
let bytes = to_bytes(response.into_body(), 64 * 1024)
|
||||
.await
|
||||
.expect("worker error response body should be readable");
|
||||
let response_payload: Value =
|
||||
serde_json::from_slice(&bytes).expect("worker error response should be JSON");
|
||||
|
||||
assert_eq!(
|
||||
response_payload["error"]["details"]["provider"],
|
||||
json!("elevenlabs")
|
||||
);
|
||||
assert_eq!(
|
||||
response_payload["error"]["details"]["endpoint"],
|
||||
json!("https://api.elevenlabs.test/v1/sound-generation")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_queue_result_keeps_required_editor_agent_response_fields() {
|
||||
let source = include_str!("generation.rs");
|
||||
|
||||
Reference in New Issue
Block a user