feat: add inline external generation mode

This commit is contained in:
2026-06-07 00:56:53 +08:00
parent 853d1db618
commit 4bb6d0bd1e
20 changed files with 393 additions and 114 deletions

View File

@@ -22,6 +22,7 @@ pub struct AppConfig {
pub listen_backlog: i32,
pub worker_threads: Option<usize>,
pub process_role: ProcessRole,
pub external_generation_mode: ExternalGenerationMode,
pub external_generation_worker_id: String,
pub external_generation_worker_concurrency: usize,
pub external_generation_worker_poll_interval: Duration,
@@ -171,6 +172,25 @@ pub enum ProcessRole {
All,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExternalGenerationMode {
Inline,
Queue,
}
impl ExternalGenerationMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Inline => "inline",
Self::Queue => "queue",
}
}
pub fn is_inline(self) -> bool {
matches!(self, Self::Inline)
}
}
impl ProcessRole {
pub fn as_str(self) -> &'static str {
match self {
@@ -197,6 +217,7 @@ impl Default for AppConfig {
listen_backlog: 1024,
worker_threads: None,
process_role: ProcessRole::Api,
external_generation_mode: ExternalGenerationMode::Queue,
external_generation_worker_id: default_external_generation_worker_id(),
external_generation_worker_concurrency: 2,
external_generation_worker_poll_interval: Duration::from_millis(2_000),
@@ -385,6 +406,11 @@ impl AppConfig {
if let Some(process_role) = read_first_process_role_env(&["GENARRATIVE_PROCESS_ROLE"]) {
config.process_role = process_role;
}
if let Some(external_generation_mode) =
read_first_external_generation_mode_env(&["GENARRATIVE_EXTERNAL_GENERATION_MODE"])
{
config.external_generation_mode = external_generation_mode;
}
if let Some(worker_id) =
read_first_non_empty_env(&["GENARRATIVE_EXTERNAL_GENERATION_WORKER_ID"])
{
@@ -1046,6 +1072,14 @@ fn read_first_process_role_env(keys: &[&str]) -> Option<ProcessRole> {
})
}
fn read_first_external_generation_mode_env(keys: &[&str]) -> Option<ExternalGenerationMode> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_external_generation_mode(&value))
})
}
fn read_first_positive_u32_env(keys: &[&str]) -> Option<u32> {
keys.iter().find_map(|key| {
env::var(key)
@@ -1111,6 +1145,16 @@ fn parse_process_role(value: &str) -> Option<ProcessRole> {
}
}
fn parse_external_generation_mode(value: &str) -> Option<ExternalGenerationMode> {
match trim_quoted_env_value(value).to_ascii_lowercase().as_str() {
"inline" | "sync" | "synchronous" => Some(ExternalGenerationMode::Inline),
"queue" | "queued" | "worker" | "async" | "asynchronous" => {
Some(ExternalGenerationMode::Queue)
}
_ => None,
}
}
fn trim_quoted_env_value(raw: &str) -> &str {
let raw = raw.trim();
raw.strip_prefix('"')
@@ -1243,8 +1287,8 @@ fn parse_positive_u16(raw: &str) -> Option<u16> {
#[cfg(test)]
mod tests {
use super::{
AppConfig, DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, LlmProvider, ProcessRole,
parse_bool, parse_process_role,
AppConfig, DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, ExternalGenerationMode,
LlmProvider, ProcessRole, parse_bool, parse_external_generation_mode, parse_process_role,
};
use std::sync::{Mutex, OnceLock};
@@ -1312,6 +1356,51 @@ mod tests {
assert!(ProcessRole::All.runs_external_generation_worker());
}
#[test]
fn external_generation_mode_parses_inline_and_queue_aliases() {
assert_eq!(
parse_external_generation_mode("inline"),
Some(ExternalGenerationMode::Inline)
);
assert_eq!(
parse_external_generation_mode("'sync'"),
Some(ExternalGenerationMode::Inline)
);
assert_eq!(
parse_external_generation_mode("\"queue\""),
Some(ExternalGenerationMode::Queue)
);
assert_eq!(
parse_external_generation_mode("worker"),
Some(ExternalGenerationMode::Queue)
);
assert_eq!(parse_external_generation_mode("unknown"), None);
assert!(ExternalGenerationMode::Inline.is_inline());
assert!(!ExternalGenerationMode::Queue.is_inline());
}
#[test]
fn from_env_reads_external_generation_mode() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock");
unsafe {
std::env::set_var("GENARRATIVE_EXTERNAL_GENERATION_MODE", "inline");
}
let config = AppConfig::from_env();
assert_eq!(
config.external_generation_mode,
ExternalGenerationMode::Inline
);
unsafe {
std::env::remove_var("GENARRATIVE_EXTERNAL_GENERATION_MODE");
}
}
#[test]
fn from_env_reads_sms_enabled_when_shell_value_keeps_quotes() {
let _guard = ENV_LOCK

View File

@@ -77,17 +77,13 @@ pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
{
return Some("puzzle");
}
if normalized.starts_with("/api/runtime/puzzle/gallery/")
&& normalized.ends_with("/remix")
{
if normalized.starts_with("/api/runtime/puzzle/gallery/") && normalized.ends_with("/remix") {
return Some("puzzle");
}
if normalized == "/api/runtime/big-fish/agent/sessions" {
return Some("big-fish");
}
if normalized.starts_with("/api/runtime/big-fish/gallery/")
&& normalized.ends_with("/remix")
{
if normalized.starts_with("/api/runtime/big-fish/gallery/") && normalized.ends_with("/remix") {
return Some("big-fish");
}
if normalized == "/api/runtime/custom-world/agent/sessions"

View File

@@ -496,11 +496,11 @@ fn build_external_generation_write_lease_guard(
worker_id: &str,
job: &ExternalGenerationJobRecord,
) -> Result<ExternalGenerationWriteLeaseGuard, String> {
Ok(ExternalGenerationWriteLeaseGuard {
job_id: job.job_id.clone(),
worker_id: worker_id.to_string(),
lease_token: require_job_lease_token(job)?,
})
Ok(ExternalGenerationWriteLeaseGuard::from_claimed_job(
job.job_id.clone(),
worker_id.to_string(),
require_job_lease_token(job)?,
))
}
fn duration_micros_i64(duration: Duration) -> i64 {
@@ -527,9 +527,9 @@ mod tests {
let guard = build_external_generation_write_lease_guard("worker-a", &job)
.expect("guard should build");
assert_eq!(guard.job_id, "extgen-1");
assert_eq!(guard.worker_id, "worker-a");
assert_eq!(guard.lease_token, "lease-1");
assert_eq!(guard.job_id.as_deref(), Some("extgen-1"));
assert_eq!(guard.worker_id.as_deref(), Some("worker-a"));
assert_eq!(guard.lease_token.as_deref(), Some("lease-1"));
}
#[test]

View File

@@ -136,9 +136,27 @@ const PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE: &str = "1024x1536";
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ExternalGenerationWriteLeaseGuard {
pub(crate) job_id: String,
pub(crate) worker_id: String,
pub(crate) lease_token: String,
pub(crate) job_id: Option<String>,
pub(crate) worker_id: Option<String>,
pub(crate) lease_token: Option<String>,
}
impl ExternalGenerationWriteLeaseGuard {
pub(crate) fn inline() -> Self {
Self {
job_id: None,
worker_id: None,
lease_token: None,
}
}
pub(crate) fn from_claimed_job(job_id: String, worker_id: String, lease_token: String) -> Self {
Self {
job_id: Some(job_id),
worker_id: Some(worker_id),
lease_token: Some(lease_token),
}
}
}
#[derive(Debug)]
@@ -166,6 +184,10 @@ impl PuzzleExternalGenerationWorkerError {
self.error.body_text()
}
pub(crate) fn into_app_error(self) -> AppError {
self.error
}
pub(crate) fn should_fail_queue_job(&self) -> bool {
self.should_fail_queue_job
}

View File

@@ -648,6 +648,53 @@ pub async fn execute_puzzle_agent_action(
image_model: payload.image_model.clone(),
requested_at_micros: now,
};
if state
.root_state()
.config
.external_generation_mode
.is_inline()
{
tracing::info!(
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
session_id = %compile_session_id,
owner_user_id = %owner_user_id,
external_generation_mode = state.root_state().config.external_generation_mode.as_str(),
"拼图首关草稿生成使用 inline 模式同步执行"
);
let session = execute_puzzle_compile_draft_worker_job(
&state,
&request_context,
worker_payload,
ExternalGenerationWriteLeaseGuard::inline(),
)
.await
.map_err(|error| {
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
error.into_app_error(),
)
})?;
return Ok(json_success_body(
Some(&request_context),
PuzzleAgentActionResponse {
operation: PuzzleAgentOperationResponse {
operation_id: build_prefixed_uuid_id("extgen-inline-"),
operation_type: "compile_puzzle_draft".to_string(),
status: "completed".to_string(),
phase_label: "首关拼图草稿".to_string(),
phase_detail: if ai_redraw {
"首关草稿生成已完成。".to_string()
} else {
"首关草稿编译已完成。".to_string()
},
progress: 100,
error: None,
},
session: map_puzzle_agent_session_response(session),
},
));
}
let request_payload_json = serde_json::to_string(&worker_payload).map_err(|error| {
puzzle_error_response(
&request_context,
@@ -811,6 +858,49 @@ pub async fn execute_puzzle_agent_action(
levels_json,
requested_at_micros: now,
};
if state
.root_state()
.config
.external_generation_mode
.is_inline()
{
tracing::info!(
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
session_id = %session_id,
owner_user_id = %owner_user_id,
external_generation_mode = state.root_state().config.external_generation_mode.as_str(),
"拼图关卡图片生成使用 inline 模式同步执行"
);
let session = execute_puzzle_generate_images_worker_job(
&state,
&request_context,
worker_payload,
ExternalGenerationWriteLeaseGuard::inline(),
)
.await
.map_err(|error| {
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
error.into_app_error(),
)
})?;
return Ok(json_success_body(
Some(&request_context),
PuzzleAgentActionResponse {
operation: PuzzleAgentOperationResponse {
operation_id: build_prefixed_uuid_id("extgen-inline-"),
operation_type: "generate_puzzle_images".to_string(),
status: "completed".to_string(),
phase_label: "拼图图片生成".to_string(),
phase_detail: "关卡图片生成已完成。".to_string(),
progress: 100,
error: None,
},
session: map_puzzle_agent_session_response(session),
},
));
}
let request_payload_json = serde_json::to_string(&worker_payload).map_err(|error| {
puzzle_error_response(
&request_context,
@@ -908,6 +998,49 @@ pub async fn execute_puzzle_agent_action(
levels_json,
requested_at_micros: now,
};
if state
.root_state()
.config
.external_generation_mode
.is_inline()
{
tracing::info!(
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
session_id = %session_id,
owner_user_id = %owner_user_id,
external_generation_mode = state.root_state().config.external_generation_mode.as_str(),
"拼图 UI 背景图生成使用 inline 模式同步执行"
);
let session = execute_puzzle_generate_ui_background_worker_job(
&state,
&request_context,
worker_payload,
ExternalGenerationWriteLeaseGuard::inline(),
)
.await
.map_err(|error| {
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
error.into_app_error(),
)
})?;
return Ok(json_success_body(
Some(&request_context),
PuzzleAgentActionResponse {
operation: PuzzleAgentOperationResponse {
operation_id: build_prefixed_uuid_id("extgen-inline-"),
operation_type: "generate_puzzle_ui_background".to_string(),
status: "completed".to_string(),
phase_label: "UI 背景图生成".to_string(),
phase_detail: "拼图 UI 背景图生成已完成。".to_string(),
progress: 100,
error: None,
},
session: map_puzzle_agent_session_response(session),
},
));
}
let request_payload_json = serde_json::to_string(&worker_payload).map_err(|error| {
puzzle_error_response(
&request_context,

View File

@@ -78,9 +78,9 @@ pub struct PuzzleDraftCompileFailureInput {
pub owner_user_id: String,
pub error_message: String,
pub failed_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -92,9 +92,9 @@ pub struct PuzzleLevelGenerationFailureInput {
pub levels_json: Option<String>,
pub error_message: String,
pub failed_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -106,9 +106,9 @@ pub struct PuzzleGeneratedImagesSaveInput {
pub levels_json: Option<String>,
pub candidates_json: String,
pub saved_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -122,9 +122,9 @@ pub struct PuzzleUiBackgroundSaveInput {
pub image_src: String,
pub image_object_key: Option<String>,
pub saved_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]

View File

@@ -642,9 +642,9 @@ pub struct PuzzleDraftCompileFailureRecordInput {
pub owner_user_id: String,
pub error_message: String,
pub failed_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -655,9 +655,9 @@ pub struct PuzzleLevelGenerationFailureRecordInput {
pub levels_json: Option<String>,
pub error_message: String,
pub failed_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -668,9 +668,9 @@ pub struct PuzzleGeneratedImagesSaveRecordInput {
pub levels_json: Option<String>,
pub candidates_json: String,
pub saved_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -683,9 +683,9 @@ pub struct PuzzleUiBackgroundSaveRecordInput {
pub image_src: String,
pub image_object_key: Option<String>,
pub saved_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]

View File

@@ -11,9 +11,9 @@ pub struct PuzzleDraftCompileFailureInput {
pub owner_user_id: String,
pub error_message: String,
pub failed_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
impl __sdk::InModule for PuzzleDraftCompileFailureInput {

View File

@@ -13,9 +13,9 @@ pub struct PuzzleGeneratedImagesSaveInput {
pub levels_json: Option<String>,
pub candidates_json: String,
pub saved_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
impl __sdk::InModule for PuzzleGeneratedImagesSaveInput {

View File

@@ -13,9 +13,9 @@ pub struct PuzzleLevelGenerationFailureInput {
pub levels_json: Option<String>,
pub error_message: String,
pub failed_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
impl __sdk::InModule for PuzzleLevelGenerationFailureInput {

View File

@@ -15,9 +15,9 @@ pub struct PuzzleUiBackgroundSaveInput {
pub image_src: String,
pub image_object_key: Option<String>,
pub saved_at_micros: i64,
pub external_generation_job_id: String,
pub external_generation_worker_id: String,
pub external_generation_lease_token: String,
pub external_generation_job_id: Option<String>,
pub external_generation_worker_id: Option<String>,
pub external_generation_lease_token: Option<String>,
}
impl __sdk::InModule for PuzzleUiBackgroundSaveInput {

View File

@@ -147,8 +147,13 @@ impl SpacetimeClient {
owner_user_id: String,
compiled_at_micros: i64,
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
self.compile_puzzle_agent_draft_inner(session_id, owner_user_id, compiled_at_micros, None)
.await
self.compile_puzzle_agent_draft_inner(
session_id,
owner_user_id,
compiled_at_micros,
(None, None, None),
)
.await
}
pub async fn compile_puzzle_agent_draft_with_external_generation_guard(
@@ -156,19 +161,19 @@ impl SpacetimeClient {
session_id: String,
owner_user_id: String,
compiled_at_micros: i64,
external_generation_job_id: String,
external_generation_worker_id: String,
external_generation_lease_token: String,
external_generation_job_id: Option<String>,
external_generation_worker_id: Option<String>,
external_generation_lease_token: Option<String>,
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
self.compile_puzzle_agent_draft_inner(
session_id,
owner_user_id,
compiled_at_micros,
Some((
(
external_generation_job_id,
external_generation_worker_id,
external_generation_lease_token,
)),
),
)
.await
}
@@ -178,17 +183,13 @@ impl SpacetimeClient {
session_id: String,
owner_user_id: String,
compiled_at_micros: i64,
external_generation_guard: Option<(String, String, String)>,
external_generation_guard: (Option<String>, Option<String>, Option<String>),
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
let (
external_generation_job_id,
external_generation_worker_id,
external_generation_lease_token,
) = external_generation_guard
.map(|(job_id, worker_id, lease_token)| {
(Some(job_id), Some(worker_id), Some(lease_token))
})
.unwrap_or((None, None, None));
) = external_generation_guard;
let procedure_input = PuzzleDraftCompileInput {
session_id,
owner_user_id,

View File

@@ -1003,26 +1003,17 @@ fn compile_puzzle_agent_draft_tx(
ctx: &TxContext,
input: PuzzleDraftCompileInput,
) -> Result<PuzzleAgentSessionSnapshot, String> {
match (
validate_optional_puzzle_external_generation_write_guard(
ctx,
input.external_generation_job_id.as_deref(),
input.external_generation_worker_id.as_deref(),
input.external_generation_lease_token.as_deref(),
) {
(Some(job_id), Some(worker_id), Some(lease_token)) => {
validate_puzzle_external_generation_write_guard(
ctx,
job_id,
worker_id,
lease_token,
&[PUZZLE_COMPILE_DRAFT_JOB_KIND],
&input.session_id,
&input.owner_user_id,
None,
)?;
}
(None, None, None) => {}
_ => return Err("拼图草稿编译外部生成 guard 不完整".to_string()),
}
&[PUZZLE_COMPILE_DRAFT_JOB_KIND],
&input.session_id,
&input.owner_user_id,
None,
"拼图草稿编译外部生成 guard 不完整",
)?;
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
if row.seed_text.trim().is_empty() {
return Err("请先填写拼图作品信息".to_string());
@@ -1073,15 +1064,16 @@ fn mark_puzzle_draft_generation_failed_tx(
ctx: &TxContext,
input: PuzzleDraftCompileFailureInput,
) -> Result<PuzzleAgentSessionSnapshot, String> {
validate_puzzle_external_generation_write_guard(
validate_optional_puzzle_external_generation_write_guard(
ctx,
&input.external_generation_job_id,
&input.external_generation_worker_id,
&input.external_generation_lease_token,
input.external_generation_job_id.as_deref(),
input.external_generation_worker_id.as_deref(),
input.external_generation_lease_token.as_deref(),
&[PUZZLE_COMPILE_DRAFT_JOB_KIND],
&input.session_id,
&input.owner_user_id,
None,
"拼图草稿失败态外部生成 guard 不完整",
)?;
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
let updated_at = Timestamp::from_micros_since_unix_epoch(input.failed_at_micros);
@@ -1138,11 +1130,11 @@ fn mark_puzzle_level_generation_failed_tx(
ctx: &TxContext,
input: PuzzleLevelGenerationFailureInput,
) -> Result<PuzzleAgentSessionSnapshot, String> {
validate_puzzle_external_generation_write_guard(
validate_optional_puzzle_external_generation_write_guard(
ctx,
&input.external_generation_job_id,
&input.external_generation_worker_id,
&input.external_generation_lease_token,
input.external_generation_job_id.as_deref(),
input.external_generation_worker_id.as_deref(),
input.external_generation_lease_token.as_deref(),
&[
PUZZLE_GENERATE_IMAGES_JOB_KIND,
PUZZLE_GENERATE_UI_BACKGROUND_JOB_KIND,
@@ -1150,6 +1142,7 @@ fn mark_puzzle_level_generation_failed_tx(
&input.session_id,
&input.owner_user_id,
input.level_id.as_deref(),
"拼图关卡失败态外部生成 guard 不完整",
)?;
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
let updated_at = Timestamp::from_micros_since_unix_epoch(input.failed_at_micros);
@@ -1259,6 +1252,35 @@ fn validate_puzzle_external_generation_write_guard(
)
}
fn validate_optional_puzzle_external_generation_write_guard(
ctx: &TxContext,
job_id: Option<&str>,
worker_id: Option<&str>,
lease_token: Option<&str>,
expected_job_kinds: &[&str],
session_id: &str,
owner_user_id: &str,
level_id: Option<&str>,
incomplete_message: &str,
) -> Result<(), String> {
match (job_id, worker_id, lease_token) {
(Some(job_id), Some(worker_id), Some(lease_token)) => {
validate_puzzle_external_generation_write_guard(
ctx,
job_id,
worker_id,
lease_token,
expected_job_kinds,
session_id,
owner_user_id,
level_id,
)
}
(None, None, None) => Ok(()),
_ => Err(incomplete_message.to_string()),
}
}
fn save_puzzle_form_draft_tx(
ctx: &TxContext,
input: PuzzleFormDraftSaveInput,
@@ -1316,11 +1338,11 @@ fn save_puzzle_generated_images_tx(
ctx: &TxContext,
input: PuzzleGeneratedImagesSaveInput,
) -> Result<PuzzleAgentSessionSnapshot, String> {
validate_puzzle_external_generation_write_guard(
validate_optional_puzzle_external_generation_write_guard(
ctx,
&input.external_generation_job_id,
&input.external_generation_worker_id,
&input.external_generation_lease_token,
input.external_generation_job_id.as_deref(),
input.external_generation_worker_id.as_deref(),
input.external_generation_lease_token.as_deref(),
&[
PUZZLE_COMPILE_DRAFT_JOB_KIND,
PUZZLE_GENERATE_IMAGES_JOB_KIND,
@@ -1328,6 +1350,7 @@ fn save_puzzle_generated_images_tx(
&input.session_id,
&input.owner_user_id,
input.level_id.as_deref(),
"拼图图片保存外部生成 guard 不完整",
)?;
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
let mut draft = deserialize_draft_required(&row.draft_json)?;
@@ -1413,15 +1436,16 @@ fn save_puzzle_ui_background_tx(
ctx: &TxContext,
input: PuzzleUiBackgroundSaveInput,
) -> Result<PuzzleAgentSessionSnapshot, String> {
validate_puzzle_external_generation_write_guard(
validate_optional_puzzle_external_generation_write_guard(
ctx,
&input.external_generation_job_id,
&input.external_generation_worker_id,
&input.external_generation_lease_token,
input.external_generation_job_id.as_deref(),
input.external_generation_worker_id.as_deref(),
input.external_generation_lease_token.as_deref(),
&[PUZZLE_GENERATE_UI_BACKGROUND_JOB_KIND],
&input.session_id,
&input.owner_user_id,
input.level_id.as_deref(),
"拼图 UI 背景保存外部生成 guard 不完整",
)?;
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
let mut draft = deserialize_draft_required(&row.draft_json)?;