Add generationStatus and match3d/runtime fixes

Introduce persistent generationStatus to work summaries (puzzle & match3d) and propagate generation recovery rules across docs and frontend/backends so "generating" is restored from server-side work summary rather than ephemeral front-end notices. Update API server image/asset handling (improve match3d material sheet green/alpha decontamination and promote generatedItemAssets background fields) and add runtime improvements: alpha-based hotspot hit-testing, tray insertion/three-match animation behavior, and session re-read on client-side VectorEngine timeouts/lock-screen interruptions. Many docs, tests and related frontend modules updated/added to reflect these contract and behavior changes.
This commit is contained in:
2026-05-16 22:59:02 +08:00
parent bb60ca91ef
commit a45e358e83
42 changed files with 3872 additions and 443 deletions

View File

@@ -870,7 +870,10 @@ fn compile_puzzle_agent_draft_tx(
}
let anchor_pack = infer_anchor_pack(&row.seed_text, Some(&row.seed_text));
let messages = list_session_messages(ctx, &row.session_id);
let draft = compile_result_draft_from_seed(&anchor_pack, &messages, Some(&row.seed_text));
let draft = mark_puzzle_draft_generation_status(
compile_result_draft_from_seed(&anchor_pack, &messages, Some(&row.seed_text)),
"generating",
);
// 创作中心的拼图草稿卡只是 Agent session 的列表投影,
// 每次编译结果页时同步 upsert保证后续能按 source_session_id 恢复聊天。
upsert_puzzle_draft_work_profile(
@@ -2472,10 +2475,52 @@ fn profile_for_single_level(
level: &module_puzzle::PuzzleDraftLevel,
) -> PuzzleWorkProfile {
let mut next_profile = profile.clone();
let ui_background_carrier = profile.levels.iter().find(|candidate| {
candidate
.ui_background_image_src
.as_deref()
.map(str::trim)
.map(|value| !value.is_empty())
.unwrap_or(false)
|| candidate
.ui_background_image_object_key
.as_deref()
.map(str::trim)
.map(|value| !value.is_empty())
.unwrap_or(false)
});
let mut single_level = level.clone();
if single_level
.ui_background_image_src
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
&& single_level
.ui_background_image_object_key
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
&& let Some(carrier) = ui_background_carrier
{
single_level.ui_background_image_src = carrier
.ui_background_image_src
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string);
single_level.ui_background_image_object_key = carrier
.ui_background_image_object_key
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.trim_start_matches('/').to_string());
}
next_profile.level_name = level.level_name.clone();
next_profile.cover_image_src = level.cover_image_src.clone();
next_profile.cover_asset_id = level.cover_asset_id.clone();
next_profile.levels = vec![level.clone()];
next_profile.levels = vec![single_level];
next_profile
}
@@ -2496,6 +2541,17 @@ fn micros_to_millis(value: i64) -> u64 {
(value as u64).saturating_div(1_000)
}
fn mark_puzzle_draft_generation_status(
mut draft: PuzzleResultDraft,
generation_status: &str,
) -> PuzzleResultDraft {
draft.generation_status = generation_status.to_string();
for level in &mut draft.levels {
level.generation_status = generation_status.to_string();
}
draft
}
fn upsert_puzzle_draft_work_profile(
ctx: &TxContext,
session_id: &str,
@@ -3466,6 +3522,37 @@ mod tests {
assert!(preview.publish_ready);
}
#[test]
fn puzzle_draft_generation_status_updates_all_levels() {
let anchor_pack = infer_anchor_pack("蒸汽城市雨夜猫咪", Some("蒸汽城市雨夜猫咪"));
let mut draft = compile_result_draft(&anchor_pack, &[]);
draft.levels.push(module_puzzle::PuzzleDraftLevel {
level_id: "puzzle-level-2".to_string(),
level_name: "第二关".to_string(),
picture_description: "第二关画面".to_string(),
picture_reference: None,
ui_background_prompt: None,
ui_background_image_src: None,
ui_background_image_object_key: None,
background_music: None,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
});
let draft = mark_puzzle_draft_generation_status(draft, "generating");
assert_eq!(draft.generation_status, "generating");
assert!(
draft
.levels
.iter()
.all(|level| level.generation_status == "generating")
);
}
#[test]
fn puzzle_generated_images_replace_existing_candidate() {
let anchor_pack = infer_anchor_pack("蒸汽城市雨夜猫咪", Some("蒸汽城市雨夜猫咪"));