Files
Genarrative/server-rs/crates/spacetime-client/src/editor_project.rs
T
lhk229 ae2d1a3a21
Project CI / AI game creator shell Rust crates (push) Successful in 1m47s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m1s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 5m18s
Project CI / Frontend tests (push) Successful in 2m27s
Project CI / Backend tests (push) Successful in 5m49s
Project CI / Native shell tests (push) Successful in 6m21s
Project CI / Repository checks (push) Successful in 3m36s
Project CI / AI game creator shell web tests (push) Successful in 2m54s
修复 AGC 生成准备依赖全量素材库的问题 (#592)
修复已有项目和素材目录绑定时,AGC 生成准备仍读取完整账户素材库、导致无关素材读取失败阻断生成的问题。

- 新增站内与 External v1 的单目录 GET,按主键读取并校验账号归属;复用目录 DTO,不读取素材列表或创建默认目录。
- 已有 binding 和 partial 恢复改为确认目标目录;首次建绑直接沿用稳定幂等创建,不再读取全库。
- 目录缺失、越权、超时、响应身份不符或会话切换时停止,保留恢复证据,不自动重建、不换键重发生成;已有生成账本继续沿用冻结快照恢复。
- 既有素材库与目录创建、修改、删除 API 合同保持不变,持久化表结构不变。同步 OpenAPI、生成绑定及技术文档。

验证:
- AGC Rust:目录绑定 7 项、生成 94 项、资源编辑 60 项、抠图 5 项通过(目录绑定用例包含在生成组中)。
- api-server:External Editor 30 项、受保护路由矩阵 3 项通过。
- 真实隔离 SpacetimeDB:目录读取成功、空目录、缺失、越权、删除后缺失、只读不变,以及既有幂等/并发/孤儿检查通过。
- smoke 脚本测试 4 项通过;module 编译、生成绑定、87 张表 schema guard、DDD、编码、文档索引、定向 Rust 格式与 diff 检查通过。
- 已比较 OpenAPI:除新增单目录 GET 外,原有定义逐项保持一致。

验证限制:未完成独立 API `/healthz` 联调,本地隔离数据库与授权配置未就绪;启动尝试已停止并清理。未运行全量测试或真实付费生成。

发布顺序:先发布后端模块与 API,再发布新客户端;不提供全库回退。#574 的素材分页、筛选及按素材 ID 读取不在本 PR 范围内。

Closes #550

Reviewed-on: #592
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-10-03 14:48:54 +08:00

1895 lines
70 KiB
Rust

use super::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorGenerationTargetPreflightRecordInput {
pub owner_user_id: String,
pub project_id: Option<String>,
pub asset_folder_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorPixelArtCanvasPlaceholderRecordInput {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub original_width: f64,
pub original_height: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorPixelArtCanvasCompletionRecordInput {
pub dialog_id: String,
pub title: String,
pub placeholder: EditorPixelArtCanvasPlaceholderRecordInput,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorPixelArtResultPreflightRecordInput {
pub owner_user_id: String,
pub project_id: String,
pub asset_folder_id: String,
pub project_resource: EditorProjectResourceCreateRecordInput,
pub canvas_completion: EditorPixelArtCanvasCompletionRecordInput,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorPixelArtResultPersistRecordInput {
pub owner_user_id: String,
pub project_id: String,
pub operation_id: String,
pub operation_fingerprint: String,
pub asset_object: module_assets::AssetObjectUpsertInput,
pub project_resource: EditorProjectResourceCreateRecordInput,
pub asset: EditorAssetCreateRecordInput,
pub canvas_completion: EditorPixelArtCanvasCompletionRecordInput,
pub completed_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorPixelArtResultPersistRecord {
pub asset_object: module_assets::AssetObjectUpsertSnapshot,
pub project_resource: EditorProjectResourceRecord,
pub asset: EditorAssetRecord,
pub project: Option<EditorProjectRecord>,
}
impl From<EditorPixelArtCanvasPlaceholderRecordInput>
for crate::module_bindings::EditorPixelArtCanvasPlaceholderInput
{
fn from(input: EditorPixelArtCanvasPlaceholderRecordInput) -> Self {
Self {
x: input.x,
y: input.y,
width: input.width,
height: input.height,
original_width: input.original_width,
original_height: input.original_height,
}
}
}
impl From<EditorGenerationTargetPreflightRecordInput>
for crate::module_bindings::EditorGenerationTargetPreflightInput
{
fn from(input: EditorGenerationTargetPreflightRecordInput) -> Self {
Self {
owner_user_id: input.owner_user_id,
project_id: input.project_id,
asset_folder_id: input.asset_folder_id,
}
}
}
impl From<EditorPixelArtCanvasCompletionRecordInput>
for crate::module_bindings::EditorPixelArtCanvasCompletionInput
{
fn from(input: EditorPixelArtCanvasCompletionRecordInput) -> Self {
Self {
dialog_id: input.dialog_id,
title: input.title,
placeholder: input.placeholder.into(),
}
}
}
impl From<EditorPixelArtResultPreflightRecordInput>
for crate::module_bindings::EditorPixelArtResultPreflightInput
{
fn from(input: EditorPixelArtResultPreflightRecordInput) -> Self {
Self {
owner_user_id: input.owner_user_id,
project_id: input.project_id,
asset_folder_id: input.asset_folder_id,
project_resource: input.project_resource.into(),
canvas_completion: input.canvas_completion.into(),
}
}
}
impl From<EditorPixelArtResultPersistRecordInput>
for crate::module_bindings::EditorPixelArtResultPersistInput
{
fn from(input: EditorPixelArtResultPersistRecordInput) -> Self {
Self {
owner_user_id: input.owner_user_id,
project_id: input.project_id,
operation_id: input.operation_id,
operation_fingerprint: input.operation_fingerprint,
asset_object: input.asset_object.into(),
project_resource: input.project_resource.into(),
asset: input.asset.into(),
canvas_completion: input.canvas_completion.into(),
completed_at_micros: input.completed_at_micros,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorSpritesheetSlicePersistItemRecordInput {
pub asset_object: module_assets::AssetObjectUpsertInput,
pub project_resource: Option<EditorProjectResourceCreateRecordInput>,
pub asset: Option<EditorAssetCreateRecordInput>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorSpritesheetSliceBatchPersistRecordInput {
pub owner_user_id: String,
pub task_id: String,
pub group_task_id: Option<String>,
pub expected_asset_count: u32,
pub items: Vec<EditorSpritesheetSlicePersistItemRecordInput>,
pub completed_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorSpritesheetSlicePersistedItemRecord {
pub asset_object: module_assets::AssetObjectUpsertSnapshot,
pub project_resource: Option<EditorProjectResourceRecord>,
pub asset: Option<EditorAssetRecord>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorSpritesheetSliceBatchPersistRecord {
pub items: Vec<EditorSpritesheetSlicePersistedItemRecord>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorGenerationResultPersistItemRecordInput {
pub slot: String,
pub asset_object: Option<module_assets::AssetObjectUpsertInput>,
pub project_resource: Option<EditorProjectResourceCreateRecordInput>,
pub asset: Option<EditorAssetCreateRecordInput>,
pub binding: Option<module_assets::AssetEntityBindingInput>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorGenerationResultPersistRecordInput {
pub owner_user_id: String,
pub operation_kind: String,
pub operation_id: String,
pub operation_fingerprint: String,
pub items: Vec<EditorGenerationResultPersistItemRecordInput>,
pub canvas_layout: Option<EditorProjectLayoutSaveV2RecordInput>,
pub job_completion: Option<ExternalGenerationJobCompleteRecordInput>,
pub completed_at_micros: i64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EditorGenerationResultPersistStatusRecord {
Applied,
AlreadyApplied,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorGenerationResultPersistedItemRecord {
pub slot: String,
pub asset_object: Option<module_assets::AssetObjectUpsertSnapshot>,
pub project_resource: Option<EditorProjectResourceRecord>,
pub asset: Option<EditorAssetRecord>,
pub binding: Option<module_assets::AssetEntityBindingSnapshot>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorGenerationResultPersistRecord {
pub status: EditorGenerationResultPersistStatusRecord,
pub items: Vec<EditorGenerationResultPersistedItemRecord>,
pub project: Option<EditorProjectRecord>,
pub job: Option<ExternalGenerationJobRecord>,
}
impl From<EditorSpritesheetSlicePersistItemRecordInput>
for crate::module_bindings::EditorSpritesheetSlicePersistItemInput
{
fn from(input: EditorSpritesheetSlicePersistItemRecordInput) -> Self {
Self {
asset_object: input.asset_object.into(),
project_resource: input.project_resource.map(Into::into),
asset: input.asset.map(Into::into),
}
}
}
impl From<EditorSpritesheetSliceBatchPersistRecordInput>
for crate::module_bindings::EditorSpritesheetSliceBatchPersistInput
{
fn from(input: EditorSpritesheetSliceBatchPersistRecordInput) -> Self {
Self {
owner_user_id: input.owner_user_id,
task_id: input.task_id,
group_task_id: input.group_task_id,
expected_asset_count: input.expected_asset_count,
items: input.items.into_iter().map(Into::into).collect(),
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<EditorGenerationResultPersistItemRecordInput>
for crate::module_bindings::EditorGenerationResultPersistItemInput
{
fn from(input: EditorGenerationResultPersistItemRecordInput) -> Self {
Self {
slot: input.slot,
asset_object: input.asset_object.map(Into::into),
project_resource: input.project_resource.map(Into::into),
asset: input.asset.map(Into::into),
binding: input.binding.map(Into::into),
}
}
}
impl From<EditorGenerationResultPersistRecordInput>
for crate::module_bindings::EditorGenerationResultPersistInput
{
fn from(input: EditorGenerationResultPersistRecordInput) -> Self {
Self {
owner_user_id: input.owner_user_id,
operation_kind: input.operation_kind,
operation_id: input.operation_id,
operation_fingerprint: input.operation_fingerprint,
items: input.items.into_iter().map(Into::into).collect(),
canvas_layout: input.canvas_layout.map(Into::into),
job_completion: input.job_completion.map(Into::into),
completed_at_micros: input.completed_at_micros,
}
}
}
impl SpacetimeClient {
pub async fn resolve_editor_reference(
&self,
input: EditorReferenceResolveRecordInput,
) -> Result<EditorReferenceRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"resolve_editor_reference_and_return",
move |connection, sender| {
connection
.procedures()
.resolve_editor_reference_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_reference_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn preflight_editor_generation_target(
&self,
input: EditorGenerationTargetPreflightRecordInput,
) -> Result<(), SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"preflight_editor_generation_target_and_return",
move |connection, sender| {
connection
.procedures()
.preflight_editor_generation_target_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_generation_target_preflight_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn preflight_editor_pixel_art_result(
&self,
input: EditorPixelArtResultPreflightRecordInput,
) -> Result<(), SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"preflight_editor_pixel_art_result_and_return",
move |connection, sender| {
connection
.procedures()
.preflight_editor_pixel_art_result_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_pixel_art_result_preflight_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn persist_editor_pixel_art_result(
&self,
input: EditorPixelArtResultPersistRecordInput,
) -> Result<EditorPixelArtResultPersistRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"persist_editor_pixel_art_result_and_return",
move |connection, sender| {
connection
.procedures()
.persist_editor_pixel_art_result_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_pixel_art_result_persist_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn persist_editor_spritesheet_slice_batch(
&self,
input: EditorSpritesheetSliceBatchPersistRecordInput,
) -> Result<EditorSpritesheetSliceBatchPersistRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"persist_editor_spritesheet_slice_batch_and_return",
move |connection, sender| {
connection
.procedures()
.persist_editor_spritesheet_slice_batch_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_spritesheet_slice_batch_persist_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn persist_editor_generation_result(
&self,
input: EditorGenerationResultPersistRecordInput,
) -> Result<EditorGenerationResultPersistRecord, SpacetimeClientError> {
self.persist_editor_generation_result_with_dispatch(input, || {})
.await
}
pub async fn persist_editor_generation_result_with_dispatch<F>(
&self,
input: EditorGenerationResultPersistRecordInput,
on_dispatch: F,
) -> Result<EditorGenerationResultPersistRecord, SpacetimeClientError>
where
F: FnOnce() + Send + 'static,
{
let procedure_input = input.into();
self.call_after_connect(
"persist_editor_generation_result_and_return",
move |connection, sender| {
on_dispatch();
connection
.procedures()
.persist_editor_generation_result_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_generation_result_persist_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn create_editor_project(
&self,
input: EditorProjectCreateRecordInput,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"create_editor_project_and_return",
move |connection, sender| {
connection
.procedures()
.create_editor_project_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_required_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn get_recent_editor_project(
&self,
owner_user_id: String,
) -> Result<Option<EditorProjectRecord>, SpacetimeClientError> {
let procedure_input = EditorProjectGetRecentInput { owner_user_id };
self.call_after_connect(
"get_recent_editor_project_and_return",
move |connection, sender| {
connection
.procedures()
.get_recent_editor_project_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_optional_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn get_editor_project(
&self,
input: EditorProjectGetRecordInput,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"get_editor_project_and_return",
move |connection, sender| {
connection.procedures().get_editor_project_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_required_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn list_editor_projects(
&self,
owner_user_id: String,
) -> Result<Vec<EditorProjectRecord>, SpacetimeClientError> {
let procedure_input = EditorProjectListInput { owner_user_id };
self.call_after_connect(
"list_editor_projects_and_return",
move |connection, sender| {
connection
.procedures()
.list_editor_projects_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_list_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn rename_editor_project(
&self,
input: EditorProjectRenameRecordInput,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"rename_editor_project_and_return",
move |connection, sender| {
connection
.procedures()
.rename_editor_project_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_required_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn delete_editor_project(
&self,
input: EditorProjectDeleteRecordInput,
) -> Result<String, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"delete_editor_project_and_return",
move |connection, sender| {
connection
.procedures()
.delete_editor_project_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_delete_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn save_editor_project_layout(
&self,
input: EditorProjectLayoutSaveRecordInput,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"save_editor_project_layout_and_return",
move |connection, sender| {
connection
.procedures()
.save_editor_project_layout_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_required_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn save_editor_project_layout_ack(
&self,
input: EditorProjectLayoutSaveRecordInput,
) -> Result<EditorProjectLayoutSaveAckRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"save_editor_project_layout_ack",
move |connection, sender| {
connection.procedures().save_editor_project_layout_ack_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_layout_save_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn save_editor_project_layout_v2_ack(
&self,
input: EditorProjectLayoutSaveV2RecordInput,
) -> Result<EditorProjectLayoutSaveV2AckRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"save_editor_project_layout_v_2_ack",
move |connection, sender| {
connection
.procedures()
.save_editor_project_layout_v_2_ack_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_layout_save_v2_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn save_editor_project_layout_v2(
&self,
input: EditorProjectLayoutSaveV2RecordInput,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"save_editor_project_layout_v_2_and_return",
move |connection, sender| {
connection
.procedures()
.save_editor_project_layout_v_2_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_required_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn create_editor_project_resource(
&self,
input: EditorProjectResourceCreateRecordInput,
) -> Result<EditorProjectResourceRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"create_editor_project_resource_and_return",
move |connection, sender| {
connection
.procedures()
.create_editor_project_resource_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_resource_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn repair_editor_project_resource_media(
&self,
input: EditorProjectResourceMediaRepairRecordInput,
) -> Result<EditorProjectResourceRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"repair_editor_project_resource_media_and_return",
move |connection, sender| {
connection
.procedures()
.repair_editor_project_resource_media_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_resource_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn update_editor_project_resource_showcase(
&self,
input: EditorProjectResourceShowcaseUpdateRecordInput,
) -> Result<EditorProjectResourceRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"update_editor_project_resource_showcase_and_return",
move |connection, sender| {
connection
.procedures()
.update_editor_project_resource_showcase_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_resource_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn list_public_editor_project_resources(
&self,
input: EditorProjectResourcePublicShowcaseListRecordInput,
) -> Result<Vec<EditorProjectResourceRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"list_public_editor_project_resources_and_return",
move |connection, sender| {
connection
.procedures()
.list_public_editor_project_resources_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_project_resource_list_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn get_editor_asset_library(
&self,
owner_user_id: String,
now_micros: i64,
) -> Result<EditorAssetLibraryRecord, SpacetimeClientError> {
let procedure_input = EditorAssetLibraryGetInput {
owner_user_id,
now_micros,
};
self.call_after_connect(
"get_editor_asset_library_and_return",
move |connection, sender| {
connection
.procedures()
.get_editor_asset_library_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_library_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn admin_list_editor_assets(
&self,
input: AdminEditorAssetListRecordInput,
) -> Result<Vec<AdminEditorAssetRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"admin_list_editor_assets_and_return",
move |connection, sender| {
connection
.procedures()
.admin_list_editor_assets_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_admin_editor_asset_list_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn find_editor_asset_group_source(
&self,
input: EditorAssetGroupSourceLookupRecordInput,
) -> Result<Option<EditorAssetRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"find_editor_asset_group_source_and_return",
move |connection, sender| {
connection
.procedures()
.find_editor_asset_group_source_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_optional_editor_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn get_editor_asset_folder(
&self,
input: EditorAssetFolderGetRecordInput,
) -> Result<EditorAssetFolderRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"get_editor_asset_folder_and_return",
move |connection, sender| {
connection
.procedures()
.get_editor_asset_folder_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_folder_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn create_editor_asset_folder(
&self,
input: EditorAssetFolderCreateRecordInput,
) -> Result<EditorAssetFolderRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"create_editor_asset_folder_and_return",
move |connection, sender| {
connection
.procedures()
.create_editor_asset_folder_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_folder_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn update_editor_asset_folder(
&self,
input: EditorAssetFolderUpdateRecordInput,
) -> Result<EditorAssetFolderRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"update_editor_asset_folder_and_return",
move |connection, sender| {
connection
.procedures()
.update_editor_asset_folder_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_folder_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn delete_editor_asset_folder(
&self,
input: EditorAssetFolderDeleteRecordInput,
) -> Result<EditorAssetLibraryRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"delete_editor_asset_folder_and_return",
move |connection, sender| {
connection
.procedures()
.delete_editor_asset_folder_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_folder_library_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn create_editor_asset(
&self,
input: EditorAssetCreateRecordInput,
) -> Result<EditorAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"create_editor_asset_and_return",
move |connection, sender| {
connection.procedures().create_editor_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn complete_editor_asset_group_cohort(
&self,
input: EditorAssetGroupCohortCompleteRecordInput,
) -> Result<EditorAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"complete_editor_asset_group_cohort_and_return",
move |connection, sender| {
connection
.procedures()
.complete_editor_asset_group_cohort_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn update_editor_asset(
&self,
input: EditorAssetUpdateRecordInput,
) -> Result<EditorAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"update_editor_asset_and_return",
move |connection, sender| {
connection.procedures().update_editor_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn repair_editor_asset_media(
&self,
input: EditorAssetMediaRepairRecordInput,
) -> Result<EditorAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"repair_editor_asset_media_and_return",
move |connection, sender| {
connection
.procedures()
.repair_editor_asset_media_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn delete_editor_asset(
&self,
input: EditorAssetDeleteRecordInput,
) -> Result<EditorAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"delete_editor_asset_and_return",
move |connection, sender| {
connection.procedures().delete_editor_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn submit_editor_showcase_asset(
&self,
input: EditorShowcaseAssetSubmitRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"submit_editor_showcase_asset_and_return",
move |connection, sender| {
connection
.procedures()
.submit_editor_showcase_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn list_public_editor_showcase_assets(
&self,
input: EditorShowcaseAssetPublicListRecordInput,
) -> Result<Vec<EditorShowcaseAssetRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"list_public_editor_showcase_assets_and_return",
move |connection, sender| {
connection
.procedures()
.list_public_editor_showcase_assets_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_list_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn list_public_editor_showcase_assets_for_viewer(
&self,
input: EditorShowcaseAssetViewerListRecordInput,
) -> Result<Vec<EditorShowcaseAssetViewerRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"list_public_editor_showcase_assets_for_viewer_and_return",
move |connection, sender| {
connection
.procedures()
.list_public_editor_showcase_assets_for_viewer_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_viewer_list_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn admin_list_editor_showcase_assets(
&self,
input: EditorShowcaseAssetAdminListRecordInput,
) -> Result<Vec<EditorShowcaseAssetRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"admin_list_editor_showcase_assets_and_return",
move |connection, sender| {
connection
.procedures()
.admin_list_editor_showcase_assets_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_list_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn admin_review_editor_showcase_asset(
&self,
input: EditorShowcaseAssetAdminReviewRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"admin_review_editor_showcase_asset_and_return",
move |connection, sender| {
connection
.procedures()
.admin_review_editor_showcase_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn update_editor_showcase_asset_display(
&self,
input: EditorShowcaseAssetDisplayUpdateRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"update_editor_showcase_asset_display_and_return",
move |connection, sender| {
connection
.procedures()
.update_editor_showcase_asset_display_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn mark_editor_showcase_asset_refunded(
&self,
input: EditorShowcaseAssetRefundMarkRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"mark_editor_showcase_asset_refunded_and_return",
move |connection, sender| {
connection
.procedures()
.mark_editor_showcase_asset_refunded_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn set_editor_showcase_asset_like_for_viewer(
&self,
input: EditorShowcaseAssetLikeToggleRecordInput,
) -> Result<EditorShowcaseAssetViewerRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"set_editor_showcase_asset_like_for_viewer_and_return",
move |connection, sender| {
connection
.procedures()
.set_editor_showcase_asset_like_for_viewer_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_viewer_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn get_editor_showcase_campaign_config(
&self,
input: EditorShowcaseCampaignConfigGetRecordInput,
) -> Result<Option<EditorShowcaseCampaignConfigRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"get_editor_showcase_campaign_config_and_return",
move |connection, sender| {
connection
.procedures()
.get_editor_showcase_campaign_config_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_campaign_config_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn upsert_editor_showcase_campaign_config(
&self,
input: EditorShowcaseCampaignConfigUpsertRecordInput,
) -> Result<Option<EditorShowcaseCampaignConfigRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"upsert_editor_showcase_campaign_config_and_return",
move |connection, sender| {
connection
.procedures()
.upsert_editor_showcase_campaign_config_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_campaign_config_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn get_editor_generation_pricing_config(
&self,
) -> Result<Option<EditorGenerationPricingConfigRecord>, SpacetimeClientError> {
self.call_after_connect(
"get_editor_generation_pricing_config_and_return",
move |connection, sender| {
connection
.procedures()
.get_editor_generation_pricing_config_and_return_then(move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(
map_editor_generation_pricing_config_optional_procedure_result,
);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn upsert_editor_generation_pricing_config(
&self,
input: EditorGenerationPricingConfigUpsertRecordInput,
) -> Result<EditorGenerationPricingConfigRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"upsert_editor_generation_pricing_config_and_return",
move |connection, sender| {
connection
.procedures()
.upsert_editor_generation_pricing_config_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(
map_editor_generation_pricing_config_required_procedure_result,
);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn initialize_editor_generation_pricing_config_if_missing(
&self,
input: EditorGenerationPricingConfigUpsertRecordInput,
) -> Result<EditorGenerationPricingConfigRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"initialize_editor_generation_pricing_config_if_missing_and_return",
move |connection, sender| {
connection
.procedures()
.initialize_editor_generation_pricing_config_if_missing_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(
map_editor_generation_pricing_config_required_procedure_result,
);
send_once(&sender, mapped);
},
);
},
)
.await
}
}
fn map_editor_pixel_art_result_preflight_result(
result: crate::module_bindings::EditorPixelArtResultPreflightResult,
) -> Result<(), SpacetimeClientError> {
if result.ok {
return Ok(());
}
Err(SpacetimeClientError::procedure_failed(result.error_message))
}
fn map_editor_generation_target_preflight_result(
result: crate::module_bindings::EditorGenerationTargetPreflightResult,
) -> Result<(), SpacetimeClientError> {
if result.ok {
return Ok(());
}
Err(SpacetimeClientError::procedure_failed(result.error_message))
}
fn map_editor_pixel_art_result_persist_result(
result: crate::module_bindings::EditorPixelArtResultPersistResult,
) -> Result<EditorPixelArtResultPersistRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
// 中文注释:三种落库状态(Applied / DialogMissing / AlreadyApplied)目前没有任何
// 上层消费方,不再向上透传;但状态缺失说明 procedure 返回体不完整,仍要拦在这里。
if result.status.is_none() {
return Err(SpacetimeClientError::validation_failed(
"完美像素持久化结果缺少状态",
));
}
let asset_object = result.asset_object.ok_or_else(|| {
SpacetimeClientError::validation_failed("完美像素持久化结果缺少 asset object")
})?;
let project_resource = result
.project_resource
.ok_or_else(|| SpacetimeClientError::validation_failed("完美像素持久化结果缺少项目资源"))?;
let asset = result
.asset
.ok_or_else(|| SpacetimeClientError::validation_failed("完美像素持久化结果缺少账号素材"))?;
let project = result
.project
.map(|project| {
map_editor_project_optional_procedure_result(
crate::module_bindings::EditorProjectProcedureResult {
ok: true,
project: Some(project),
error_message: None,
},
)
})
.transpose()?
.flatten();
Ok(EditorPixelArtResultPersistRecord {
asset_object: map_editor_spritesheet_asset_object_snapshot(asset_object),
project_resource: map_editor_spritesheet_project_resource_snapshot(project_resource)?,
asset: map_editor_spritesheet_asset_snapshot(asset)?,
project,
})
}
fn map_editor_spritesheet_slice_batch_persist_result(
result: crate::module_bindings::EditorSpritesheetSliceBatchPersistResult,
) -> Result<EditorSpritesheetSliceBatchPersistRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let items = result
.items
.into_iter()
.map(|item| {
Ok(EditorSpritesheetSlicePersistedItemRecord {
asset_object: map_editor_spritesheet_asset_object_snapshot(item.asset_object),
project_resource: item
.project_resource
.map(map_editor_spritesheet_project_resource_snapshot)
.transpose()?,
asset: item
.asset
.map(map_editor_spritesheet_asset_snapshot)
.transpose()?,
})
})
.collect::<Result<Vec<_>, SpacetimeClientError>>()?;
Ok(EditorSpritesheetSliceBatchPersistRecord { items })
}
fn map_editor_generation_result_persist_result(
result: crate::module_bindings::EditorGenerationResultPersistResult,
) -> Result<EditorGenerationResultPersistRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let status = match result
.status
.ok_or_else(|| SpacetimeClientError::validation_failed("编辑器生成结果原子提交缺少状态"))?
{
crate::module_bindings::EditorGenerationResultPersistStatus::Applied => {
EditorGenerationResultPersistStatusRecord::Applied
}
crate::module_bindings::EditorGenerationResultPersistStatus::AlreadyApplied => {
EditorGenerationResultPersistStatusRecord::AlreadyApplied
}
};
let items = result
.items
.into_iter()
.map(|item| {
Ok(EditorGenerationResultPersistedItemRecord {
slot: item.slot,
asset_object: item
.asset_object
.map(map_editor_spritesheet_asset_object_snapshot),
project_resource: item
.project_resource
.map(map_editor_spritesheet_project_resource_snapshot)
.transpose()?,
asset: item
.asset
.map(map_editor_spritesheet_asset_snapshot)
.transpose()?,
binding: item.binding.map(map_editor_generation_binding_snapshot),
})
})
.collect::<Result<Vec<_>, SpacetimeClientError>>()?;
let project = result
.project
.map(|project| {
map_editor_project_optional_procedure_result(
crate::module_bindings::EditorProjectProcedureResult {
ok: true,
project: Some(project),
error_message: None,
},
)
})
.transpose()?
.flatten();
let job = result.job.map(map_external_generation_job_snapshot);
Ok(EditorGenerationResultPersistRecord {
status,
items,
project,
job,
})
}
fn map_editor_generation_binding_snapshot(
snapshot: crate::module_bindings::AssetEntityBindingSnapshot,
) -> module_assets::AssetEntityBindingSnapshot {
module_assets::AssetEntityBindingSnapshot {
binding_id: snapshot.binding_id,
asset_object_id: snapshot.asset_object_id,
entity_kind: snapshot.entity_kind,
entity_id: snapshot.entity_id,
slot: snapshot.slot,
asset_kind: snapshot.asset_kind,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
fn map_editor_spritesheet_asset_object_snapshot(
snapshot: crate::module_bindings::AssetObjectUpsertSnapshot,
) -> module_assets::AssetObjectUpsertSnapshot {
module_assets::AssetObjectUpsertSnapshot {
asset_object_id: snapshot.asset_object_id,
bucket: snapshot.bucket,
object_key: snapshot.object_key,
access_policy: match snapshot.access_policy {
crate::module_bindings::AssetObjectAccessPolicy::Private => {
module_assets::AssetObjectAccessPolicy::Private
}
crate::module_bindings::AssetObjectAccessPolicy::PublicRead => {
module_assets::AssetObjectAccessPolicy::PublicRead
}
},
content_type: snapshot.content_type,
content_length: snapshot.content_length,
content_hash: snapshot.content_hash,
version: snapshot.version,
source_job_id: snapshot.source_job_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
entity_id: snapshot.entity_id,
asset_kind: snapshot.asset_kind,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
fn map_editor_spritesheet_project_resource_snapshot(
snapshot: crate::module_bindings::EditorProjectResourceSnapshot,
) -> Result<EditorProjectResourceRecord, SpacetimeClientError> {
Ok(EditorProjectResourceRecord {
resource_id: snapshot.resource_id,
project_id: snapshot.project_id,
owner_user_id: snapshot.owner_user_id,
asset_object_id: snapshot.asset_object_id,
image_src: snapshot.image_src,
object_key: snapshot.object_key,
width: snapshot.width,
height: snapshot.height,
source_type: snapshot.source_type,
prompt: snapshot.prompt,
actual_prompt: snapshot.actual_prompt,
model: snapshot.model,
provider: snapshot.provider,
task_id: snapshot.task_id,
source_resource_id: snapshot.source_resource_id,
asset_kind: snapshot.asset_kind,
generation_inputs: parse_editor_spritesheet_generation_inputs(
snapshot.generation_inputs_json,
"图片画布图集切片资源生成输入 JSON",
)?,
image_sequence_frames: parse_editor_spritesheet_generation_inputs(
snapshot.image_sequence_frames_json,
"图片画布图集切片资源序列帧 JSON",
)?,
image_sequence_duration_ms: snapshot.image_sequence_duration_ms,
public_showcase_enabled: snapshot.public_showcase_enabled,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
})
}
fn map_editor_spritesheet_asset_snapshot(
snapshot: crate::module_bindings::EditorAssetSnapshot,
) -> Result<EditorAssetRecord, SpacetimeClientError> {
Ok(EditorAssetRecord {
asset_id: snapshot.asset_id,
folder_id: snapshot.folder_id,
label: snapshot.label,
asset_object_id: snapshot.asset_object_id,
image_src: snapshot.image_src,
object_key: snapshot.object_key,
width: snapshot.width,
height: snapshot.height,
source_type: snapshot.source_type,
prompt: snapshot.prompt,
actual_prompt: snapshot.actual_prompt,
model: snapshot.model,
provider: snapshot.provider,
task_id: snapshot.task_id,
asset_kind: snapshot.asset_kind,
generation_inputs: parse_editor_spritesheet_generation_inputs(
snapshot.generation_inputs_json,
"图片画布图集切片素材生成输入 JSON",
)?,
image_sequence_frames: parse_editor_spritesheet_generation_inputs(
snapshot.image_sequence_frames_json,
"图片画布图集切片素材序列帧 JSON",
)?,
image_sequence_duration_ms: snapshot.image_sequence_duration_ms,
source_resource_id: snapshot.source_resource_id,
public_showcase_enabled: snapshot.public_showcase_enabled,
thumbnail_src: snapshot.thumbnail_src,
generation_cost_mud_points: snapshot.generation_cost_mud_points,
showcase_id: snapshot.showcase_id,
showcase_review_status: snapshot.showcase_review_status,
showcase_display_enabled: snapshot.showcase_display_enabled,
showcase_like_count: snapshot.showcase_like_count,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
})
}
fn parse_editor_spritesheet_generation_inputs(
raw: Option<String>,
label: &str,
) -> Result<Option<serde_json::Value>, SpacetimeClientError> {
raw.map(|value| {
serde_json::from_str(&value).map_err(|error| {
SpacetimeClientError::validation_failed(format!("{label} 无法解析:{error}"))
})
})
.transpose()
}
#[cfg(test)]
mod editor_generation_result_persist_mapper_tests {
use super::*;
fn empty_result(ok: bool) -> crate::module_bindings::EditorGenerationResultPersistResult {
crate::module_bindings::EditorGenerationResultPersistResult {
ok,
status: None,
items: Vec::new(),
project: None,
job: None,
error_message: None,
}
}
#[test]
fn editor_generation_result_mapper_preserves_procedure_failure() {
let mut result = empty_result(false);
result.error_message = Some("事务提交失败".to_string());
let error = map_editor_generation_result_persist_result(result)
.expect_err("ok=false 必须保留 procedure 失败");
assert!(matches!(
error,
SpacetimeClientError::Procedure(message) if message == "事务提交失败"
));
}
#[test]
fn editor_generation_result_mapper_requires_status() {
let error = map_editor_generation_result_persist_result(empty_result(true))
.expect_err("成功结果缺少状态时必须拒绝");
assert!(matches!(
error,
SpacetimeClientError::Runtime(message)
if message == "编辑器生成结果原子提交缺少状态"
));
}
#[test]
fn editor_generation_result_mapper_preserves_apply_status() {
let cases = [
(
crate::module_bindings::EditorGenerationResultPersistStatus::Applied,
EditorGenerationResultPersistStatusRecord::Applied,
),
(
crate::module_bindings::EditorGenerationResultPersistStatus::AlreadyApplied,
EditorGenerationResultPersistStatusRecord::AlreadyApplied,
),
];
for (binding_status, expected) in cases {
let mut result = empty_result(true);
result.status = Some(binding_status);
let mapped =
map_editor_generation_result_persist_result(result).expect("完整状态应映射成功");
assert_eq!(mapped.status, expected);
assert!(mapped.items.is_empty());
assert!(mapped.project.is_none());
assert!(mapped.job.is_none());
}
}
}
#[cfg(test)]
mod pixel_art_persist_mapper_tests {
use super::*;
fn empty_pixel_art_persist_result(
ok: bool,
) -> crate::module_bindings::EditorPixelArtResultPersistResult {
crate::module_bindings::EditorPixelArtResultPersistResult {
ok,
status: None,
asset_object: None,
project_resource: None,
asset: None,
project: None,
error_message: None,
}
}
fn pixel_art_asset_object_snapshot() -> crate::module_bindings::AssetObjectUpsertSnapshot {
crate::module_bindings::AssetObjectUpsertSnapshot {
asset_object_id: "assetobj_pixel".to_string(),
bucket: "private".to_string(),
object_key: "editor/pixel.png".to_string(),
access_policy: crate::module_bindings::AssetObjectAccessPolicy::Private,
content_type: Some("image/png".to_string()),
content_length: 16,
content_hash: Some("output-sha256".to_string()),
version: 1,
source_job_id: Some("pixel-art-snap-dialog-1".to_string()),
owner_user_id: Some("user-1".to_string()),
profile_id: None,
entity_id: Some("project-1".to_string()),
asset_kind: "editor_pixel_art_snap".to_string(),
created_at_micros: 10,
updated_at_micros: 11,
}
}
fn pixel_art_project_resource_snapshot() -> crate::module_bindings::EditorProjectResourceSnapshot
{
crate::module_bindings::EditorProjectResourceSnapshot {
resource_id: "editor-resource-pixel".to_string(),
project_id: "project-1".to_string(),
owner_user_id: "user-1".to_string(),
asset_object_id: Some("assetobj_pixel".to_string()),
image_src: "/api/assets/assetobj_pixel/content".to_string(),
object_key: Some("editor/pixel.png".to_string()),
width: 32,
height: 32,
source_type: "perfect-pixel".to_string(),
prompt: None,
actual_prompt: None,
model: None,
provider: None,
task_id: Some("pixel-art-snap-dialog-1".to_string()),
source_resource_id: Some("source-resource".to_string()),
asset_kind: None,
generation_inputs_json: Some(r#"{"source":"canvas"}"#.to_string()),
public_showcase_enabled: false,
created_at_micros: 10,
updated_at_micros: 11,
image_sequence_frames_json: None,
image_sequence_duration_ms: None,
}
}
fn pixel_art_asset_snapshot() -> crate::module_bindings::EditorAssetSnapshot {
crate::module_bindings::EditorAssetSnapshot {
asset_id: "editor-asset-pixel".to_string(),
folder_id: "folder-1".to_string(),
label: "Pixel result".to_string(),
asset_object_id: Some("assetobj_pixel".to_string()),
image_src: "/api/assets/assetobj_pixel/content".to_string(),
object_key: Some("editor/pixel.png".to_string()),
width: 32,
height: 32,
source_type: "perfect-pixel".to_string(),
prompt: None,
actual_prompt: None,
model: None,
provider: None,
task_id: Some("pixel-art-snap-dialog-1".to_string()),
asset_kind: None,
generation_inputs_json: Some(r#"{"source":"canvas"}"#.to_string()),
source_resource_id: Some("source-resource".to_string()),
public_showcase_enabled: Some(false),
created_at_micros: 10,
updated_at_micros: 11,
thumbnail_src: None,
generation_cost_mud_points: 0,
showcase_id: None,
showcase_review_status: None,
showcase_display_enabled: None,
showcase_like_count: None,
group_task_id: None,
image_sequence_frames_json: None,
image_sequence_duration_ms: None,
}
}
fn successful_pixel_art_persist_result(
status: crate::module_bindings::EditorPixelArtResultPersistStatus,
) -> crate::module_bindings::EditorPixelArtResultPersistResult {
crate::module_bindings::EditorPixelArtResultPersistResult {
ok: true,
status: Some(status),
asset_object: Some(pixel_art_asset_object_snapshot()),
project_resource: Some(pixel_art_project_resource_snapshot()),
asset: Some(pixel_art_asset_snapshot()),
project: None,
error_message: None,
}
}
#[test]
fn editor_pixel_art_persist_mapper_rejects_procedure_failure_before_snapshots() {
let mut result = empty_pixel_art_persist_result(false);
result.error_message = Some("事务提交失败".to_string());
let error = map_editor_pixel_art_result_persist_result(result)
.expect_err("ok=false 必须保留 procedure 失败");
assert!(matches!(
error,
SpacetimeClientError::Procedure(message) if message == "事务提交失败"
));
}
#[test]
fn editor_pixel_art_persist_mapper_rejects_missing_required_snapshot() {
let mut result = empty_pixel_art_persist_result(true);
result.status = Some(crate::module_bindings::EditorPixelArtResultPersistStatus::Applied);
let error = map_editor_pixel_art_result_persist_result(result)
.expect_err("成功结果缺少 asset object 时必须拒绝");
assert!(matches!(
error,
SpacetimeClientError::Runtime(message)
if message == "完美像素持久化结果缺少 asset object"
));
}
#[test]
fn editor_pixel_art_persist_mapper_rejects_missing_status() {
let mut result = successful_pixel_art_persist_result(
crate::module_bindings::EditorPixelArtResultPersistStatus::Applied,
);
result.status = None;
let error = map_editor_pixel_art_result_persist_result(result)
.expect_err("成功结果缺少状态时必须拒绝");
assert!(matches!(
error,
SpacetimeClientError::Runtime(message)
if message == "完美像素持久化结果缺少状态"
));
}
#[test]
fn editor_pixel_art_persist_mapper_accepts_every_status_and_missing_project() {
let cases = [
crate::module_bindings::EditorPixelArtResultPersistStatus::Applied,
crate::module_bindings::EditorPixelArtResultPersistStatus::DialogMissing,
crate::module_bindings::EditorPixelArtResultPersistStatus::AlreadyApplied,
];
for binding_status in cases {
let mapped = map_editor_pixel_art_result_persist_result(
successful_pixel_art_persist_result(binding_status),
)
.expect("project=None 仍应映射成功");
assert_eq!(mapped.asset_object.asset_object_id, "assetobj_pixel");
assert_eq!(mapped.project_resource.resource_id, "editor-resource-pixel");
assert_eq!(mapped.asset.asset_id, "editor-asset-pixel");
assert!(mapped.project.is_none());
}
}
}