This commit is contained in:
2026-05-05 14:40:41 +08:00
parent e847fcea6f
commit 07e777fef8
76 changed files with 4246 additions and 444 deletions

View File

@@ -946,10 +946,18 @@ fn save_puzzle_generated_images_tx(
) -> Result<PuzzleAgentSessionSnapshot, String> {
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
let mut draft = deserialize_draft_required(&row.draft_json)?;
let previous_primary_level_name = draft.level_name.clone();
let previous_work_title = draft.work_title.clone();
if let Some(levels) = deserialize_optional_levels_input(input.levels_json.as_deref())? {
// 中文注释:结果页新增关卡可能还没等到自动保存,生成图时以本次 action 携带的关卡快照作为写回目标。
draft.levels = levels;
module_puzzle::sync_primary_level_fields(&mut draft);
// 中文注释:入口直创会在 api-server 生成首关名后随 levels_json 写入;作品名仍是旧首关名或空值时才跟随首关名,避免覆盖用户手动命名。
sync_generated_primary_level_name_as_default_work_title(
&mut draft,
&previous_work_title,
&previous_primary_level_name,
);
}
let candidates: Vec<PuzzleGeneratedImageCandidate> = json_from_str(&input.candidates_json)
.map_err(|error| format!("拼图候选图 JSON 非法: {error}"))?;
@@ -1014,6 +1022,18 @@ fn save_puzzle_generated_images_tx(
)
}
fn sync_generated_primary_level_name_as_default_work_title(
draft: &mut PuzzleResultDraft,
previous_work_title: &str,
previous_primary_level_name: &str,
) {
if previous_work_title.trim().is_empty()
|| previous_work_title.trim() == previous_primary_level_name.trim()
{
draft.work_title = draft.level_name.clone();
}
}
fn select_puzzle_cover_image_tx(
ctx: &TxContext,
input: PuzzleSelectCoverImageInput,
@@ -1189,7 +1209,7 @@ fn update_puzzle_work_tx(
return Err("无权修改该拼图作品".to_string());
}
let theme_tags = normalize_theme_tags(input.theme_tags);
if theme_tags.is_empty() || theme_tags.len() > PUZZLE_MAX_TAG_COUNT {
if theme_tags.len() > PUZZLE_MAX_TAG_COUNT {
return Err("拼图标签数量不合法".to_string());
}
let levels = deserialize_optional_levels_input(input.levels_json.as_deref())?
@@ -1251,6 +1271,7 @@ fn update_puzzle_work_tx(
published_at: row.published_at,
};
replace_puzzle_work_profile(ctx, &row, next_row);
sync_puzzle_source_session_draft_from_work(ctx, &row, &preview_draft, input.updated_at_micros)?;
get_puzzle_work_detail_tx(
ctx,
PuzzleWorkGetInput {
@@ -1259,6 +1280,53 @@ fn update_puzzle_work_tx(
)
}
fn sync_puzzle_source_session_draft_from_work(
ctx: &TxContext,
work_row: &PuzzleWorkProfileRow,
draft: &PuzzleResultDraft,
updated_at_micros: i64,
) -> Result<(), String> {
let Some(session_id) = work_row.source_session_id.as_ref() else {
return Ok(());
};
let Some(session_row) = ctx.db.puzzle_agent_session().session_id().find(session_id) else {
return Ok(());
};
if session_row.owner_user_id != work_row.owner_user_id {
return Ok(());
}
let normalized_draft = normalize_puzzle_draft(draft.clone());
let updated_at = Timestamp::from_micros_since_unix_epoch(updated_at_micros);
let next_stage = if session_row.stage == PuzzleAgentStage::Published {
PuzzleAgentStage::Published
} else if build_result_preview(&normalized_draft, Some(&work_row.author_display_name))
.publish_ready
{
PuzzleAgentStage::ReadyToPublish
} else {
PuzzleAgentStage::ImageRefining
};
replace_puzzle_agent_session(
ctx,
&session_row,
PuzzleAgentSessionRow {
session_id: session_row.session_id.clone(),
owner_user_id: session_row.owner_user_id.clone(),
seed_text: session_row.seed_text.clone(),
current_turn: session_row.current_turn,
progress_percent: session_row.progress_percent.max(94),
stage: next_stage,
anchor_pack_json: session_row.anchor_pack_json.clone(),
draft_json: Some(serialize_json(&normalized_draft)),
last_assistant_reply: session_row.last_assistant_reply.clone(),
published_profile_id: session_row.published_profile_id.clone(),
created_at: session_row.created_at,
updated_at,
},
);
Ok(())
}
fn delete_puzzle_work_tx(
ctx: &TxContext,
input: PuzzleWorkDeleteInput,
@@ -3298,6 +3366,53 @@ mod tests {
assert!(draft.candidates[0].selected);
}
#[test]
fn generated_first_level_name_defaults_work_title_when_previous_title_is_fallback() {
let anchor_pack = infer_anchor_pack("画面描述:一只猫在雨夜灯牌下回头。", None);
let mut draft = compile_result_draft_from_seed(
&anchor_pack,
&[],
Some("画面描述:一只猫在雨夜灯牌下回头。"),
);
let previous_level_name = draft.level_name.clone();
let previous_work_title = draft.work_title.clone();
draft.levels[0].level_name = "雨夜猫街".to_string();
module_puzzle::sync_primary_level_fields(&mut draft);
sync_generated_primary_level_name_as_default_work_title(
&mut draft,
&previous_work_title,
&previous_level_name,
);
assert_eq!(draft.level_name, "雨夜猫街");
assert_eq!(draft.work_title, "雨夜猫街");
}
#[test]
fn generated_first_level_name_keeps_manual_work_title() {
let anchor_pack = infer_anchor_pack("画面描述:一只猫在雨夜灯牌下回头。", None);
let mut draft = compile_result_draft_from_seed(
&anchor_pack,
&[],
Some("画面描述:一只猫在雨夜灯牌下回头。"),
);
let previous_level_name = draft.level_name.clone();
let previous_work_title = "我的猫街合集".to_string();
draft.work_title = previous_work_title.clone();
draft.levels[0].level_name = "雨夜猫街".to_string();
module_puzzle::sync_primary_level_fields(&mut draft);
sync_generated_primary_level_name_as_default_work_title(
&mut draft,
&previous_work_title,
&previous_level_name,
);
assert_eq!(draft.level_name, "雨夜猫街");
assert_eq!(draft.work_title, "我的猫街合集");
}
#[test]
fn puzzle_recommendation_score_prefers_same_author_weight() {
let left = PuzzleWorkProfile {