合并 master 并保留拼消消体验路径

合入 master 的平台 UI 与后端更新。

解决拼消消创作页、结果页和运行态冲突。

保留拼消消发布前检查、背景图命名、拖拽边界与补牌动画修复。
This commit is contained in:
2026-06-12 16:38:23 +08:00
396 changed files with 43653 additions and 13291 deletions

View File

@@ -20,8 +20,8 @@ use crate::match3d::tables::{
match_3_d_work_profile, match3d_agent_message, match3d_agent_session, match3d_runtime_run,
};
use crate::puzzle::{
puzzle_agent_message, puzzle_agent_session, puzzle_event, puzzle_leaderboard_entry,
puzzle_runtime_run, puzzle_work_profile,
puzzle_agent_message, puzzle_agent_session, puzzle_background_compile_task, puzzle_event,
puzzle_leaderboard_entry, puzzle_runtime_run, puzzle_work_profile,
};
use crate::puzzle_clear::tables::{
puzzle_clear_agent_session, puzzle_clear_event, puzzle_clear_runtime_run,
@@ -229,6 +229,7 @@ macro_rules! migration_tables {
asset_entity_binding,
asset_event,
puzzle_agent_session,
puzzle_background_compile_task,
puzzle_agent_message,
puzzle_work_profile,
puzzle_event,

View File

@@ -10,14 +10,16 @@ use module_puzzle::{
PUZZLE_NEXT_LEVEL_MODE_SIMILAR_WORKS, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
PuzzleAgentMessageRole, PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput,
PuzzleAgentSessionGetInput, PuzzleAgentSessionProcedureResult, PuzzleAgentSessionSnapshot,
PuzzleAgentStage, PuzzleAnchorPack, PuzzleDraftCompileFailureInput, PuzzleDraftCompileInput,
PuzzleFormDraftSaveInput, PuzzleGeneratedImageCandidate, PuzzleGeneratedImagesSaveInput,
PuzzleLeaderboardEntry, PuzzleLeaderboardSubmitInput, PuzzlePublicationStatus,
PuzzlePublishInput, PuzzleRecommendedNextWork, PuzzleResultDraft, PuzzleRunDragInput,
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult,
PuzzleRunPropInput, PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput,
PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput, PuzzleUiBackgroundSaveInput,
PuzzleWorkDeleteInput, PuzzleWorkGetInput, PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput,
PuzzleAgentStage, PuzzleAnchorPack, PuzzleBackgroundCompileTaskClaimInput,
PuzzleBackgroundCompileTaskProcedureResult, PuzzleBackgroundCompileTaskReleaseInput,
PuzzleDraftCompileFailureInput, PuzzleDraftCompileInput, PuzzleFormDraftSaveInput,
PuzzleGeneratedImageCandidate, PuzzleGeneratedImagesSaveInput, PuzzleLeaderboardEntry,
PuzzleLeaderboardSubmitInput, PuzzlePublicationStatus, PuzzlePublishInput,
PuzzleRecommendedNextWork, PuzzleResultDraft, PuzzleRunDragInput, PuzzleRunGetInput,
PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult, PuzzleRunPropInput,
PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus,
PuzzleSelectCoverImageInput, PuzzleUiBackgroundSaveInput, PuzzleWorkDeleteInput,
PuzzleWorkGetInput, PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput,
PuzzleWorkPointIncentiveClaimInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
PuzzleWorkRemixInput, PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
apply_publish_overrides_to_draft, apply_selected_candidate, build_form_draft_from_seed,
@@ -38,6 +40,7 @@ use spacetimedb::{
use crate::auth::user_account;
const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0;
const PUZZLE_BACKGROUND_COMPILE_TASK_LEASE_MICROS: i64 = 30 * 60 * 1_000_000;
const WORK_VISIBLE_DEFAULT: bool = true;
/// 拼图 Agent session 真相表。
@@ -62,6 +65,22 @@ pub struct PuzzleAgentSessionRow {
updated_at: Timestamp,
}
/// 拼图首图后台编译活动任务表。
/// 中文注释:该表只保存跨 api-server 实例互斥 claim不表达最终生成结果。
#[spacetimedb::table(
accessor = puzzle_background_compile_task,
index(accessor = by_puzzle_background_compile_task_session_id, btree(columns = [session_id]))
)]
pub struct PuzzleBackgroundCompileTaskRow {
#[primary_key]
task_id: String,
claim_id: String,
session_id: String,
owner_user_id: String,
created_at: Timestamp,
updated_at: Timestamp,
}
/// 拼图 Agent 消息真相表。
#[spacetimedb::table(
accessor = puzzle_agent_message,
@@ -388,6 +407,44 @@ pub fn mark_puzzle_draft_generation_failed(
}
}
#[spacetimedb::procedure]
pub fn claim_puzzle_background_compile_task(
ctx: &mut ProcedureContext,
input: PuzzleBackgroundCompileTaskClaimInput,
) -> PuzzleBackgroundCompileTaskProcedureResult {
match ctx.try_with_tx(|tx| claim_puzzle_background_compile_task_tx(tx, input.clone())) {
Ok(claimed) => PuzzleBackgroundCompileTaskProcedureResult {
ok: true,
claimed,
error_message: None,
},
Err(message) => PuzzleBackgroundCompileTaskProcedureResult {
ok: false,
claimed: false,
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn release_puzzle_background_compile_task(
ctx: &mut ProcedureContext,
input: PuzzleBackgroundCompileTaskReleaseInput,
) -> PuzzleBackgroundCompileTaskProcedureResult {
match ctx.try_with_tx(|tx| release_puzzle_background_compile_task_tx(tx, input.clone())) {
Ok(released) => PuzzleBackgroundCompileTaskProcedureResult {
ok: true,
claimed: released,
error_message: None,
},
Err(message) => PuzzleBackgroundCompileTaskProcedureResult {
ok: false,
claimed: false,
error_message: Some(message),
},
}
}
/// 保存拼图入口表单草稿。
/// 中文注释:该 procedure 只更新 session 与创作中心草稿卡,不触发图片生成或发布校验。
#[spacetimedb::procedure]
@@ -1024,6 +1081,84 @@ fn compile_puzzle_agent_draft_tx(
)
}
fn claim_puzzle_background_compile_task_tx(
ctx: &TxContext,
input: PuzzleBackgroundCompileTaskClaimInput,
) -> Result<bool, String> {
let task_id = normalize_required_puzzle_task_field(&input.task_id, "拼图后台任务 ID")?;
let claim_id = normalize_required_puzzle_task_field(&input.claim_id, "拼图后台任务 claim ID")?;
let session_id = normalize_required_puzzle_task_field(&input.session_id, "拼图 session ID")?;
let owner_user_id = normalize_required_puzzle_task_field(&input.owner_user_id, "拼图用户 ID")?;
let claimed_at = Timestamp::from_micros_since_unix_epoch(input.claimed_at_micros);
get_owned_session_row(ctx, &session_id, &owner_user_id)?;
if let Some(existing) = ctx
.db
.puzzle_background_compile_task()
.task_id()
.find(&task_id)
{
if !is_stale_puzzle_background_compile_task(&existing, input.claimed_at_micros) {
return Ok(false);
}
ctx.db
.puzzle_background_compile_task()
.task_id()
.delete(&task_id);
}
ctx.db
.puzzle_background_compile_task()
.insert(PuzzleBackgroundCompileTaskRow {
task_id,
claim_id,
session_id,
owner_user_id,
created_at: claimed_at,
updated_at: claimed_at,
});
Ok(true)
}
fn release_puzzle_background_compile_task_tx(
ctx: &TxContext,
input: PuzzleBackgroundCompileTaskReleaseInput,
) -> Result<bool, String> {
let task_id = normalize_required_puzzle_task_field(&input.task_id, "拼图后台任务 ID")?;
let claim_id = normalize_required_puzzle_task_field(&input.claim_id, "拼图后台任务 claim ID")?;
let session_id = normalize_required_puzzle_task_field(&input.session_id, "拼图 session ID")?;
let owner_user_id = normalize_required_puzzle_task_field(&input.owner_user_id, "拼图用户 ID")?;
let Some(row) = ctx
.db
.puzzle_background_compile_task()
.task_id()
.find(&task_id)
else {
return Ok(false);
};
if row.session_id != session_id || row.owner_user_id != owner_user_id {
return Err("无权释放该拼图后台任务".to_string());
}
if row.claim_id != claim_id {
return Ok(false);
}
ctx.db
.puzzle_background_compile_task()
.task_id()
.delete(&task_id);
Ok(true)
}
fn is_stale_puzzle_background_compile_task(
row: &PuzzleBackgroundCompileTaskRow,
now_micros: i64,
) -> bool {
now_micros.saturating_sub(row.updated_at.to_micros_since_unix_epoch())
>= PUZZLE_BACKGROUND_COMPILE_TASK_LEASE_MICROS
}
fn mark_puzzle_draft_generation_failed_tx(
ctx: &TxContext,
input: PuzzleDraftCompileFailureInput,
@@ -2950,6 +3085,14 @@ fn get_owned_session_row(
Ok(row)
}
fn normalize_required_puzzle_task_field(value: &str, field_name: &str) -> Result<String, String> {
let normalized = value.trim();
if normalized.is_empty() {
return Err(format!("{field_name} 不能为空"));
}
Ok(normalized.to_string())
}
fn get_owned_run_row(
ctx: &TxContext,
run_id: &str,