合并 origin/master

合入 master 的钱包退款 outbox、拼图后台编译互斥与公开链路更新

保留当前分支外部生成 worker 队列语义,并对齐拼图首图 claim 释放顺序
This commit is contained in:
2026-06-11 23:06:41 +08:00
70 changed files with 3167 additions and 538 deletions

View File

@@ -588,7 +588,7 @@ pub async fn execute_puzzle_agent_action(
let owner_user_id = authenticated.claims().user_id().to_string();
let now = current_utc_micros();
let action = payload.action.trim().to_string();
let billing_asset_id = format!("{session_id}:{now}");
let billing_asset_id = format!("{}:{}:{}", session_id, action, request_context.request_id());
tracing::info!(
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
session_id = %session_id,
@@ -658,6 +658,79 @@ pub async fn execute_puzzle_agent_action(
reference_image_src: primary_reference_image_src.map(ToOwned::to_owned),
image_model: payload.image_model.clone(),
requested_at_micros: now,
background_task_id: None,
background_claim_id: None,
};
let worker_payload = if ai_redraw {
let background_task_id =
build_puzzle_background_compile_task_id(&compile_session_id);
let background_claim_id = build_puzzle_background_compile_claim_id(
&background_task_id,
request_context.request_id(),
);
let claim_result = state
.spacetime_client()
.claim_puzzle_background_compile_task(
PuzzleBackgroundCompileTaskClaimRecordInput {
task_id: background_task_id.clone(),
claim_id: background_claim_id.clone(),
session_id: compile_session_id.clone(),
owner_user_id: owner_user_id.clone(),
claimed_at_micros: current_utc_micros(),
},
)
.await
.map_err(|error| {
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
map_puzzle_client_error(error),
)
})?;
if !claim_result {
tracing::info!(
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
session_id = %compile_session_id,
owner_user_id = %owner_user_id,
task_id = %background_task_id,
"拼图首图后台生成任务已存在,本次 action 直接返回生成中会话"
);
let session = state
.spacetime_client()
.get_puzzle_agent_session(compile_session_id.clone(), owner_user_id.clone())
.await
.map(mark_puzzle_initial_generation_started_snapshot)
.map_err(|error| {
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
map_puzzle_client_error(error),
)
})?;
return Ok(json_success_body(
Some(&request_context),
PuzzleAgentActionResponse {
operation: PuzzleAgentOperationResponse {
operation_id: background_task_id,
operation_type: "compile_puzzle_draft".to_string(),
status: "running".to_string(),
phase_label: "首关拼图草稿".to_string(),
phase_detail: "首关草稿生成已在后台处理中。".to_string(),
progress: session.progress_percent.max(10),
error: None,
},
session: map_puzzle_agent_session_response(session),
},
));
}
PuzzleCompileDraftWorkerPayload {
background_task_id: Some(background_task_id),
background_claim_id: Some(background_claim_id),
..worker_payload
}
} else {
worker_payload
};
if state
.root_state()
@@ -675,7 +748,7 @@ pub async fn execute_puzzle_agent_action(
let session = execute_puzzle_compile_draft_worker_job(
&state,
&request_context,
worker_payload,
worker_payload.clone(),
ExternalGenerationWriteLeaseGuard::inline(),
)
.await
@@ -707,6 +780,18 @@ pub async fn execute_puzzle_agent_action(
));
}
let request_payload_json = serde_json::to_string(&worker_payload).map_err(|error| {
if let (Some(task_id), Some(claim_id)) = (
worker_payload.background_task_id.as_deref(),
worker_payload.background_claim_id.as_deref(),
) {
spawn_release_claimed_puzzle_background_compile_task(
state.clone(),
task_id.to_string(),
claim_id.to_string(),
compile_session_id.clone(),
owner_user_id.clone(),
);
}
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
@@ -736,6 +821,18 @@ pub async fn execute_puzzle_agent_action(
})
.await
.map_err(|error| {
if let (Some(task_id), Some(claim_id)) = (
worker_payload.background_task_id.as_deref(),
worker_payload.background_claim_id.as_deref(),
) {
spawn_release_claimed_puzzle_background_compile_task(
state.clone(),
task_id.to_string(),
claim_id.to_string(),
compile_session_id.clone(),
owner_user_id.clone(),
);
}
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
@@ -2034,7 +2131,7 @@ pub async fn use_puzzle_runtime_prop(
}
};
let should_sync_freeze_boundary = matches!(prop_kind.as_str(), "freezeTime" | "freeze_time");
let billing_asset_id = format!("{}:{}:{}", run_id, prop_kind, current_utc_micros());
let billing_asset_id = format!("{}:{}:{}", run_id, prop_kind, request_context.request_id());
let reducer_owner_user_id = owner_user_id.clone();
let reducer_run_id = run_id.clone();
let fallback_run_id = run_id.clone();