feat: add inline external generation mode
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user