This commit is contained in:
2026-04-29 11:51:04 +08:00
parent e191619ab3
commit 412279ae11
89 changed files with 3966 additions and 491 deletions

View File

@@ -11,7 +11,7 @@ use module_puzzle::{
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunProcedureResult, PuzzleRunSnapshot,
PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput,
PuzzleWorkDeleteInput, PuzzleWorkGetInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
PuzzleWorkRemixInput, 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,
@@ -77,6 +77,8 @@ pub struct PuzzleWorkProfileRow {
cover_asset_id: Option<String>,
publication_status: PuzzlePublicationStatus,
play_count: u32,
remix_count: u32,
like_count: u32,
anchor_pack_json: String,
publish_ready: bool,
created_at: Timestamp,
@@ -387,6 +389,25 @@ pub fn get_puzzle_gallery_detail(
}
}
#[spacetimedb::procedure]
pub fn remix_puzzle_work(
ctx: &mut ProcedureContext,
input: PuzzleWorkRemixInput,
) -> PuzzleAgentSessionProcedureResult {
match ctx.try_with_tx(|tx| remix_puzzle_work_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 start_puzzle_run(
ctx: &mut ProcedureContext,
@@ -931,6 +952,8 @@ fn update_puzzle_work_tx(
cover_asset_id: input.cover_asset_id,
publication_status: row.publication_status,
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
anchor_pack_json: row.anchor_pack_json.clone(),
publish_ready: row.publish_ready,
created_at: row.created_at,
@@ -1033,6 +1056,140 @@ fn get_puzzle_gallery_detail_tx(
build_puzzle_work_profile_from_row(&row)
}
fn remix_puzzle_work_tx(
ctx: &TxContext,
input: PuzzleWorkRemixInput,
) -> Result<PuzzleAgentSessionSnapshot, String> {
let source_profile_id = input.source_profile_id.trim();
let target_owner_user_id = input.target_owner_user_id.trim();
let target_session_id = input.target_session_id.trim();
let target_profile_id = input.target_profile_id.trim();
let target_work_id = input.target_work_id.trim();
if source_profile_id.is_empty()
|| target_owner_user_id.is_empty()
|| target_session_id.is_empty()
|| target_profile_id.is_empty()
|| target_work_id.is_empty()
{
return Err("拼图 remix 参数不能为空".to_string());
}
if input.author_display_name.trim().is_empty() {
return Err("拼图 remix 作者名不能为空".to_string());
}
ensure_session_missing(ctx, target_session_id)?;
ensure_message_missing(ctx, input.welcome_message_id.trim())?;
if ctx
.db
.puzzle_work_profile()
.profile_id()
.find(&target_profile_id.to_string())
.is_some()
{
return Err("拼图 remix 目标作品已存在".to_string());
}
let source = ctx
.db
.puzzle_work_profile()
.profile_id()
.find(&source_profile_id.to_string())
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
.ok_or_else(|| "拼图已发布源作品不存在".to_string())?;
let source_profile = build_puzzle_work_profile_from_row(&source)?;
let remixed_at = Timestamp::from_micros_since_unix_epoch(input.remixed_at_micros);
replace_puzzle_work_profile(
ctx,
&source,
PuzzleWorkProfileRow {
profile_id: source.profile_id.clone(),
work_id: source.work_id.clone(),
owner_user_id: source.owner_user_id.clone(),
source_session_id: source.source_session_id.clone(),
author_display_name: source.author_display_name.clone(),
level_name: source.level_name.clone(),
summary: source.summary.clone(),
theme_tags_json: source.theme_tags_json.clone(),
cover_image_src: source.cover_image_src.clone(),
cover_asset_id: source.cover_asset_id.clone(),
publication_status: source.publication_status,
play_count: source.play_count,
remix_count: source.remix_count.saturating_add(1),
like_count: source.like_count,
anchor_pack_json: source.anchor_pack_json.clone(),
publish_ready: source.publish_ready,
created_at: source.created_at,
updated_at: remixed_at,
published_at: source.published_at,
},
);
let draft = PuzzleResultDraft {
level_name: source_profile.level_name.clone(),
summary: source_profile.summary.clone(),
theme_tags: source_profile.theme_tags.clone(),
forbidden_directives: Vec::new(),
creator_intent: None,
anchor_pack: source_profile.anchor_pack.clone(),
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: source_profile.cover_image_src.clone(),
cover_asset_id: source_profile.cover_asset_id.clone(),
generation_status: "ready".to_string(),
};
ctx.db.puzzle_agent_session().insert(PuzzleAgentSessionRow {
session_id: target_session_id.to_string(),
owner_user_id: target_owner_user_id.to_string(),
seed_text: source_profile.summary.clone(),
current_turn: 1,
progress_percent: 88,
stage: PuzzleAgentStage::DraftReady,
anchor_pack_json: serialize_json(&source_profile.anchor_pack),
draft_json: Some(serialize_json(&draft)),
last_assistant_reply: Some("已从公开作品 Remix 出新的拼图草稿。".to_string()),
published_profile_id: None,
created_at: remixed_at,
updated_at: remixed_at,
});
ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow {
message_id: input.welcome_message_id,
session_id: target_session_id.to_string(),
role: PuzzleAgentMessageRole::Assistant,
kind: PuzzleAgentMessageKind::Summary,
text: "已复制公开作品为你的草稿。".to_string(),
created_at: remixed_at,
});
ctx.db.puzzle_work_profile().insert(PuzzleWorkProfileRow {
profile_id: target_profile_id.to_string(),
work_id: target_work_id.to_string(),
owner_user_id: target_owner_user_id.to_string(),
source_session_id: Some(target_session_id.to_string()),
author_display_name: input.author_display_name.trim().to_string(),
level_name: source_profile.level_name,
summary: source_profile.summary,
theme_tags_json: serialize_json(&source_profile.theme_tags),
cover_image_src: source_profile.cover_image_src,
cover_asset_id: source_profile.cover_asset_id,
publication_status: PuzzlePublicationStatus::Draft,
play_count: 0,
remix_count: 0,
like_count: 0,
anchor_pack_json: serialize_json(&source_profile.anchor_pack),
publish_ready: true,
created_at: remixed_at,
updated_at: remixed_at,
published_at: None,
});
get_puzzle_agent_session_tx(
ctx,
PuzzleAgentSessionGetInput {
session_id: target_session_id.to_string(),
owner_user_id: target_owner_user_id.to_string(),
},
)
}
fn start_puzzle_run_tx(
ctx: &TxContext,
input: PuzzleRunStartInput,
@@ -1308,6 +1465,8 @@ fn build_puzzle_work_profile_from_row(
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
publish_ready: row.publish_ready,
anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?,
})
@@ -1507,6 +1666,8 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
// 二次编辑发布同一个 profile 时,作品内容可以覆盖,但历史游玩数属于
// 广场消费数据,不能因为重新发布被清零。
play_count: existing.play_count.max(profile.play_count),
remix_count: existing.remix_count.max(profile.remix_count),
like_count: existing.like_count.max(profile.like_count),
anchor_pack_json: serialize_json(&profile.anchor_pack),
publish_ready: profile.publish_ready,
created_at: existing.created_at,
@@ -1532,6 +1693,8 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
cover_asset_id: profile.cover_asset_id,
publication_status: profile.publication_status,
play_count: profile.play_count,
remix_count: profile.remix_count,
like_count: profile.like_count,
anchor_pack_json: serialize_json(&profile.anchor_pack),
publish_ready: profile.publish_ready,
created_at: Timestamp::from_micros_since_unix_epoch(profile.updated_at_micros),
@@ -1620,6 +1783,8 @@ fn increment_puzzle_profile_play_count(
cover_asset_id: row.cover_asset_id.clone(),
publication_status: row.publication_status,
play_count: row.play_count.saturating_add(1),
remix_count: row.remix_count,
like_count: row.like_count,
anchor_pack_json: row.anchor_pack_json.clone(),
publish_ready: row.publish_ready,
created_at: row.created_at,