新增外部生成任务结果查询

新增仅返回任务结果的 SpacetimeDB procedure

编辑器 Agent 回填改用结果查询接口

补齐客户端绑定与接口文档
This commit is contained in:
2026-07-15 13:36:00 +08:00
parent 9a6bf8d86d
commit d55f4ba71c
11 changed files with 231 additions and 7 deletions
@@ -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。
@@ -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(),
})
@@ -307,6 +307,31 @@ impl SpacetimeClient {
.await
}
pub async fn get_external_generation_job_result(
&self,
input: ExternalGenerationJobGetRecordInput,
) -> Result<ExternalGenerationJobResultRecord, SpacetimeClientError> {
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,
@@ -58,6 +58,7 @@ pub use mapper::{
ExternalGenerationJobEnqueueRecordInput, ExternalGenerationJobFailRecordInput,
ExternalGenerationJobGetRecordInput, ExternalGenerationJobListRecord,
ExternalGenerationJobListRecordInput, ExternalGenerationJobRecord,
ExternalGenerationJobResultRecord,
ExternalGenerationJobRenewLeaseRecordInput, ExternalGenerationJobSummaryListRecord,
ExternalGenerationJobSummaryRecord, ExternalGenerationQueueStatsRecord,
FeatureGateConfigRecord, JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType,
@@ -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,
};
@@ -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<ExternalGenerationJobResultRecord, SpacetimeClientError> {
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<Vec<ExternalGenerationJobRecord>, SpacetimeClientError> {
@@ -372,6 +390,14 @@ pub struct ExternalGenerationJobRecord {
pub notification_acknowledged_at_micros: Option<i64>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalGenerationJobResultRecord {
pub job_id: String,
pub status: String,
pub last_error_message: Option<String>,
pub result_payload_json: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalGenerationJobListRecord {
pub jobs: Vec<ExternalGenerationJobRecord>,
@@ -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;
@@ -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<ExternalGenerationJobResultSnapshot>,
pub error_message: Option<String>,
}
impl __sdk::InModule for ExternalGenerationJobResultProcedureResult {
type Module = super::RemoteModule;
}
@@ -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<String>,
pub result_payload_json: Option<String>,
}
impl __sdk::InModule for ExternalGenerationJobResultSnapshot {
type Module = super::RemoteModule;
}
@@ -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<ExternalGenerationJobResultProcedureResult, __sdk::InternalError>,
) + 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<ExternalGenerationJobResultProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, ExternalGenerationJobResultProcedureResult>(
"get_external_generation_job_result_and_return",
GetExternalGenerationJobResultAndReturnArgs { input },
__callback,
);
}
}
@@ -271,6 +271,22 @@ pub struct ExternalGenerationJobProcedureResult {
pub error_message: Option<String>,
}
// 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<String>,
pub result_payload_json: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct ExternalGenerationJobResultProcedureResult {
pub ok: bool,
pub result: Option<ExternalGenerationJobResultSnapshot>,
pub error_message: Option<String>,
}
#[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<ExternalGenerationJobSnapshot, String> {
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<ExternalGenerationJobResultSnapshot, String> {
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 {