This commit is contained in:
2026-04-22 22:01:07 +08:00
parent d8716d70b0
commit b317c2a8ea
37 changed files with 1821 additions and 515 deletions

View File

@@ -1,18 +1,16 @@
use module_puzzle::{
use module_puzzle::{
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageKind, PuzzleAgentMessageRole,
PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput, PuzzleAgentSessionGetInput,
PuzzleAgentSessionProcedureResult, PuzzleAgentSessionSnapshot, PuzzleAgentStage,
PuzzleAnchorPack, PuzzleDraftCompileInput, PuzzleGeneratedImageCandidate,
PuzzleGeneratedImagesSaveInput, PuzzlePublicationStatus, PuzzlePublishInput,
PuzzleResultDraft, PuzzleRunDragInput, PuzzleRunGetInput, PuzzleRunNextLevelInput,
PuzzleRunProcedureResult, PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput,
PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput, PuzzleWorkGetInput,
PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkUpsertInput,
PuzzleWorksListInput, PuzzleWorksProcedureResult, apply_publish_overrides_to_draft,
apply_selected_candidate,
build_result_preview, compile_result_draft, create_work_profile, infer_anchor_pack,
normalize_theme_tags, publish_work_profile, resolve_puzzle_grid_size,
select_next_profile, start_run, swap_pieces,
PuzzleGeneratedImagesSaveInput, PuzzlePublicationStatus, PuzzlePublishInput, PuzzleResultDraft,
PuzzleRunDragInput, PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunProcedureResult,
PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus,
PuzzleSelectCoverImageInput, PuzzleWorkGetInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
apply_publish_overrides_to_draft, apply_selected_candidate, build_result_preview,
compile_result_draft, create_work_profile, infer_anchor_pack, normalize_theme_tags,
publish_work_profile, resolve_puzzle_grid_size, select_next_profile, start_run, swap_pieces,
};
use serde_json::from_str as json_from_str;
use serde_json::to_string as json_to_string;
@@ -488,7 +486,10 @@ fn submit_puzzle_agent_message_tx(
text: input.user_message_text.clone(),
created_at: submitted_at,
});
let assistant_message_id = format!("{}assistant-{}", input.session_id, input.submitted_at_micros);
let assistant_message_id = format!(
"{}assistant-{}",
input.session_id, input.submitted_at_micros
);
ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow {
message_id: assistant_message_id,
session_id: input.session_id.clone(),
@@ -547,7 +548,9 @@ fn compile_puzzle_agent_draft_tx(
stage: PuzzleAgentStage::DraftReady,
anchor_pack_json: serialize_json(&anchor_pack),
draft_json: Some(serialize_json(&draft)),
last_assistant_reply: Some("拼图结果页草稿已经生成,可以开始出图并确认标签。".to_string()),
last_assistant_reply: Some(
"拼图结果页草稿已经生成,可以开始出图并确认标签。".to_string(),
),
published_profile_id: row.published_profile_id.clone(),
created_at: row.created_at,
updated_at: compiled_at,
@@ -574,14 +577,19 @@ 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 candidates: Vec<PuzzleGeneratedImageCandidate> =
json_from_str(&input.candidates_json).map_err(|error| format!("拼图候选图 JSON 非法: {error}"))?;
let candidates: Vec<PuzzleGeneratedImageCandidate> = json_from_str(&input.candidates_json)
.map_err(|error| format!("拼图候选图 JSON 非法: {error}"))?;
if candidates.is_empty() {
return Err("拼图候选图不能为空".to_string());
}
draft.candidates = candidates;
draft.generation_status = "ready".to_string();
if let Some(selected) = draft.candidates.iter().find(|entry| entry.selected).cloned() {
if let Some(selected) = draft
.candidates
.iter()
.find(|entry| entry.selected)
.cloned()
{
draft.selected_candidate_id = Some(selected.candidate_id);
draft.cover_image_src = Some(selected.image_src);
draft.cover_asset_id = Some(selected.asset_id);
@@ -626,7 +634,8 @@ fn select_puzzle_cover_image_tx(
) -> Result<PuzzleAgentSessionSnapshot, String> {
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
let draft = deserialize_draft_required(&row.draft_json)?;
let draft = apply_selected_candidate(draft, &input.candidate_id).map_err(|error| error.to_string())?;
let draft =
apply_selected_candidate(draft, &input.candidate_id).map_err(|error| error.to_string())?;
let selected_at = Timestamp::from_micros_since_unix_epoch(input.selected_at_micros);
let next_stage = if build_result_preview(&draft, Some("创作者")).publish_ready {
PuzzleAgentStage::ReadyToPublish
@@ -814,7 +823,13 @@ fn start_puzzle_run_tx(
ctx: &TxContext,
input: PuzzleRunStartInput,
) -> Result<PuzzleRunSnapshot, String> {
if ctx.db.puzzle_runtime_run().run_id().find(&input.run_id).is_some() {
if ctx
.db
.puzzle_runtime_run()
.run_id()
.find(&input.run_id)
.is_some()
{
return Err("拼图 run 已存在".to_string());
}
let entry_profile_row = ctx
@@ -827,7 +842,8 @@ fn start_puzzle_run_tx(
return Err("入口拼图作品未发布".to_string());
}
let entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
let mut run = start_run(input.run_id.clone(), &entry_profile, 0).map_err(|error| error.to_string())?;
let mut run =
start_run(input.run_id.clone(), &entry_profile, 0).map_err(|error| error.to_string())?;
run.recommended_next_profile_id = select_next_profile(
&entry_profile,
&run.played_profile_ids,
@@ -854,8 +870,8 @@ fn swap_puzzle_pieces_tx(
) -> Result<PuzzleRunSnapshot, String> {
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
let current_run = deserialize_run(&row.snapshot_json)?;
let mut next_run =
swap_pieces(&current_run, &input.first_piece_id, &input.second_piece_id).map_err(|error| error.to_string())?;
let mut next_run = swap_pieces(&current_run, &input.first_piece_id, &input.second_piece_id)
.map_err(|error| error.to_string())?;
refresh_next_profile_recommendation(ctx, &mut next_run)?;
replace_puzzle_runtime_run(ctx, &row, &next_run, input.swapped_at_micros);
Ok(next_run)
@@ -900,17 +916,18 @@ fn advance_puzzle_next_level_tx(
.ok_or_else(|| "当前拼图作品不存在".to_string())?,
)?;
let candidates = list_published_puzzle_profiles(ctx)?;
let next_profile = select_next_profile(&current_profile, &current_run.played_profile_ids, &candidates)
.ok_or_else(|| "没有可用的下一关候选".to_string())?
.clone();
let mut next_run =
module_puzzle::advance_next_level(&current_run, &next_profile).map_err(|error| error.to_string())?;
next_run.recommended_next_profile_id = select_next_profile(
&next_profile,
&next_run.played_profile_ids,
let next_profile = select_next_profile(
&current_profile,
&current_run.played_profile_ids,
&candidates,
)
.map(|value| value.profile_id.clone());
.ok_or_else(|| "没有可用的下一关候选".to_string())?
.clone();
let mut next_run = module_puzzle::advance_next_level(&current_run, &next_profile)
.map_err(|error| error.to_string())?;
next_run.recommended_next_profile_id =
select_next_profile(&next_profile, &next_run.played_profile_ids, &candidates)
.map(|value| value.profile_id.clone());
if let Some(next_profile_row) = ctx
.db
@@ -954,7 +971,9 @@ fn build_puzzle_agent_session_snapshot(
})
}
fn build_puzzle_work_profile_from_row(row: &PuzzleWorkProfileRow) -> Result<PuzzleWorkProfile, String> {
fn build_puzzle_work_profile_from_row(
row: &PuzzleWorkProfileRow,
) -> Result<PuzzleWorkProfile, String> {
Ok(PuzzleWorkProfile {
work_id: row.work_id.clone(),
profile_id: row.profile_id.clone(),
@@ -968,7 +987,9 @@ fn build_puzzle_work_profile_from_row(row: &PuzzleWorkProfileRow) -> Result<Puzz
cover_asset_id: row.cover_asset_id.clone(),
publication_status: row.publication_status,
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
play_count: row.play_count,
publish_ready: row.publish_ready,
anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?,
@@ -994,7 +1015,9 @@ fn list_session_messages(ctx: &TxContext, session_id: &str) -> Vec<PuzzleAgentMe
items
}
fn build_puzzle_suggested_actions(stage: PuzzleAgentStage) -> Vec<module_puzzle::PuzzleAgentSuggestedAction> {
fn build_puzzle_suggested_actions(
stage: PuzzleAgentStage,
) -> Vec<module_puzzle::PuzzleAgentSuggestedAction> {
match stage {
PuzzleAgentStage::CollectingAnchors => vec![module_puzzle::PuzzleAgentSuggestedAction {
id: "compile-draft".to_string(),
@@ -1051,14 +1074,26 @@ fn append_system_message(
}
fn ensure_session_missing(ctx: &TxContext, session_id: &str) -> Result<(), String> {
if ctx.db.puzzle_agent_session().session_id().find(&session_id.to_string()).is_some() {
if ctx
.db
.puzzle_agent_session()
.session_id()
.find(&session_id.to_string())
.is_some()
{
return Err("拼图 session 已存在".to_string());
}
Ok(())
}
fn ensure_message_missing(ctx: &TxContext, message_id: &str) -> Result<(), String> {
if ctx.db.puzzle_agent_message().message_id().find(&message_id.to_string()).is_some() {
if ctx
.db
.puzzle_agent_message()
.message_id()
.find(&message_id.to_string())
.is_some()
{
return Err("拼图消息已存在".to_string());
}
Ok(())
@@ -1122,10 +1157,7 @@ fn replace_puzzle_work_profile(
ctx.db.puzzle_work_profile().insert(next);
}
fn upsert_puzzle_work_profile(
ctx: &TxContext,
profile: PuzzleWorkProfile,
) -> Result<(), String> {
fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Result<(), String> {
if let Some(existing) = ctx
.db
.puzzle_work_profile()
@@ -1219,10 +1251,7 @@ fn replace_puzzle_runtime_run(
run: &PuzzleRunSnapshot,
updated_at_micros: i64,
) {
ctx.db
.puzzle_runtime_run()
.run_id()
.delete(&current.run_id);
ctx.db.puzzle_runtime_run().run_id().delete(&current.run_id);
ctx.db.puzzle_runtime_run().insert(PuzzleRuntimeRunRow {
run_id: run.run_id.clone(),
owner_user_id: current.owner_user_id.clone(),
@@ -1414,6 +1443,9 @@ mod tests {
author_display_name: "作者".to_string(),
summary: String::new(),
};
assert!(recommendation_score(&left, &right) > tag_similarity_score(&left.theme_tags, &right.theme_tags));
assert!(
recommendation_score(&left, &right)
> tag_similarity_score(&left.theme_tags, &right.theme_tags)
);
}
}