扩展外部生成Worker队列

新增外部生成队列概览和单任务状态契约

将跳一跳、拼消消、敲木鱼图片生成动作接入worker队列

前端生成等待页展示当前任务和队列数量

更新外部生成worker运维文档和团队决策记录
This commit is contained in:
2026-06-12 23:15:55 +08:00
parent 3bccfd1a83
commit 951caac32d
43 changed files with 1913 additions and 67 deletions

View File

@@ -104,6 +104,12 @@ pub struct ExternalGenerationJobFailInput {
pub failed_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct ExternalGenerationJobGetInput {
pub job_id: String,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct ExternalGenerationJobSnapshot {
pub job_id: String,
@@ -218,6 +224,17 @@ pub fn fail_external_generation_job_and_return(
}
}
#[spacetimedb::procedure]
pub fn get_external_generation_job_and_return(
ctx: &mut ProcedureContext,
input: ExternalGenerationJobGetInput,
) -> ExternalGenerationJobProcedureResult {
match ctx.try_with_tx(|tx| get_external_generation_job_tx(tx, input.clone())) {
Ok(job) => single_external_generation_job_result(job),
Err(message) => failed_external_generation_job_result(message),
}
}
#[spacetimedb::procedure]
pub fn get_external_generation_queue_stats_and_return(
ctx: &mut ProcedureContext,
@@ -405,6 +422,28 @@ fn complete_external_generation_job_tx(
Ok(map_external_generation_job_row(row))
}
fn get_external_generation_job_tx(
ctx: &ReducerContext,
input: ExternalGenerationJobGetInput,
) -> Result<ExternalGenerationJobSnapshot, String> {
validate_required("external_generation_job.job_id", &input.job_id)?;
validate_required(
"external_generation_job.owner_user_id",
&input.owner_user_id,
)?;
let row = ctx
.db
.external_generation_job()
.job_id()
.find(&input.job_id.trim().to_string())
.ok_or_else(|| "external_generation_job 不存在".to_string())?;
if row.owner_user_id.trim() != input.owner_user_id.trim() {
return Err("external_generation_job 不存在".to_string());
}
Ok(map_external_generation_job_row(row))
}
fn renew_external_generation_job_lease_tx(
ctx: &ReducerContext,
input: ExternalGenerationJobRenewLeaseInput,

View File

@@ -345,6 +345,9 @@ fn compile_puzzle_clear_draft_tx(
if input.generation_status.as_deref() == Some(PUZZLE_CLEAR_GENERATION_FAILED) {
return mark_puzzle_clear_generation_failed_tx(ctx, input, session);
}
if input.generation_status.as_deref() == Some(PUZZLE_CLEAR_GENERATION_GENERATING) {
return mark_puzzle_clear_generation_generating_tx(ctx, input, session);
}
let pattern_groups: Vec<PuzzleClearPatternGroupSnapshot> = input
.pattern_groups_json
.as_deref()
@@ -457,6 +460,71 @@ fn compile_puzzle_clear_draft_tx(
)
}
fn mark_puzzle_clear_generation_generating_tx(
ctx: &ReducerContext,
input: PuzzleClearDraftCompileInput,
session: PuzzleClearAgentSessionRow,
) -> Result<PuzzleClearAgentSessionSnapshot, String> {
let updated_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros);
let mut draft = if session.draft_json.trim().is_empty() {
None
} else {
parse_json::<PuzzleClearDraftSnapshot>(&session.draft_json).ok()
}
.unwrap_or_else(|| PuzzleClearDraftSnapshot {
template_id: PUZZLE_CLEAR_TEMPLATE_ID.to_string(),
template_name: PUZZLE_CLEAR_TEMPLATE_NAME.to_string(),
profile_id: Some(input.profile_id.clone()),
work_title: clean_string(&input.work_title, PUZZLE_CLEAR_TEMPLATE_NAME),
work_description: input.work_description.trim().to_string(),
theme_prompt: clean_string(&input.theme_prompt, PUZZLE_CLEAR_TEMPLATE_NAME),
generate_board_background: input.generate_board_background,
board_background_asset: None,
board_background_prompt: clean_string(&input.board_background_prompt, &input.theme_prompt),
card_back_image_src: Some(PUZZLE_CLEAR_CARD_BACK_IMAGE_SRC.to_string()),
atlas_asset: None,
pattern_groups: Vec::new(),
card_assets: Vec::new(),
generation_status: PUZZLE_CLEAR_GENERATION_GENERATING.to_string(),
});
draft.profile_id = Some(input.profile_id.clone());
draft.work_title = clean_string(&input.work_title, PUZZLE_CLEAR_TEMPLATE_NAME);
draft.work_description = input.work_description.trim().to_string();
draft.theme_prompt = clean_string(&input.theme_prompt, PUZZLE_CLEAR_TEMPLATE_NAME);
draft.generate_board_background = input.generate_board_background;
draft.board_background_prompt =
clean_string(&input.board_background_prompt, &input.theme_prompt);
if let Some(board_background_asset) = input
.board_background_asset_json
.as_deref()
.map(parse_json)
.transpose()?
{
draft.board_background_asset = Some(board_background_asset);
}
draft.generation_status = PUZZLE_CLEAR_GENERATION_GENERATING.to_string();
replace_session(
ctx,
&session,
PuzzleClearAgentSessionRow {
status: PUZZLE_CLEAR_GENERATION_GENERATING.to_string(),
draft_json: to_json_string(&draft),
published_profile_id: input.profile_id,
updated_at,
..clone_session(&session)
},
);
get_puzzle_clear_agent_session_tx(
ctx,
PuzzleClearAgentSessionGetInput {
session_id: input.session_id,
owner_user_id: input.owner_user_id,
},
)
}
fn mark_puzzle_clear_generation_failed_tx(
ctx: &ReducerContext,
input: PuzzleClearDraftCompileInput,