diff --git a/server-rs/crates/api-server/src/puzzle_clear.rs b/server-rs/crates/api-server/src/puzzle_clear.rs index 053db575..589686e6 100644 --- a/server-rs/crates/api-server/src/puzzle_clear.rs +++ b/server-rs/crates/api-server/src/puzzle_clear.rs @@ -1186,103 +1186,120 @@ async fn maybe_prepare_puzzle_clear_assets_inner( let http_client = build_openai_image_http_client(&settings).map_err(|error| { puzzle_clear_error_response(request_context, PUZZLE_CLEAR_CREATION_PROVIDER, error) })?; + + // 中文注释:5 张 sheet 并行生成,每张内部最多重试 4 次上游错误 + let theme_prompt = payload.theme_prompt.as_deref().unwrap_or_default(); let mut generated_sheets = Vec::with_capacity(sheet_specs.len()); - for sheet_spec in sheet_specs { - let sheet_prompt = build_puzzle_clear_atlas_prompt( - payload.theme_prompt.as_deref().unwrap_or_default(), - &sheet_spec, - ); - let mut accepted_sheet = None; - for attempt_index in 0..PUZZLE_CLEAR_SHEET_GENERATION_MAX_ATTEMPTS { - let failure_context = format!( - "拼消消素材 {} 生成失败,第 {} 次", - sheet_spec.sheet_id, - attempt_index + 1 - ); - if let Some(debug_run) = image_debug_run.as_ref() { - debug_run.record_sheet_request(&sheet_spec, attempt_index, sheet_prompt.as_str()); - } - let generated = match create_openai_image_generation( - &http_client, - &settings, - sheet_prompt.as_str(), - Some(PUZZLE_CLEAR_ATLAS_NEGATIVE_PROMPT), - PUZZLE_CLEAR_ATLAS_GENERATION_SIZE, - 1, - &[], - failure_context.as_str(), - ) - .await - { - Ok(generated) => generated, - Err(error) - if attempt_index + 1 < PUZZLE_CLEAR_SHEET_GENERATION_MAX_ATTEMPTS - && is_retryable_puzzle_clear_sheet_generation_error(&error) => - { - if let Some(debug_run) = image_debug_run.as_ref() { - debug_run.record_sheet_generation_error(&sheet_spec, attempt_index, &error); + { + let futures: Vec<_> = sheet_specs + .iter() + .map(|sheet_spec| { + let sheet_prompt = build_puzzle_clear_atlas_prompt(theme_prompt, sheet_spec); + let client = http_client.clone(); + let settings = settings.clone(); + let debug_run = image_debug_run.clone(); + async move { + for attempt_index in 0..PUZZLE_CLEAR_SHEET_GENERATION_MAX_ATTEMPTS { + let failure_context = format!( + "拼消消素材 {} 生成失败,第 {} 次", + sheet_spec.sheet_id, + attempt_index + 1 + ); + if let Some(ref debug_run) = debug_run { + debug_run.record_sheet_request( + sheet_spec, + attempt_index, + sheet_prompt.as_str(), + ); + } + let generated = match create_openai_image_generation( + &client, + &settings, + sheet_prompt.as_str(), + Some(PUZZLE_CLEAR_ATLAS_NEGATIVE_PROMPT), + PUZZLE_CLEAR_ATLAS_GENERATION_SIZE, + 1, + &[], + failure_context.as_str(), + ) + .await + { + Ok(generated) => generated, + Err(error) + if attempt_index + 1 + < PUZZLE_CLEAR_SHEET_GENERATION_MAX_ATTEMPTS + && is_retryable_puzzle_clear_sheet_generation_error(&error) => + { + if let Some(ref debug_run) = debug_run { + debug_run.record_sheet_generation_error( + sheet_spec, + attempt_index, + &error, + ); + } + tracing::warn!( + provider = PUZZLE_CLEAR_CREATION_PROVIDER, + sheet_id = sheet_spec.sheet_id, + attempt = attempt_index + 1, + generation_error = %error.body_text(), + "拼消消素材 sheet 生成遇到可重试上游错误,准备重试" + ); + continue; + } + Err(error) => { + return Err(error); + } + }; + let task_id = generated.task_id; + let actual_prompt = generated.actual_prompt; + let image = generated.images.into_iter().next().ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "vector-engine", + "message": format!("拼消消素材 {} 生成成功但未返回图片。", sheet_spec.sheet_id), + })) + })?; + if let Some(ref debug_run) = debug_run { + debug_run.record_sheet_attempt_image( + sheet_spec, + attempt_index, + task_id.as_str(), + actual_prompt.as_deref(), + &image, + ); + debug_run.record_sheet_accepted( + sheet_spec, + task_id.as_str(), + &image, + ); + } + return Ok(PuzzleClearGeneratedSheet { + spec: *sheet_spec, + prompt: sheet_prompt.clone(), + task_id, + image, + }); } - tracing::warn!( - provider = PUZZLE_CLEAR_CREATION_PROVIDER, - sheet_id = sheet_spec.sheet_id, - attempt = attempt_index + 1, - generation_error = %error.body_text(), - "拼消消素材 sheet 生成遇到可重试上游错误,准备重试" - ); - continue; + Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": PUZZLE_CLEAR_CREATION_PROVIDER, + "message": format!("拼消消素材 {} 多次生成后仍未得到可切图集。", sheet_spec.sheet_id), + }))) } + }) + .collect(); + + let results = futures_util::future::join_all(futures).await; + for result in results { + match result { + Ok(sheet) => generated_sheets.push(sheet), Err(error) => { - if let Some(debug_run) = image_debug_run.as_ref() { - debug_run.record_sheet_generation_error(&sheet_spec, attempt_index, &error); - } return Err(puzzle_clear_error_response( request_context, PUZZLE_CLEAR_CREATION_PROVIDER, error, )); } - }; - let task_id = generated.task_id; - let actual_prompt = generated.actual_prompt; - let image = generated.images.into_iter().next().ok_or_else(|| { - puzzle_clear_error_response( - request_context, - PUZZLE_CLEAR_CREATION_PROVIDER, - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "vector-engine", - "message": format!("拼消消素材 {} 生成成功但未返回图片。", sheet_spec.sheet_id), - })), - ) - })?; - if let Some(debug_run) = image_debug_run.as_ref() { - debug_run.record_sheet_attempt_image( - &sheet_spec, - attempt_index, - task_id.as_str(), - actual_prompt.as_deref(), - &image, - ); - debug_run.record_sheet_accepted(&sheet_spec, task_id.as_str(), &image); } - accepted_sheet = Some(PuzzleClearGeneratedSheet { - spec: sheet_spec, - prompt: sheet_prompt.clone(), - task_id, - image, - }); - break; } - let Some(accepted_sheet) = accepted_sheet else { - return Err(puzzle_clear_error_response( - request_context, - PUZZLE_CLEAR_CREATION_PROVIDER, - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": PUZZLE_CLEAR_CREATION_PROVIDER, - "message": format!("拼消消素材 {} 多次生成后仍未得到可切图集。", sheet_spec.sheet_id), - })), - )); - }; - generated_sheets.push(accepted_sheet); } let mut slices = Vec::new();