1
This commit is contained in:
@@ -258,7 +258,7 @@ pub async fn generate_puzzle_onboarding_work(
|
||||
profile_id: format!("onboarding-profile-{now}"),
|
||||
owner_user_id: "onboarding-guest".to_string(),
|
||||
source_session_id: None,
|
||||
author_display_name: "百梦主".to_string(),
|
||||
author_display_name: "陶泥儿主".to_string(),
|
||||
work_title: level_name.clone(),
|
||||
work_description: prompt_text.clone(),
|
||||
level_name,
|
||||
@@ -3436,18 +3436,20 @@ fn attach_puzzle_level_ui_background(
|
||||
levels[index].ui_background_image_object_key = Some(generated.object_key);
|
||||
}
|
||||
|
||||
async fn try_generate_puzzle_background_music(
|
||||
async fn generate_puzzle_background_music_required(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
session_id: &str,
|
||||
profile_id: &str,
|
||||
title: &str,
|
||||
) -> Option<CreationAudioAsset> {
|
||||
) -> Result<CreationAudioAsset, AppError> {
|
||||
let normalized_title = title.trim();
|
||||
if normalized_title.is_empty() {
|
||||
return None;
|
||||
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"message": "拼图草稿背景音乐名称为空,无法完成背景音乐生成",
|
||||
})));
|
||||
}
|
||||
match generate_background_music_asset_for_creation(
|
||||
generate_background_music_asset_for_creation(
|
||||
state,
|
||||
owner_user_id,
|
||||
String::new(),
|
||||
@@ -3464,50 +3466,72 @@ async fn try_generate_puzzle_background_music(
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(music) => Some(music),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
session_id,
|
||||
profile_id,
|
||||
error = %error,
|
||||
"拼图草稿背景音乐生成失败,保留草稿并允许结果页重试"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_generate_puzzle_initial_ui_background(
|
||||
async fn generate_puzzle_initial_ui_background_required(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
session_id: &str,
|
||||
draft: &PuzzleResultDraftRecord,
|
||||
target_level: &PuzzleDraftLevelRecord,
|
||||
) -> Option<(String, GeneratedPuzzleUiBackgroundResponse)> {
|
||||
) -> Result<(String, GeneratedPuzzleUiBackgroundResponse), AppError> {
|
||||
let prompt = normalize_puzzle_ui_background_prompt("", draft, target_level);
|
||||
match generate_puzzle_ui_background_image(
|
||||
let generated = generate_puzzle_ui_background_image(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
target_level.level_name.as_str(),
|
||||
prompt.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(generated) => Some((prompt, generated)),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
session_id,
|
||||
level_id = %target_level.level_id,
|
||||
error = %error,
|
||||
"拼图草稿 UI 背景图自动生成失败,保留草稿并允许结果页重试"
|
||||
);
|
||||
None
|
||||
}
|
||||
.await?;
|
||||
Ok((prompt, generated))
|
||||
}
|
||||
|
||||
fn ensure_puzzle_initial_level_assets_ready(
|
||||
level: &PuzzleDraftLevelRecord,
|
||||
) -> Result<(), AppError> {
|
||||
let has_background_music = level
|
||||
.background_music
|
||||
.as_ref()
|
||||
.map(|music| music.audio_src.trim())
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let has_ui_background = level
|
||||
.ui_background_image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
|| level
|
||||
.ui_background_image_object_key
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
if has_background_music && has_ui_background {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut missing = Vec::new();
|
||||
if !has_background_music {
|
||||
missing.push("背景音乐");
|
||||
}
|
||||
if !has_ui_background {
|
||||
missing.push("UI背景图");
|
||||
}
|
||||
|
||||
Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"message": format!("拼图草稿资源生成未完成:缺少{}", missing.join("、")),
|
||||
"missingAssets": missing,
|
||||
})))
|
||||
}
|
||||
|
||||
fn find_puzzle_level_for_initial_asset_check<'a>(
|
||||
levels: &'a [PuzzleDraftLevelRecord],
|
||||
level_id: &str,
|
||||
) -> Option<&'a PuzzleDraftLevelRecord> {
|
||||
levels
|
||||
.iter()
|
||||
.find(|level| level.level_id == level_id)
|
||||
.or_else(|| levels.first())
|
||||
}
|
||||
|
||||
async fn compile_puzzle_draft_with_initial_cover(
|
||||
@@ -3587,38 +3611,41 @@ async fn compile_puzzle_draft_with_initial_cover(
|
||||
let mut updated_levels =
|
||||
build_puzzle_levels_with_primary_update(&draft, &target_level, reference_image_src);
|
||||
let music_title = resolve_puzzle_background_music_title(&draft, &target_level);
|
||||
// 中文注释:音乐和 UI 背景都只依赖最终关卡名与草稿快照,名称确定后即可并行生成。
|
||||
let (music_result, ui_background_result) = tokio::join!(
|
||||
try_generate_puzzle_background_music(
|
||||
// 中文注释:UI 背景先生成,避免其失败后留下已经扣费但未写入草稿的音乐资产。
|
||||
let (ui_prompt, ui_background) = generate_puzzle_initial_ui_background_required(
|
||||
state,
|
||||
owner_user_id.as_str(),
|
||||
compiled_session.session_id.as_str(),
|
||||
&draft,
|
||||
&target_level,
|
||||
)
|
||||
.await?;
|
||||
attach_puzzle_level_ui_background(
|
||||
&mut updated_levels,
|
||||
target_level.level_id.as_str(),
|
||||
ui_prompt,
|
||||
ui_background,
|
||||
);
|
||||
attach_puzzle_level_background_music(
|
||||
&mut updated_levels,
|
||||
target_level.level_id.as_str(),
|
||||
generate_puzzle_background_music_required(
|
||||
state,
|
||||
owner_user_id.as_str(),
|
||||
compiled_session.session_id.as_str(),
|
||||
profile_id.as_str(),
|
||||
music_title.as_str(),
|
||||
),
|
||||
try_generate_puzzle_initial_ui_background(
|
||||
state,
|
||||
owner_user_id.as_str(),
|
||||
compiled_session.session_id.as_str(),
|
||||
&draft,
|
||||
&target_level,
|
||||
),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
if let Some(music) = music_result {
|
||||
attach_puzzle_level_background_music(
|
||||
&mut updated_levels,
|
||||
target_level.level_id.as_str(),
|
||||
music,
|
||||
);
|
||||
}
|
||||
if let Some((ui_prompt, ui_background)) = ui_background_result {
|
||||
attach_puzzle_level_ui_background(
|
||||
&mut updated_levels,
|
||||
target_level.level_id.as_str(),
|
||||
ui_prompt,
|
||||
ui_background,
|
||||
);
|
||||
}
|
||||
let ready_level =
|
||||
find_puzzle_level_for_initial_asset_check(&updated_levels, target_level.level_id.as_str())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"message": "拼图草稿资源生成完成后未找到目标关卡",
|
||||
}))
|
||||
})?;
|
||||
ensure_puzzle_initial_level_assets_ready(ready_level)?;
|
||||
let levels_json_with_generated_name =
|
||||
Some(serialize_puzzle_level_records_for_module(&updated_levels)?);
|
||||
let candidates_json = serde_json::to_string(
|
||||
@@ -3794,38 +3821,41 @@ async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
let mut updated_levels =
|
||||
build_puzzle_levels_with_primary_update(&draft, &target_level, reference_image_src);
|
||||
let music_title = resolve_puzzle_background_music_title(&draft, &target_level);
|
||||
// 中文注释:直用上传图时,名称分支和上传图落库完成后,再并行补齐音乐与 UI 背景。
|
||||
let (music_result, ui_background_result) = tokio::join!(
|
||||
try_generate_puzzle_background_music(
|
||||
// 中文注释:直用上传图时同样先补 UI 背景,再生成会单独扣费的音乐资产。
|
||||
let (ui_prompt, ui_background) = generate_puzzle_initial_ui_background_required(
|
||||
state,
|
||||
owner_user_id.as_str(),
|
||||
compiled_session.session_id.as_str(),
|
||||
&draft,
|
||||
&target_level,
|
||||
)
|
||||
.await?;
|
||||
attach_puzzle_level_ui_background(
|
||||
&mut updated_levels,
|
||||
target_level.level_id.as_str(),
|
||||
ui_prompt,
|
||||
ui_background,
|
||||
);
|
||||
attach_puzzle_level_background_music(
|
||||
&mut updated_levels,
|
||||
target_level.level_id.as_str(),
|
||||
generate_puzzle_background_music_required(
|
||||
state,
|
||||
owner_user_id.as_str(),
|
||||
compiled_session.session_id.as_str(),
|
||||
profile_id.as_str(),
|
||||
music_title.as_str(),
|
||||
),
|
||||
try_generate_puzzle_initial_ui_background(
|
||||
state,
|
||||
owner_user_id.as_str(),
|
||||
compiled_session.session_id.as_str(),
|
||||
&draft,
|
||||
&target_level,
|
||||
),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
if let Some(music) = music_result {
|
||||
attach_puzzle_level_background_music(
|
||||
&mut updated_levels,
|
||||
target_level.level_id.as_str(),
|
||||
music,
|
||||
);
|
||||
}
|
||||
if let Some((ui_prompt, ui_background)) = ui_background_result {
|
||||
attach_puzzle_level_ui_background(
|
||||
&mut updated_levels,
|
||||
target_level.level_id.as_str(),
|
||||
ui_prompt,
|
||||
ui_background,
|
||||
);
|
||||
}
|
||||
let ready_level =
|
||||
find_puzzle_level_for_initial_asset_check(&updated_levels, target_level.level_id.as_str())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"message": "拼图草稿资源生成完成后未找到目标关卡",
|
||||
}))
|
||||
})?;
|
||||
ensure_puzzle_initial_level_assets_ready(ready_level)?;
|
||||
let levels_json_with_generated_name =
|
||||
Some(serialize_puzzle_level_records_for_module(&updated_levels)?);
|
||||
let candidate = PuzzleGeneratedImageCandidateRecord {
|
||||
@@ -5268,6 +5298,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_initial_draft_assets_must_include_music_and_ui_background() {
|
||||
let mut draft = test_puzzle_draft_record();
|
||||
let missing_all = ensure_puzzle_initial_level_assets_ready(&draft.levels[0])
|
||||
.expect_err("缺少自动生成资产时不能把草稿标记为完成");
|
||||
assert_eq!(missing_all.status_code(), StatusCode::BAD_GATEWAY);
|
||||
assert!(missing_all.body_text().contains("背景音乐"));
|
||||
assert!(missing_all.body_text().contains("UI背景图"));
|
||||
|
||||
draft.levels[0].ui_background_image_src =
|
||||
Some("/generated-puzzle-assets/session/ui/background.png".to_string());
|
||||
let missing_music = ensure_puzzle_initial_level_assets_ready(&draft.levels[0])
|
||||
.expect_err("只有 UI 背景时仍不能完成草稿");
|
||||
assert!(missing_music.body_text().contains("背景音乐"));
|
||||
|
||||
draft.levels[0].background_music = Some(PuzzleAudioAssetRecord {
|
||||
task_id: "suno-task-1".to_string(),
|
||||
provider: "vector-engine-suno".to_string(),
|
||||
asset_object_id: Some("assetobj_1".to_string()),
|
||||
asset_kind: Some("puzzle_background_music".to_string()),
|
||||
audio_src: "/generated-puzzle-assets/session/music.mp3".to_string(),
|
||||
prompt: Some(String::new()),
|
||||
title: Some("雨夜猫街".to_string()),
|
||||
updated_at: Some("2026-05-14T00:00:00Z".to_string()),
|
||||
});
|
||||
|
||||
ensure_puzzle_initial_level_assets_ready(&draft.levels[0])
|
||||
.expect("音乐和 UI 背景都存在时才能完成自动草稿");
|
||||
}
|
||||
|
||||
fn test_puzzle_anchor_pack_record() -> PuzzleAnchorPackRecord {
|
||||
let item = PuzzleAnchorItemRecord {
|
||||
key: "visualSubject".to_string(),
|
||||
@@ -5391,7 +5451,7 @@ mod tests {
|
||||
}));
|
||||
let other_error = AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "spacetimedb",
|
||||
"message": "光点余额不足",
|
||||
"message": "泥点余额不足",
|
||||
}));
|
||||
|
||||
assert!(should_sync_puzzle_freeze_boundary(&invalid_operation, true));
|
||||
|
||||
Reference in New Issue
Block a user