feat: unify phase one creation flow

This commit is contained in:
2026-05-30 05:05:02 +08:00
parent 3a87b2d966
commit 26975644b5
33 changed files with 2037 additions and 539 deletions

View File

@@ -163,7 +163,14 @@ pub(super) async fn compile_match3d_draft_for_session(
.clone()
.unwrap_or_else(|| fallback_work_metadata.tags.clone());
let billing_asset_id = format!("{}:{}:{}", session_id, profile_id, current_utc_micros());
execute_billable_match3d_draft_generation(
let compile_session_id = session_id.clone();
let compile_owner_user_id = owner_user_id.clone();
let compile_profile_id = profile_id.clone();
let compile_initial_game_name = initial_game_name.clone();
let compile_requested_summary = requested_summary.clone();
let compile_initial_tags = initial_tags.clone();
let compile_requested_cover_image_src = requested_cover_image_src.clone();
let result = execute_billable_match3d_draft_generation(
state,
request_context,
owner_user_id.as_str(),
@@ -307,7 +314,108 @@ pub(super) async fn compile_match3d_draft_for_session(
Ok((next_session, generated_item_assets))
},
)
.await;
if let Err(response) = result.as_ref()
&& response.status().is_server_error()
{
let failure_message = match3d_response_failure_message(response);
persist_failed_match3d_draft_generation(
state,
request_context,
authenticated,
compile_session_id,
compile_owner_user_id,
compile_profile_id,
compile_initial_game_name,
compile_requested_summary,
compile_initial_tags,
compile_requested_cover_image_src,
failure_message,
)
.await;
}
result
}
#[allow(clippy::too_many_arguments)]
async fn persist_failed_match3d_draft_generation(
state: &AppState,
request_context: &RequestContext,
authenticated: &AuthenticatedAccessToken,
session_id: String,
owner_user_id: String,
profile_id: String,
game_name: String,
summary: Option<String>,
tags: Vec<String>,
cover_image_src: Option<String>,
failure_message: String,
) {
let failure_assets_json = serialize_match3d_failed_generation_assets(failure_message.as_str());
if let Err(persist_error) = upsert_match3d_draft_snapshot(
state,
request_context,
authenticated,
session_id,
owner_user_id,
profile_id,
Some(game_name),
summary.or_else(|| Some(String::new())),
Some(serde_json::to_string(&tags).unwrap_or_default()),
cover_image_src,
None,
failure_assets_json,
)
.await
{
tracing::error!(
provider = MATCH3D_AGENT_PROVIDER,
status = ?persist_error.status(),
"抓大鹅草稿生成失败后的状态回写失败"
);
}
}
fn serialize_match3d_failed_generation_assets(message: &str) -> Option<String> {
let background_asset = Match3DGeneratedBackgroundAsset {
prompt: String::new(),
status: "failed".to_string(),
error: Some(message.trim().to_string()),
..Default::default()
};
let assets = vec![Match3DGeneratedItemAssetJson {
item_id: "match3d-generation-failure".to_string(),
item_name: "生成失败".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_LARGE.to_string()),
image_src: None,
image_object_key: None,
image_views: Vec::new(),
model_src: None,
model_object_key: None,
model_file_name: None,
task_uuid: None,
subscription_key: None,
sound_prompt: None,
background_music_title: None,
background_music_style: None,
background_music_prompt: None,
background_music: None,
click_sound: None,
background_asset: Some(background_asset),
status: "failed".to_string(),
error: Some(message.trim().to_string()),
}];
serde_json::to_string(&assets).ok()
}
fn match3d_response_failure_message(response: &Response) -> String {
response
.extensions()
.get::<String>()
.cloned()
.unwrap_or_else(|| format!("抓大鹅草稿生成失败HTTP {}", response.status()))
}
/// 中文注释:抓大鹅草稿生成是一次完整外部生成动作,按 session/profile 幂等预扣 10 泥点。

View File

@@ -453,6 +453,32 @@ fn match3d_background_asset_has_image(asset: &Match3DGeneratedBackgroundAsset) -
|| match3d_text_present(asset.container_image_object_key.as_ref())
}
fn match3d_asset_status_is_failure(status: &str) -> bool {
let normalized = status.trim().to_ascii_lowercase().replace(['-', ' '], "_");
matches!(
normalized.as_str(),
"failed" | "failure" | "error" | "partial_failed"
)
}
fn match3d_error_present(value: Option<&String>) -> bool {
value.is_some_and(|value| !value.trim().is_empty())
}
fn match3d_item_asset_has_failure(asset: &Match3DGeneratedItemAssetJson) -> bool {
match3d_asset_status_is_failure(asset.status.as_str())
|| match3d_error_present(asset.error.as_ref())
|| asset.background_asset.as_ref().is_some_and(|background| {
match3d_asset_status_is_failure(background.status.as_str())
|| match3d_error_present(background.error.as_ref())
})
}
fn match3d_background_asset_has_failure(asset: &Match3DGeneratedBackgroundAsset) -> bool {
match3d_asset_status_is_failure(asset.status.as_str())
|| match3d_error_present(asset.error.as_ref())
}
fn resolve_match3d_work_generation_status(
item: &Match3DWorkProfileRecord,
assets: &[Match3DGeneratedItemAssetJson],
@@ -462,6 +488,21 @@ fn resolve_match3d_work_generation_status(
return Some("ready".to_string());
}
let has_failure = assets.iter().any(match3d_item_asset_has_failure)
|| background_asset.is_some_and(match3d_background_asset_has_failure);
if has_failure {
let has_partial_result = assets.iter().any(match3d_item_asset_has_image)
|| background_asset.is_some_and(match3d_background_asset_has_image);
return Some(
if has_partial_result {
"partial_failed"
} else {
"failed"
}
.to_string(),
);
}
if assets.is_empty()
|| !assets.iter().any(match3d_item_asset_has_image)
|| !background_asset.is_some_and(match3d_background_asset_has_image)

View File

@@ -1842,3 +1842,45 @@ fn match3d_work_summary_marks_complete_generated_assets_ready() {
assert_eq!(response.generation_status.as_deref(), Some("ready"));
}
#[test]
fn match3d_work_summary_marks_failed_generated_assets_failed() {
let assets = vec![Match3DGeneratedItemAsset {
background_asset: Some(Match3DGeneratedBackgroundAsset {
prompt: "水果厨房背景".to_string(),
status: "failed".to_string(),
error: Some("VectorEngine 请求失败".to_string()),
..Default::default()
}),
status: "failed".to_string(),
error: Some("VectorEngine 请求失败".to_string()),
..test_match3d_generated_item_asset(1, "草莓")
}];
let response = map_match3d_work_summary_response(Match3DWorkProfileRecord {
work_id: "match3d-profile-1".to_string(),
profile_id: "match3d-profile-1".to_string(),
owner_user_id: "user-1".to_string(),
source_session_id: Some("match3d-session-1".to_string()),
author_display_name: "玩家".to_string(),
game_name: "水果抓大鹅".to_string(),
theme_text: "水果".to_string(),
summary: "水果主题".to_string(),
tags: vec!["水果".to_string()],
cover_image_src: None,
cover_asset_id: None,
reference_image_src: None,
clear_count: 3,
difficulty: 3,
publication_status: "draft".to_string(),
play_count: 0,
updated_at: "2026-05-10T00:00:00.000Z".to_string(),
published_at: None,
publish_ready: false,
generated_item_assets_json: serialize_match3d_generated_item_assets(&assets),
});
assert_eq!(
response.generation_status.as_deref(),
Some("partial_failed")
);
}