Merge remote-tracking branch 'origin/master' into editor-agent-refactored
# Conflicts: # docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md # docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md # docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md # server-rs/crates/api-server/src/editor_agent.rs # server-rs/crates/api-server/src/external_generation_worker.rs # server-rs/crates/spacetime-client/src/lib.rs # server-rs/crates/spacetime-client/src/mapper.rs # server-rs/crates/spacetime-client/src/mapper/external_generation.rs
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -17,8 +17,6 @@ const DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS: u64 = 600;
|
||||
const DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS: u64 = 900;
|
||||
const DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS: u64 = 1_800;
|
||||
pub(crate) const DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000;
|
||||
const DEFAULT_EDITOR_BACKGROUND_REMOVAL_BASE_URL: &str = "http://58.87.105.82";
|
||||
const DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS: u64 = 120_000;
|
||||
const DEFAULT_EDITOR_BGFILTER_BASE_URL: &str = "http://58.87.105.82/bgfilter";
|
||||
const DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS: u64 = 180_000;
|
||||
const DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD: u32 = 3;
|
||||
@@ -64,9 +62,6 @@ pub struct AppConfig {
|
||||
pub wallet_refund_outbox_flush_interval: Duration,
|
||||
pub wallet_refund_outbox_max_bytes: u64,
|
||||
pub editor_generation_pricing_override_path: PathBuf,
|
||||
pub editor_background_removal_base_url: String,
|
||||
pub editor_background_removal_token: Option<String>,
|
||||
pub editor_background_removal_request_timeout_ms: u64,
|
||||
pub editor_bgfilter_base_url: String,
|
||||
pub editor_bgfilter_token: Option<String>,
|
||||
pub editor_bgfilter_request_timeout_ms: u64,
|
||||
@@ -312,11 +307,6 @@ impl Default for AppConfig {
|
||||
wallet_refund_outbox_max_bytes: 64 * 1024 * 1024,
|
||||
editor_generation_pricing_override_path:
|
||||
crate::editor_generation_config::default_editor_generation_pricing_override_path(),
|
||||
editor_background_removal_base_url: DEFAULT_EDITOR_BACKGROUND_REMOVAL_BASE_URL
|
||||
.to_string(),
|
||||
editor_background_removal_token: None,
|
||||
editor_background_removal_request_timeout_ms:
|
||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS,
|
||||
editor_bgfilter_base_url: DEFAULT_EDITOR_BGFILTER_BASE_URL.to_string(),
|
||||
editor_bgfilter_token: None,
|
||||
editor_bgfilter_request_timeout_ms: DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS,
|
||||
@@ -515,18 +505,6 @@ impl AppConfig {
|
||||
{
|
||||
config.editor_generation_pricing_override_path = PathBuf::from(pricing_override_path);
|
||||
}
|
||||
if let Some(base_url) =
|
||||
read_first_non_empty_env(&["GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL"])
|
||||
{
|
||||
config.editor_background_removal_base_url = base_url;
|
||||
}
|
||||
config.editor_background_removal_token =
|
||||
read_first_non_empty_env(&["GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN"]);
|
||||
if let Some(timeout_ms) = read_first_positive_u64_env(&[
|
||||
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS",
|
||||
]) {
|
||||
config.editor_background_removal_request_timeout_ms = timeout_ms;
|
||||
}
|
||||
if let Some(base_url) = read_first_non_empty_env(&["GENARRATIVE_EDITOR_BGFILTER_BASE_URL"])
|
||||
{
|
||||
config.editor_bgfilter_base_url = base_url;
|
||||
@@ -1612,8 +1590,7 @@ fn parse_positive_u16(raw: &str) -> Option<u16> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
AppConfig, DEFAULT_EDITOR_BACKGROUND_REMOVAL_BASE_URL,
|
||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS, DEFAULT_EDITOR_BGFILTER_BASE_URL,
|
||||
AppConfig, DEFAULT_EDITOR_BGFILTER_BASE_URL,
|
||||
DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS,
|
||||
DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD,
|
||||
DEFAULT_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS,
|
||||
@@ -1638,15 +1615,6 @@ mod tests {
|
||||
assert!(config.llm_base_url.is_empty());
|
||||
// assert!(config.apimart_base_url.is_empty());
|
||||
assert!(config.vector_engine_base_url.is_empty());
|
||||
assert_eq!(
|
||||
config.editor_background_removal_base_url,
|
||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_BASE_URL
|
||||
);
|
||||
assert_eq!(
|
||||
config.editor_background_removal_request_timeout_ms,
|
||||
DEFAULT_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS
|
||||
);
|
||||
assert!(config.editor_background_removal_token.is_none());
|
||||
assert_eq!(
|
||||
config.editor_bgfilter_base_url,
|
||||
DEFAULT_EDITOR_BGFILTER_BASE_URL
|
||||
@@ -2387,46 +2355,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_editor_background_removal_settings() {
|
||||
let _guard = ENV_LOCK
|
||||
.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.expect("env lock should not poison");
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS");
|
||||
std::env::set_var(
|
||||
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL",
|
||||
"http://10.0.0.12:8090",
|
||||
);
|
||||
std::env::set_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN", "token-1");
|
||||
std::env::set_var(
|
||||
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS",
|
||||
"90000",
|
||||
);
|
||||
}
|
||||
|
||||
let config = AppConfig::from_env();
|
||||
assert_eq!(
|
||||
config.editor_background_removal_base_url,
|
||||
"http://10.0.0.12:8090"
|
||||
);
|
||||
assert_eq!(
|
||||
config.editor_background_removal_token.as_deref(),
|
||||
Some("token-1")
|
||||
);
|
||||
assert_eq!(config.editor_background_removal_request_timeout_ms, 90_000);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN");
|
||||
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_editor_bgfilter_settings_and_reuses_background_token() {
|
||||
let _guard = ENV_LOCK
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -684,6 +684,7 @@ pub async fn generate_external_editor_character_animation(
|
||||
request_context,
|
||||
principal.owner_user_id().to_string(),
|
||||
payload,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -750,6 +751,7 @@ fn editor_generation_caller(
|
||||
owner_user_id: principal.owner_user_id().to_string(),
|
||||
audit_subject_user_id: Some(principal.owner_user_id().to_string()),
|
||||
audit_project_id: normalize_optional_string(project_id),
|
||||
phase_reporter: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,6 +884,20 @@ mod tests {
|
||||
["sliceWarning"]["anyOf"][0]["$ref"],
|
||||
"#/components/schemas/EditorIconSpritesheetSliceWarning"
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorImageGenerationResponse"]["properties"]
|
||||
["warning"]["anyOf"][0]["$ref"],
|
||||
"#/components/schemas/EditorGenerationWarning"
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorIconSpritesheetGenerationResponse"]["properties"]
|
||||
["warning"]["anyOf"][0]["$ref"],
|
||||
"#/components/schemas/EditorGenerationWarning"
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorGenerationWarning"]["required"],
|
||||
json!(["code", "reason"])
|
||||
);
|
||||
assert!(
|
||||
parsed["paths"]
|
||||
.get("/api/external/v1/editor/ui-designs/assets/extractions")
|
||||
|
||||
@@ -172,6 +172,9 @@ fn map_external_generation_job_status(
|
||||
100,
|
||||
None,
|
||||
),
|
||||
"running" if job.phase.as_deref() == Some("processing") => {
|
||||
(ExternalGenerationJobStatus::Running, "正在处理。", 70, None)
|
||||
}
|
||||
"running" => (ExternalGenerationJobStatus::Running, "正在生成。", 35, None),
|
||||
"failed" => (
|
||||
ExternalGenerationJobStatus::Failed,
|
||||
@@ -314,6 +317,7 @@ mod tests {
|
||||
refund_ledger_id: None,
|
||||
notification_acknowledged_at: None,
|
||||
notification_acknowledged_at_micros: None,
|
||||
phase: None,
|
||||
warning_message: Some("连通域数量不足".to_string()),
|
||||
};
|
||||
let status = map_external_generation_job_status_detail(summary.clone());
|
||||
@@ -326,4 +330,66 @@ mod tests {
|
||||
assert_eq!(task.warning.as_deref(), Some("连通域数量不足"));
|
||||
assert!(task.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_running_processing_phase_from_backend_projection() {
|
||||
let status = map_external_generation_job_status(ExternalGenerationJobSummaryRecord {
|
||||
job_id: "task-processing".to_string(),
|
||||
job_kind: "editor_character_animation_generation".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_module: "editor-canvas".to_string(),
|
||||
source_entity_id: "project-1".to_string(),
|
||||
request_label: "角色动作生成".to_string(),
|
||||
request_prompt: None,
|
||||
status: "running".to_string(),
|
||||
last_error_message: None,
|
||||
created_at: "2026-07-13T08:00:00Z".to_string(),
|
||||
started_at: Some("2026-07-13T08:00:01Z".to_string()),
|
||||
completed_at: None,
|
||||
updated_at: "2026-07-13T08:00:10Z".to_string(),
|
||||
updated_at_micros: 1_000,
|
||||
price_mud_points: 4,
|
||||
refund_ledger_id: None,
|
||||
notification_acknowledged_at: None,
|
||||
notification_acknowledged_at_micros: None,
|
||||
phase: Some("processing".to_string()),
|
||||
warning_message: None,
|
||||
});
|
||||
|
||||
assert_eq!(status.status, ExternalGenerationJobStatus::Running);
|
||||
assert_eq!(status.phase_detail, "正在处理。");
|
||||
assert_eq!(status.progress, 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_legacy_running_job_without_phase_as_generating() {
|
||||
let mut job = ExternalGenerationJobSummaryRecord {
|
||||
job_id: "task-legacy".to_string(),
|
||||
job_kind: "editor_image_generation".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_module: "editor-canvas".to_string(),
|
||||
source_entity_id: "project-1".to_string(),
|
||||
request_label: "图片生成".to_string(),
|
||||
request_prompt: None,
|
||||
status: "running".to_string(),
|
||||
last_error_message: None,
|
||||
created_at: "2026-07-13T08:00:00Z".to_string(),
|
||||
started_at: Some("2026-07-13T08:00:01Z".to_string()),
|
||||
completed_at: None,
|
||||
updated_at: "2026-07-13T08:00:10Z".to_string(),
|
||||
updated_at_micros: 1_000,
|
||||
price_mud_points: 4,
|
||||
refund_ledger_id: None,
|
||||
notification_acknowledged_at: None,
|
||||
notification_acknowledged_at_micros: None,
|
||||
phase: None,
|
||||
warning_message: None,
|
||||
};
|
||||
|
||||
let legacy = map_external_generation_job_status(job.clone());
|
||||
assert_eq!(legacy.phase_detail, "正在生成。");
|
||||
job.phase = Some("generating".to_string());
|
||||
let generating = map_external_generation_job_status(job);
|
||||
assert_eq!(generating.phase_detail, "正在生成。");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -276,6 +276,7 @@ pub struct AppStateInner {
|
||||
llm_client: Option<LlmClient>,
|
||||
creative_agent_gpt5_client: Option<LlmClient>,
|
||||
matting_client: Option<MattingClient>,
|
||||
editor_bgfilter_http_client: reqwest::Client,
|
||||
creative_agent_executor: Arc<MockLangChainRustAgentExecutor>,
|
||||
// Phase 1 任务 E 的 creative session facade 暂存在 api-server。
|
||||
// creative_agent_* 表由任务 D 收口后,这里只保留读写 facade。
|
||||
@@ -515,6 +516,7 @@ impl AppState {
|
||||
let llm_client = build_llm_client(&config)?;
|
||||
let creative_agent_gpt5_client = build_creative_agent_gpt5_client(&config)?;
|
||||
let matting_client = build_matting_client(&config)?;
|
||||
let editor_bgfilter_http_client = build_editor_bgfilter_http_client(&config)?;
|
||||
let http_request_permit_pools = HttpRequestPermitPools::from_config(&config);
|
||||
let (profile_recharge_order_updates, _) = broadcast::channel(128);
|
||||
|
||||
@@ -555,6 +557,7 @@ impl AppState {
|
||||
llm_client,
|
||||
creative_agent_gpt5_client,
|
||||
matting_client,
|
||||
editor_bgfilter_http_client,
|
||||
creative_agent_executor: Arc::new(MockLangChainRustAgentExecutor),
|
||||
creative_agent_sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
profile_recharge_order_updates,
|
||||
@@ -1255,6 +1258,10 @@ impl AppState {
|
||||
self.matting_client.as_ref()
|
||||
}
|
||||
|
||||
pub fn editor_bgfilter_http_client(&self) -> &reqwest::Client {
|
||||
&self.editor_bgfilter_http_client
|
||||
}
|
||||
|
||||
pub fn creative_agent_executor(&self) -> Arc<MockLangChainRustAgentExecutor> {
|
||||
self.creative_agent_executor.clone()
|
||||
}
|
||||
@@ -1954,6 +1961,25 @@ fn build_matting_client(config: &AppConfig) -> Result<Option<MattingClient>, App
|
||||
.map_err(|error| AppStateInitError::DependencyUnavailable(error.to_string()))
|
||||
}
|
||||
|
||||
fn build_editor_bgfilter_http_client(
|
||||
config: &AppConfig,
|
||||
) -> Result<reqwest::Client, AppStateInitError> {
|
||||
reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_millis(
|
||||
config.editor_bgfilter_request_timeout_ms.max(1),
|
||||
))
|
||||
.connect_timeout(std::time::Duration::from_secs(30))
|
||||
.pool_idle_timeout(std::time::Duration::from_secs(300))
|
||||
.pool_max_idle_per_host(64)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(60))
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
AppStateInitError::DependencyUnavailable(format!(
|
||||
"构建共享 BgFilter HTTP 客户端失败:{error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn build_wechat_client(config: &AppConfig) -> WechatClient {
|
||||
WechatClient::new(WechatConfig {
|
||||
app_id: config.wechat_mini_program_app_id.clone(),
|
||||
|
||||
Reference in New Issue
Block a user