diff --git a/docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md b/docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md index 6908adc2c..65408b38f 100644 --- a/docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md +++ b/docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md @@ -30,6 +30,7 @@ - `acknowledge_external_generation_jobs_and_return`:按当前账号确认已终态任务的完成 / 失败提示,写入 `notification_acknowledged_at` 并追加审计事件。 - `get_external_generation_queue_stats_and_return`:controller 读取队列积压、运行中任务和过期 lease 数量,用于计算 worker 目标实例数;该 procedure 只读 `external_generation_job`,不直接操作 systemd。 - `get_external_generation_job_and_return`:按 `job_id` 读取单个任务状态,给 BFF 和生成页展示使用;必须只返回调用者有权读取的任务,不能暴露其它用户的 payload、错误详情或 worker 内部字段。 +- `get_external_generation_job_result_and_return`:仅供后端内部回填异步编辑器 Agent 工具调用;按 `job_id + owner_user_id` 返回 `status`、`last_error_message` 和已持久化的 `result_payload_json`,不返回请求 payload、lease 或其它 worker 字段。该 procedure 不替代上述状态读取接口,也不经 BFF 暴露给前端。 这个 Module 的 **Seam** 在 SpacetimeDB procedure + `spacetime-client` facade;`api-server` HTTP role 和 worker role 都只依赖这个 Interface。外部 provider、OSS、计费补偿、玩法草稿回写仍留在 `api-server` worker implementation 内,不进入 SpacetimeDB reducer。 diff --git a/server-rs/crates/api-server/src/editor_agent/reconcile.rs b/server-rs/crates/api-server/src/editor_agent/reconcile.rs index 6a4e44902..68f1bca5e 100644 --- a/server-rs/crates/api-server/src/editor_agent/reconcile.rs +++ b/server-rs/crates/api-server/src/editor_agent/reconcile.rs @@ -1,3 +1,6 @@ +use crate::editor_agent::resp_to_asset; +use crate::http_error::AppError; +use crate::state::AppState; use module_editor_agent::agent::tools::context::EditorToolContext; use module_editor_agent::agent::tools::edit_image::{ EditImageTool, EditImageToolArgs, EditorImageEditResult, @@ -17,9 +20,6 @@ use module_editor_agent::agent::tools::generate_sound_effect::{ }; use module_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool; use module_editor_agent::agent::tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs}; -use crate::editor_agent::resp_to_asset; -use crate::http_error::AppError; -use crate::state::AppState; use module_editor_agent::framework::tool::Tool; use serde_json::Value; use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse}; @@ -50,7 +50,7 @@ pub async fn reconcile_editor_agent_tool_calls( for (message_index, job_id) in candidates { let job = match state .spacetime_client() - .get_external_generation_job(ExternalGenerationJobGetRecordInput { + .get_external_generation_job_result(ExternalGenerationJobGetRecordInput { job_id, owner_user_id: conversation.owner_user_id.clone(), }) diff --git a/server-rs/crates/spacetime-client/src/external_generation.rs b/server-rs/crates/spacetime-client/src/external_generation.rs index 6d70087ea..fc3bc467d 100644 --- a/server-rs/crates/spacetime-client/src/external_generation.rs +++ b/server-rs/crates/spacetime-client/src/external_generation.rs @@ -307,6 +307,31 @@ impl SpacetimeClient { .await } + pub async fn get_external_generation_job_result( + &self, + input: ExternalGenerationJobGetRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "get_external_generation_job_result_and_return", + move |connection, sender| { + connection + .procedures() + .get_external_generation_job_result_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_external_generation_job_result_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + pub async fn get_external_generation_job_summary( &self, input: ExternalGenerationJobGetRecordInput, diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index ea01c864f..9f04f72ef 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -58,6 +58,7 @@ pub use mapper::{ ExternalGenerationJobEnqueueRecordInput, ExternalGenerationJobFailRecordInput, ExternalGenerationJobGetRecordInput, ExternalGenerationJobListRecord, ExternalGenerationJobListRecordInput, ExternalGenerationJobRecord, + ExternalGenerationJobResultRecord, ExternalGenerationJobRenewLeaseRecordInput, ExternalGenerationJobSummaryListRecord, ExternalGenerationJobSummaryRecord, ExternalGenerationQueueStatsRecord, FeatureGateConfigRecord, JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 4b1427806..de6b1d1c8 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -111,7 +111,8 @@ pub use self::external_generation::{ ExternalGenerationJobCompleteRecordInput, ExternalGenerationJobEnqueueRecordInput, ExternalGenerationJobFailRecordInput, ExternalGenerationJobGetRecordInput, ExternalGenerationJobListRecord, ExternalGenerationJobListRecordInput, - ExternalGenerationJobRecord, ExternalGenerationJobRenewLeaseRecordInput, + ExternalGenerationJobRecord, ExternalGenerationJobResultRecord, + ExternalGenerationJobRenewLeaseRecordInput, ExternalGenerationJobSummaryListRecord, ExternalGenerationJobSummaryRecord, ExternalGenerationQueueStatsRecord, }; @@ -255,7 +256,9 @@ pub(crate) use self::external_api_key::{ }; pub(crate) use self::external_generation::{ map_external_generation_job_claim_result, map_external_generation_job_list_result, - map_external_generation_job_procedure_result, map_external_generation_job_summary_list_result, + map_external_generation_job_procedure_result, + map_external_generation_job_result_procedure_result, + map_external_generation_job_summary_list_result, map_external_generation_job_summary_procedure_result, map_external_generation_queue_stats_result, }; diff --git a/server-rs/crates/spacetime-client/src/mapper/external_generation.rs b/server-rs/crates/spacetime-client/src/mapper/external_generation.rs index e032f20c5..50077adc1 100644 --- a/server-rs/crates/spacetime-client/src/mapper/external_generation.rs +++ b/server-rs/crates/spacetime-client/src/mapper/external_generation.rs @@ -112,6 +112,24 @@ pub(crate) fn map_external_generation_job_procedure_result( Ok(map_external_generation_job_snapshot(job)) } +pub(crate) fn map_external_generation_job_result_procedure_result( + result: ExternalGenerationJobResultProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + let result = result.result.ok_or_else(|| { + SpacetimeClientError::missing_snapshot("external_generation_job 结果快照") + })?; + Ok(ExternalGenerationJobResultRecord { + job_id: result.job_id, + status: result.status, + last_error_message: result.last_error_message, + result_payload_json: result.result_payload_json, + }) +} + pub(crate) fn map_external_generation_job_claim_result( result: ExternalGenerationJobProcedureResult, ) -> Result, SpacetimeClientError> { @@ -372,6 +390,14 @@ pub struct ExternalGenerationJobRecord { pub notification_acknowledged_at_micros: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExternalGenerationJobResultRecord { + pub job_id: String, + pub status: String, + pub last_error_message: Option, + pub result_payload_json: Option, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExternalGenerationJobListRecord { pub jobs: Vec, diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index b9b975017..e85a08da9 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -503,8 +503,10 @@ pub mod external_generation_job_list_input_type; pub mod external_generation_job_payload_compaction_input_type; pub mod external_generation_job_payload_compaction_procedure_result_type; pub mod external_generation_job_procedure_result_type; +pub mod external_generation_job_result_procedure_result_type; pub mod external_generation_job_renew_lease_input_type; pub mod external_generation_job_snapshot_type; +pub mod external_generation_job_result_snapshot_type; pub mod external_generation_job_summary_backfill_input_type; pub mod external_generation_job_summary_backfill_procedure_result_type; pub mod external_generation_job_summary_procedure_result_type; @@ -557,6 +559,7 @@ pub mod get_editor_generation_pricing_config_and_return_procedure; pub mod get_editor_project_and_return_procedure; pub mod get_editor_showcase_campaign_config_and_return_procedure; pub mod get_external_generation_job_and_return_procedure; +pub mod get_external_generation_job_result_and_return_procedure; pub mod get_external_generation_job_summary_and_return_procedure; pub mod get_external_generation_queue_stats_and_return_procedure; pub mod get_feature_gate_config_procedure; @@ -1919,8 +1922,10 @@ pub use external_generation_job_list_input_type::ExternalGenerationJobListInput; pub use external_generation_job_payload_compaction_input_type::ExternalGenerationJobPayloadCompactionInput; pub use external_generation_job_payload_compaction_procedure_result_type::ExternalGenerationJobPayloadCompactionProcedureResult; pub use external_generation_job_procedure_result_type::ExternalGenerationJobProcedureResult; +pub use external_generation_job_result_procedure_result_type::ExternalGenerationJobResultProcedureResult; pub use external_generation_job_renew_lease_input_type::ExternalGenerationJobRenewLeaseInput; pub use external_generation_job_snapshot_type::ExternalGenerationJobSnapshot; +pub use external_generation_job_result_snapshot_type::ExternalGenerationJobResultSnapshot; pub use external_generation_job_summary_backfill_input_type::ExternalGenerationJobSummaryBackfillInput; pub use external_generation_job_summary_backfill_procedure_result_type::ExternalGenerationJobSummaryBackfillProcedureResult; pub use external_generation_job_summary_procedure_result_type::ExternalGenerationJobSummaryProcedureResult; @@ -1973,6 +1978,7 @@ pub use get_editor_generation_pricing_config_and_return_procedure::get_editor_ge pub use get_editor_project_and_return_procedure::get_editor_project_and_return; pub use get_editor_showcase_campaign_config_and_return_procedure::get_editor_showcase_campaign_config_and_return; pub use get_external_generation_job_and_return_procedure::get_external_generation_job_and_return; +pub use get_external_generation_job_result_and_return_procedure::get_external_generation_job_result_and_return; pub use get_external_generation_job_summary_and_return_procedure::get_external_generation_job_summary_and_return; pub use get_external_generation_queue_stats_and_return_procedure::get_external_generation_queue_stats_and_return; pub use get_feature_gate_config_procedure::get_feature_gate_config; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/external_generation_job_result_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/external_generation_job_result_procedure_result_type.rs new file mode 100644 index 000000000..4b07dda7d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/external_generation_job_result_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::external_generation_job_result_snapshot_type::ExternalGenerationJobResultSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct ExternalGenerationJobResultProcedureResult { + pub ok: bool, + pub result: Option, + pub error_message: Option, +} + +impl __sdk::InModule for ExternalGenerationJobResultProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/external_generation_job_result_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/external_generation_job_result_snapshot_type.rs new file mode 100644 index 000000000..881d0d786 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/external_generation_job_result_snapshot_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct ExternalGenerationJobResultSnapshot { + pub job_id: String, + pub status: String, + pub last_error_message: Option, + pub result_payload_json: Option, +} + +impl __sdk::InModule for ExternalGenerationJobResultSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_external_generation_job_result_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_external_generation_job_result_and_return_procedure.rs new file mode 100644 index 000000000..d0ff920cd --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_external_generation_job_result_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::external_generation_job_get_input_type::ExternalGenerationJobGetInput; +use super::external_generation_job_result_procedure_result_type::ExternalGenerationJobResultProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetExternalGenerationJobResultAndReturnArgs { + pub input: ExternalGenerationJobGetInput, +} + +impl __sdk::InModule for GetExternalGenerationJobResultAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_external_generation_job_result_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_external_generation_job_result_and_return { + fn get_external_generation_job_result_and_return(&self, input: ExternalGenerationJobGetInput) { + self.get_external_generation_job_result_and_return_then(input, |_, _| {}); + } + + fn get_external_generation_job_result_and_return_then( + &self, + input: ExternalGenerationJobGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_external_generation_job_result_and_return for super::RemoteProcedures { + fn get_external_generation_job_result_and_return_then( + &self, + input: ExternalGenerationJobGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, ExternalGenerationJobResultProcedureResult>( + "get_external_generation_job_result_and_return", + GetExternalGenerationJobResultAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-module/src/external_generation.rs b/server-rs/crates/spacetime-module/src/external_generation.rs index 3563cc8d6..77ba89908 100644 --- a/server-rs/crates/spacetime-module/src/external_generation.rs +++ b/server-rs/crates/spacetime-module/src/external_generation.rs @@ -271,6 +271,22 @@ pub struct ExternalGenerationJobProcedureResult { pub error_message: Option, } +// Private backend read for reconciliation; it intentionally excludes job request and lease data. +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct ExternalGenerationJobResultSnapshot { + pub job_id: String, + pub status: String, + pub last_error_message: Option, + pub result_payload_json: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct ExternalGenerationJobResultProcedureResult { + pub ok: bool, + pub result: Option, + pub error_message: Option, +} + #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct ExternalGenerationJobSummarySnapshot { pub job_id: String, @@ -464,6 +480,23 @@ pub fn get_external_generation_job_and_return( } } +#[spacetimedb::procedure] +pub fn get_external_generation_job_result_and_return( + ctx: &mut ProcedureContext, + input: ExternalGenerationJobGetInput, +) -> ExternalGenerationJobResultProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::editor_project_storage::require_editor_generation_runtime_service_identity( + tx, caller, + )?; + get_external_generation_job_result_tx(tx, input.clone()) + }) { + Ok(result) => single_external_generation_job_result_read_result(result), + Err(message) => failed_external_generation_job_result_read_result(message), + } +} + #[spacetimedb::procedure] pub fn list_external_generation_jobs_and_return( ctx: &mut ProcedureContext, @@ -869,6 +902,14 @@ fn get_external_generation_job_tx( ctx: &ReducerContext, input: ExternalGenerationJobGetInput, ) -> Result { + get_external_generation_job_summary_tx(ctx, input) + .map(map_external_generation_job_summary_to_compat_snapshot) +} + +fn get_external_generation_job_result_tx( + ctx: &ReducerContext, + input: ExternalGenerationJobGetInput, +) -> Result { validate_required("external_generation_job.job_id", &input.job_id)?; validate_required( "external_generation_job.owner_user_id", @@ -885,7 +926,12 @@ fn get_external_generation_job_tx( if row.owner_user_id.trim() != owner_user_id { return Err("external_generation_job 不存在".to_string()); } - Ok(map_external_generation_job_row(row)) + Ok(ExternalGenerationJobResultSnapshot { + job_id: row.job_id, + status: row.status, + last_error_message: row.last_error_message, + result_payload_json: row.result_payload_json, + }) } fn list_external_generation_jobs_tx( @@ -2128,6 +2174,26 @@ fn failed_external_generation_job_result(message: String) -> ExternalGenerationJ } } +fn single_external_generation_job_result_read_result( + result: ExternalGenerationJobResultSnapshot, +) -> ExternalGenerationJobResultProcedureResult { + ExternalGenerationJobResultProcedureResult { + ok: true, + result: Some(result), + error_message: None, + } +} + +fn failed_external_generation_job_result_read_result( + message: String, +) -> ExternalGenerationJobResultProcedureResult { + ExternalGenerationJobResultProcedureResult { + ok: false, + result: None, + error_message: Some(message), + } +} + fn single_external_generation_job_summary_result( job: ExternalGenerationJobSummarySnapshot, ) -> ExternalGenerationJobSummaryProcedureResult {