fix: sync rust api-server runtime and bindings

This commit is contained in:
2026-04-23 20:32:06 +08:00
parent 9d25a47b23
commit 27e84c46a0
82 changed files with 9534 additions and 2222 deletions

View File

@@ -1,8 +1,8 @@
use module_puzzle::{
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageKind, PuzzleAgentMessageRole,
PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput, PuzzleAgentSessionGetInput,
PuzzleAgentSessionProcedureResult, PuzzleAgentSessionSnapshot, PuzzleAgentStage,
PuzzleAnchorPack, PuzzleDraftCompileInput, PuzzleGeneratedImageCandidate,
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
PuzzleAgentMessageRole, PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput,
PuzzleAgentSessionGetInput, PuzzleAgentSessionProcedureResult, PuzzleAgentSessionSnapshot,
PuzzleAgentStage, PuzzleAnchorPack, PuzzleDraftCompileInput, PuzzleGeneratedImageCandidate,
PuzzleGeneratedImagesSaveInput, PuzzlePublicationStatus, PuzzlePublishInput, PuzzleResultDraft,
PuzzleRunDragInput, PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunProcedureResult,
PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus,
@@ -158,6 +158,25 @@ pub fn submit_puzzle_agent_message(
}
}
#[spacetimedb::procedure]
pub fn finalize_puzzle_agent_message_turn(
ctx: &mut ProcedureContext,
input: PuzzleAgentMessageFinalizeInput,
) -> PuzzleAgentSessionProcedureResult {
match ctx.try_with_tx(|tx| finalize_puzzle_agent_message_turn_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn compile_puzzle_agent_draft(
ctx: &mut ProcedureContext,
@@ -472,11 +491,9 @@ fn submit_puzzle_agent_message_tx(
ctx: &TxContext,
input: module_puzzle::PuzzleAgentMessageSubmitInput,
) -> Result<PuzzleAgentSessionSnapshot, String> {
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
ensure_message_missing(ctx, &input.user_message_id)?;
let submitted_at = Timestamp::from_micros_since_unix_epoch(input.submitted_at_micros);
let next_anchor_pack = infer_anchor_pack(&row.seed_text, Some(&input.user_message_text));
let assistant_message_text = build_puzzle_assistant_reply(&next_anchor_pack);
ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow {
message_id: input.user_message_id.clone(),
@@ -486,19 +503,75 @@ 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
);
get_puzzle_agent_session_tx(
ctx,
PuzzleAgentSessionGetInput {
session_id: input.session_id,
owner_user_id: input.owner_user_id,
},
)
}
fn finalize_puzzle_agent_message_turn_tx(
ctx: &TxContext,
input: PuzzleAgentMessageFinalizeInput,
) -> Result<PuzzleAgentSessionSnapshot, String> {
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
if let Some(error_message) = input
.error_message
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
replace_puzzle_agent_session(
ctx,
&row,
PuzzleAgentSessionRow {
session_id: row.session_id.clone(),
owner_user_id: row.owner_user_id.clone(),
seed_text: row.seed_text.clone(),
current_turn: row.current_turn,
progress_percent: row.progress_percent,
stage: row.stage,
anchor_pack_json: row.anchor_pack_json.clone(),
draft_json: row.draft_json.clone(),
last_assistant_reply: row.last_assistant_reply.clone(),
published_profile_id: row.published_profile_id.clone(),
created_at: row.created_at,
updated_at,
},
);
return Err(error_message.to_string());
}
let assistant_message_id = input
.assistant_message_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "拼图 assistant_message_id 不能为空".to_string())?
.to_string();
let assistant_reply_text = input
.assistant_reply_text
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "拼图 assistant_reply_text 不能为空".to_string())?
.to_string();
ensure_message_missing(ctx, &assistant_message_id)?;
let next_anchor_pack = deserialize_anchor_pack(&input.anchor_pack_json)?;
ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow {
message_id: assistant_message_id,
session_id: input.session_id.clone(),
role: PuzzleAgentMessageRole::Assistant,
kind: PuzzleAgentMessageKind::Summary,
text: assistant_message_text.clone(),
created_at: submitted_at,
kind: PuzzleAgentMessageKind::Chat,
text: assistant_reply_text.clone(),
created_at: updated_at,
});
replace_puzzle_agent_session(
ctx,
&row,
@@ -507,14 +580,14 @@ fn submit_puzzle_agent_message_tx(
owner_user_id: row.owner_user_id.clone(),
seed_text: row.seed_text.clone(),
current_turn: row.current_turn.saturating_add(1),
progress_percent: (row.progress_percent + 18).min(82),
stage: PuzzleAgentStage::CollectingAnchors,
progress_percent: input.progress_percent.min(100),
stage: input.stage,
anchor_pack_json: serialize_json(&next_anchor_pack),
draft_json: row.draft_json.clone(),
last_assistant_reply: Some(assistant_message_text),
last_assistant_reply: Some(assistant_reply_text),
published_profile_id: row.published_profile_id.clone(),
created_at: row.created_at,
updated_at: submitted_at,
updated_at,
},
);
@@ -535,6 +608,15 @@ fn compile_puzzle_agent_draft_tx(
let anchor_pack = deserialize_anchor_pack(&row.anchor_pack_json)?;
let messages = list_session_messages(ctx, &row.session_id);
let draft = compile_result_draft(&anchor_pack, &messages);
// 创作中心的拼图草稿卡只是 Agent session 的列表投影,
// 每次编译结果页时同步 upsert保证后续能按 source_session_id 恢复聊天。
upsert_puzzle_draft_work_profile(
ctx,
&row.session_id,
&row.owner_user_id,
&draft,
input.compiled_at_micros,
)?;
let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros);
replace_puzzle_agent_session(
ctx,
@@ -601,6 +683,14 @@ fn save_puzzle_generated_images_tx(
} else {
PuzzleAgentStage::ImageRefining
};
// 结果页草稿封面和候选图发生变化后,草稿卡需要同步刷新。
upsert_puzzle_draft_work_profile(
ctx,
&row.session_id,
&row.owner_user_id,
&draft,
input.saved_at_micros,
)?;
replace_puzzle_agent_session(
ctx,
&row,
@@ -642,6 +732,14 @@ fn select_puzzle_cover_image_tx(
} else {
PuzzleAgentStage::ImageRefining
};
// 选定正式封面后,创作中心草稿卡要立即反映最新正式图。
upsert_puzzle_draft_work_profile(
ctx,
&row.session_id,
&row.owner_user_id,
&draft,
input.selected_at_micros,
)?;
replace_puzzle_agent_session(
ctx,
&row,
@@ -682,9 +780,10 @@ fn publish_puzzle_work_tx(
input.theme_tags.clone(),
)
.map_err(|error| error.to_string())?;
let (work_id, profile_id) = build_puzzle_work_ids_from_session_id(&input.session_id);
let mut profile = create_work_profile(
input.work_id.clone(),
input.profile_id.clone(),
work_id,
profile_id,
input.owner_user_id.clone(),
Some(input.session_id.clone()),
input.author_display_name.clone(),
@@ -996,6 +1095,42 @@ fn build_puzzle_work_profile_from_row(
})
}
fn build_puzzle_work_ids_from_session_id(session_id: &str) -> (String, String) {
let stable_suffix = session_id
.strip_prefix("puzzle-session-")
.unwrap_or(session_id);
(
format!("puzzle-work-{stable_suffix}"),
format!("puzzle-profile-{stable_suffix}"),
)
}
fn upsert_puzzle_draft_work_profile(
ctx: &TxContext,
session_id: &str,
owner_user_id: &str,
draft: &PuzzleResultDraft,
updated_at_micros: i64,
) -> Result<(), String> {
let (work_id, profile_id) = build_puzzle_work_ids_from_session_id(session_id);
if let Some(existing) = ctx.db.puzzle_work_profile().profile_id().find(&profile_id) {
if existing.publication_status == PuzzlePublicationStatus::Published {
return Ok(());
}
}
let profile = create_work_profile(
work_id,
profile_id,
owner_user_id.to_string(),
Some(session_id.to_string()),
"创作者".to_string(),
draft,
updated_at_micros,
)
.map_err(|error| error.to_string())?;
upsert_puzzle_work_profile(ctx, profile)
}
fn list_session_messages(ctx: &TxContext, session_id: &str) -> Vec<PuzzleAgentMessageSnapshot> {
let mut items = ctx
.db
@@ -1045,15 +1180,6 @@ fn build_puzzle_suggested_actions(
}
}
fn build_puzzle_assistant_reply(anchor_pack: &PuzzleAnchorPack) -> String {
format!(
"我先帮你收束成一版拼图方向:题材是“{}”,主体聚焦“{}”,氛围偏“{}”。",
anchor_pack.theme_promise.value,
anchor_pack.visual_subject.value,
anchor_pack.visual_mood.value
)
}
fn append_system_message(
ctx: &TxContext,
session_id: &str,