fix: stabilize puzzle vector engine asset generation

This commit is contained in:
2026-06-03 02:40:07 +08:00
parent 67ba40c678
commit 08577b66c5
6 changed files with 215 additions and 28 deletions

View File

@@ -341,6 +341,8 @@ fn record_external_api_failure_otlp(failure: &ExternalApiFailureDraft) {
prompt_chars = failure.prompt_chars,
reference_image_count = failure.reference_image_count,
image_model = failure.image_model,
request_id = %failure.request_id.as_deref().unwrap_or_default(),
error_source = %failure.error_source.as_deref().unwrap_or_default(),
error = %failure.error_message,
"外部 API 调用失败"
);
@@ -394,6 +396,10 @@ mod tests {
)
.with_status_code(Some(429))
.with_retryable(true)
.with_error_source(Some(
"client error (SendRequest) -> connection closed before message completed"
.to_string(),
))
.with_latency_ms(Some(1234))
.with_prompt_chars(Some(88))
.with_reference_image_count(Some(2))
@@ -414,6 +420,10 @@ mod tests {
assert_eq!(metadata["promptChars"], 88);
assert_eq!(metadata["referenceImageCount"], 2);
assert_eq!(metadata["imageModel"], "gpt-image-2-all");
assert_eq!(
metadata["errorSource"],
"client error (SendRequest) -> connection closed before message completed"
);
assert!(matches!(metadata["occurredAt"], Value::String(_)));
}

View File

@@ -424,6 +424,7 @@ pub(crate) fn map_platform_image_error(error: PlatformImageError) -> AppError {
details["referenceImageCount"] = json!(audit.reference_image_count);
details["imageModel"] = json!(audit.image_model);
details["rawExcerpt"] = json!(audit.raw_excerpt);
details["errorSource"] = json!(audit.error_source);
}
AppError::from_status(status).with_details(details)

View File

@@ -317,7 +317,16 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
);
let http_client = build_puzzle_image_http_client(state, PuzzleImageModel::GptImage2)?;
let puzzle_reference = build_puzzle_downloaded_image_reference(puzzle_image);
let scene_generated = create_puzzle_vector_engine_image_generation(
let bundle_started_at = Instant::now();
tracing::info!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
"拼图关卡资产包生成开始"
);
let scene_started_at = Instant::now();
let scene_generated = match create_puzzle_vector_engine_image_generation(
&http_client,
&settings,
PuzzleImageModel::GptImage2,
@@ -328,7 +337,34 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
Some(&puzzle_reference),
)
.await
.map_err(map_puzzle_generation_endpoint_error)?;
.map_err(map_puzzle_generation_endpoint_error)
{
Ok(generated) => {
tracing::info!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
slot = "level_scene",
elapsed_ms = scene_started_at.elapsed().as_millis() as u64,
"拼图关卡场景图生成完成"
);
generated
}
Err(error) => {
tracing::warn!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
slot = "level_scene",
elapsed_ms = scene_started_at.elapsed().as_millis() as u64,
error = %error,
"拼图关卡场景图生成失败"
);
return Err(error);
}
};
let scene_image = scene_generated.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": VECTOR_ENGINE_PROVIDER,
@@ -336,7 +372,8 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
}))
})?;
let scene_reference = build_puzzle_downloaded_image_reference(&scene_image);
let scene_persist_future = persist_puzzle_level_asset_image(
let scene_persist_started_at = Instant::now();
let level_scene = persist_puzzle_level_asset_image(
state,
owner_user_id,
session_id,
@@ -347,8 +384,18 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
"level_scene",
"scene",
scene_image,
)
.await?;
tracing::info!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
slot = "level_scene",
elapsed_ms = scene_persist_started_at.elapsed().as_millis() as u64,
"拼图关卡场景图持久化完成"
);
let spritesheet_future = generate_and_persist_puzzle_level_asset(
let ui_spritesheet = generate_and_persist_puzzle_level_asset(
state,
&http_client,
&settings,
@@ -362,8 +409,9 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
"puzzle_ui_spritesheet_image",
"ui_spritesheet",
"spritesheet",
);
let background_future = generate_and_persist_puzzle_level_asset(
)
.await?;
let level_background = generate_and_persist_puzzle_level_asset(
state,
&http_client,
&settings,
@@ -377,14 +425,21 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
"puzzle_level_background_image",
"level_background",
"background",
);
let (level_scene, ui_spritesheet, level_background) =
tokio::join!(scene_persist_future, spritesheet_future, background_future);
)
.await?;
tracing::info!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
elapsed_ms = bundle_started_at.elapsed().as_millis() as u64,
"拼图关卡资产包生成完成"
);
Ok(GeneratedPuzzleLevelAssetBundle {
level_scene: level_scene?,
ui_spritesheet: ui_spritesheet?,
level_background: level_background?,
level_scene,
ui_spritesheet,
level_background,
})
}
@@ -403,7 +458,20 @@ async fn generate_and_persist_puzzle_level_asset(
slot: &str,
file_stem: &str,
) -> Result<GeneratedPuzzleLevelAssetResponse, AppError> {
let generated = create_puzzle_vector_engine_image_generation(
let started_at = Instant::now();
tracing::info!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
slot,
asset_kind,
size,
prompt_chars = prompt.chars().count(),
reference_image_bytes = reference_image.bytes_len,
"拼图关卡资产生成请求开始"
);
let generated = match create_puzzle_vector_engine_image_generation(
http_client,
settings,
PuzzleImageModel::GptImage2,
@@ -414,7 +482,36 @@ async fn generate_and_persist_puzzle_level_asset(
Some(reference_image),
)
.await
.map_err(map_puzzle_generation_endpoint_error)?;
.map_err(map_puzzle_generation_endpoint_error)
{
Ok(generated) => {
tracing::info!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
slot,
asset_kind,
elapsed_ms = started_at.elapsed().as_millis() as u64,
"拼图关卡资产生成请求完成"
);
generated
}
Err(error) => {
tracing::warn!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
slot,
asset_kind,
elapsed_ms = started_at.elapsed().as_millis() as u64,
error = %error,
"拼图关卡资产生成请求失败"
);
return Err(error);
}
};
let image = generated.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": VECTOR_ENGINE_PROVIDER,
@@ -427,7 +524,8 @@ async fn generate_and_persist_puzzle_level_asset(
image
};
persist_puzzle_level_asset_image(
let persist_started_at = Instant::now();
let persisted = persist_puzzle_level_asset_image(
state,
owner_user_id,
session_id,
@@ -439,7 +537,19 @@ async fn generate_and_persist_puzzle_level_asset(
file_stem,
image,
)
.await
.await?;
tracing::info!(
provider = VECTOR_ENGINE_PROVIDER,
image_model = PuzzleImageModel::GptImage2.request_model_name(),
session_id,
level_name,
slot,
asset_kind,
elapsed_ms = persist_started_at.elapsed().as_millis() as u64,
"拼图关卡资产持久化完成"
);
Ok(persisted)
}
pub(crate) fn make_puzzle_ui_spritesheet_image_transparent(