diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 440bd9b5d..8061c3b7d 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -622,7 +622,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - `generation_inputs_json` 包络契约:`fields` / `references` 是图片信息读取的用户可见生成输入快照;顶层允许保存后端内部结果扩展。现有 `screenColorHex` 保存实际背景色,角色、图标图集和 UI 图集抠图派生资产使用 `mattingProvider` / `mattingModel` 保存实际成功的处理后端与模型。BgFilter 保存本次 `seg_model`,阿里云通用抠图保存 `Aliyun Matting / segment-common-image`,本地键色保存 `Genarrative Local / screen-color-keying`。同源画布 BFF 的角色、图标和 UI 请求由前端自动提交 `screenColor=auto` 与默认 `segModel=birefnet`,其中 `segModel` 是不可由用户选择的请求控制字段,不进入 `generationInputs`;`background_mode` 和 `cross_check` 只属于 api-server 到 worker 的内部 RPC。External OpenAPI 不开放 `segModel`。上述内部结果字段不写入 `fields`,普通用户(包括素材 owner)与匿名公开读取均不得取得;普通用户响应还必须省略素材顶层 `provider` 和内部处理 `model`,但保留正常用户可见 `model` 与其他合法的顶层功能字段。后台管理和服务端审计可读取原始值。过滤只作用于普通用户 / 公开响应边界,不修改素材或精选快照,因此历史数据无需迁移。 - 普通用户生成结果契约:图片、图标图集、视频、音频和角色动画的完成响应与新建画布图层均不返回或写入生成 provider;项目资源、素材、精选和 Agent 紧凑结果使用同一读取边界。真实 provider 只保留在持久化、tracking / tracing 和后台管理原始审计中。该规则针对生成供应商元数据,不改变直传票据等必须由客户端执行的存储协议字段。 - 普通用户错误契约:手动去背景和角色动作透明化的原始服务端错误可能包含 BgFilter、分割模型或 provider 细节;Owner HTTP 响应与外部任务状态必须按 job kind 返回稳定业务文案,原始错误只保留在任务记录、tracing 与后台审计。 -- 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`。 +- 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`、`by_editor_project_resource_image_src`。 ### `editor_asset_folder` @@ -636,7 +636,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - Rust 结构体:`EditorAsset` - 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` - 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、可选封面 `thumbnail_src`、OSS 引用、尺寸、来源类型、prompt、provider、真实操作 `task_id`、可选后台归组 `group_task_id`、拆分批次预期数量 `group_task_expected_asset_count`、`asset_kind`、`generation_inputs_json`、可选 `source_resource_id` 和 `generation_cost_mud_points`。`prompt` 固定表示规范化后的用户原始意图,供跨资源搜索和用户侧元数据使用;provider 实际返回的改写只写 `actual_prompt`,提交给 provider 的系统 / 工程化 prompt 不得写入 `prompt`。角色透明图、图标透明图和自动切片等派生产物继承源用户 prompt,并用 `source_resource_id`、`generation_inputs_json`、provider / asset kind 表达处理来源。归组字段追加在表尾并默认 `None`,只用于稳定派生任务的后台分组,不替代 `task_id`;批次是否初始完整以独立完成事实为准,不按当前剩余素材数反推。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,若同次生成也创建了 `editor_project_resource`,则把该 `resource_id` 写入 `source_resource_id`。角色动作生成保留原始绿幕视频中间素材,同时把最终帧序列作为一条 `asset_kind = character-animation` 素材入库:首帧写入 `image_src` / `thumbnail_src`,完整帧列表、FPS、时长和预览视频写入 `generation_inputs_json.characterAnimation`,不把每帧拆成独立素材。生成视频会抽取首帧封面写入 `thumbnail_src`,素材库和再次放入画布时用它作为 video poster。素材库快照通过 `asset_id` 回查对应 `editor_showcase_asset`,供左侧素材菜单展示 `pending` / `approved` / `rejected` 审核状态;公开事实不落在账号素材表,素材库只发起提交审核。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。 -- 索引:`by_editor_asset_owner_user_id`、`by_editor_asset_folder_id`。 +- 索引:`by_editor_asset_owner_user_id`、`by_editor_asset_folder_id`、`by_editor_asset_image_src`。 ### `editor_asset_group_source_provenance` diff --git a/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md b/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md index 93ef6b36a..1447e73ae 100644 --- a/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md +++ b/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md @@ -40,6 +40,7 @@ ## 生成契约 - 前端提交到 `POST /api/editor/icon-spritesheets/generations`。 +- worker 解析主 `referenceImageSrc` 时必须通过 `spacetime-client` 的通用窄查询 `resolve_editor_reference` 在同一事务快照内完成引用解析和 owner 校验:资源 ID / 素材 ID 走主键,对象键按规范化 `image_src="/"` 索引定位单条资源或素材,并通过 `asset_object(bucket, object_key)` 复合索引校验对象 owner。procedure 复用既有 `EditorProjectResourceSnapshot` 或 `EditorAssetSnapshot` 返回唯一已验证行,不接收图标业务类型参数、不新建图标专属快照,也不得拉取当前用户的完整工程列表或素材库。`assetKind="icon-spec"` 与 `genre` 都由图标图集业务代码从返回行校验和提取。引用不存在、owner 不匹配、asset object 不存在或数据库调用失败时 procedure 直接失败;业务类型不符或保存的游戏类型无效时 API 失败;合法规范没有已保存游戏类型时允许 `genre=None`。 - 请求字段: - `referenceImageSrc`:图标规范的稳定引用(当前账号的 `objectKey`、项目资源 ID 或素材 ID);本地临时图必须先上传 OSS,禁止 Data URL / Blob URL。 - `iconDescriptions`:兼容现有接口的图标需求数组,`1..100`;当前画布前端固定把完整文本作为唯一数组元素提交。数组长度只表达请求文本,不作为自动拆分数量。 diff --git a/server-rs/crates/spacetime-client/src/active/mapper.rs b/server-rs/crates/spacetime-client/src/active/mapper.rs index 3f3478293..3deaa5bb4 100644 --- a/server-rs/crates/spacetime-client/src/active/mapper.rs +++ b/server-rs/crates/spacetime-client/src/active/mapper.rs @@ -57,7 +57,8 @@ pub use self::editor_project::{ EditorProjectLayoutSaveV2RecordInput, EditorProjectRecord, EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput, EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord, - EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetAdminListRecordInput, + EditorProjectResourceShowcaseUpdateRecordInput, EditorReferenceRecord, + EditorReferenceResolveRecordInput, EditorShowcaseAssetAdminListRecordInput, EditorShowcaseAssetAdminReviewRecordInput, EditorShowcaseAssetDisplayUpdateRecordInput, EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput, EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput, @@ -108,8 +109,8 @@ pub(crate) use self::editor_project::{ map_editor_project_layout_save_v2_procedure_result, map_editor_project_list_procedure_result, map_editor_project_optional_procedure_result, map_editor_project_required_procedure_result, map_editor_project_resource_list_procedure_result, - map_editor_project_resource_procedure_result, map_editor_showcase_asset_list_procedure_result, - map_editor_showcase_asset_procedure_result, + map_editor_project_resource_procedure_result, map_editor_reference_procedure_result, + map_editor_showcase_asset_list_procedure_result, map_editor_showcase_asset_procedure_result, map_editor_showcase_campaign_config_procedure_result, map_optional_editor_asset_procedure_result, }; diff --git a/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs b/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs index fa3b03ec2..261882edf 100644 --- a/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs @@ -1,5 +1,32 @@ use super::*; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorReferenceResolveRecordInput { + pub owner_user_id: String, + pub bucket: String, + pub reference_id: Option, + pub object_key: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditorReferenceRecord { + ProjectResource(EditorProjectResourceRecord), + Asset(EditorAssetRecord), +} + +impl From + for crate::module_bindings::EditorReferenceResolveInput +{ + fn from(input: EditorReferenceResolveRecordInput) -> Self { + Self { + owner_user_id: input.owner_user_id, + bucket: input.bucket, + reference_id: input.reference_id, + object_key: input.object_key, + } + } +} + #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct EditorCanvasViewportRecord { pub x: f64, @@ -1045,6 +1072,22 @@ pub(crate) fn map_editor_project_resource_procedure_result( .ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布资源快照")) } +pub(crate) fn map_editor_reference_procedure_result( + result: EditorReferenceProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + match (result.resource, result.asset) { + (Some(resource), None) => map_editor_project_resource_snapshot(resource) + .map(EditorReferenceRecord::ProjectResource), + (None, Some(asset)) => map_editor_asset_snapshot(asset).map(EditorReferenceRecord::Asset), + _ => Err(SpacetimeClientError::missing_snapshot( + "编辑器引用资源或素材快照", + )), + } +} + pub(crate) fn map_editor_project_resource_list_procedure_result( result: EditorProjectResourceListProcedureResult, ) -> Result, SpacetimeClientError> { diff --git a/server-rs/crates/spacetime-client/src/editor_project.rs b/server-rs/crates/spacetime-client/src/editor_project.rs index 2e991dc7c..ba0e1cf54 100644 --- a/server-rs/crates/spacetime-client/src/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/editor_project.rs @@ -57,6 +57,27 @@ impl From } impl SpacetimeClient { + pub async fn resolve_editor_reference( + &self, + input: EditorReferenceResolveRecordInput, + ) -> Result { + 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 persist_editor_spritesheet_slice_batch( &self, input: EditorSpritesheetSliceBatchPersistRecordInput, diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 16f931444..9278c4b33 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -50,7 +50,8 @@ pub use mapper::{ EditorProjectLayoutSaveV2RecordInput, EditorProjectRecord, EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput, EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord, - EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetAdminListRecordInput, + EditorProjectResourceShowcaseUpdateRecordInput, EditorReferenceRecord, + EditorReferenceResolveRecordInput, EditorShowcaseAssetAdminListRecordInput, EditorShowcaseAssetAdminReviewRecordInput, EditorShowcaseAssetDisplayUpdateRecordInput, EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput, EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput, diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index da3ac68b9..f98a9a830 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -108,7 +108,8 @@ pub use self::editor_project::{ EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput, EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput, EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput, - EditorShowcaseCampaignConfigRecord, EditorShowcaseCampaignConfigUpsertRecordInput, + EditorReferenceRecord, EditorReferenceResolveRecordInput, EditorShowcaseCampaignConfigRecord, + EditorShowcaseCampaignConfigUpsertRecordInput, }; pub use self::external_api_key::{ ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord, @@ -259,7 +260,7 @@ pub(crate) use self::editor_project::{ map_editor_project_delete_procedure_result, map_editor_project_layout_save_procedure_result, map_editor_project_layout_save_v2_procedure_result, map_editor_project_list_procedure_result, map_editor_project_optional_procedure_result, map_editor_project_required_procedure_result, - map_editor_project_resource_list_procedure_result, + map_editor_project_resource_list_procedure_result, map_editor_reference_procedure_result, map_editor_project_resource_procedure_result, map_editor_showcase_asset_list_procedure_result, map_editor_showcase_asset_procedure_result, map_editor_showcase_campaign_config_procedure_result, diff --git a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs index fa3b03ec2..ac7a2bb3c 100644 --- a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs @@ -1,5 +1,31 @@ use super::*; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorReferenceResolveRecordInput { + pub owner_user_id: String, + pub bucket: String, + pub reference_id: Option, + pub object_key: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditorReferenceRecord { + ProjectResource(EditorProjectResourceRecord), + Asset(EditorAssetRecord), +} + +impl From for crate::module_bindings::EditorReferenceResolveInput +{ + fn from(input: EditorReferenceResolveRecordInput) -> Self { + Self { + owner_user_id: input.owner_user_id, + bucket: input.bucket, + reference_id: input.reference_id, + object_key: input.object_key, + } + } +} + #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct EditorCanvasViewportRecord { pub x: f64, @@ -1045,6 +1071,22 @@ pub(crate) fn map_editor_project_resource_procedure_result( .ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布资源快照")) } +pub(crate) fn map_editor_reference_procedure_result( + result: EditorReferenceProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + match (result.resource, result.asset) { + (Some(resource), None) => map_editor_project_resource_snapshot(resource) + .map(EditorReferenceRecord::ProjectResource), + (None, Some(asset)) => map_editor_asset_snapshot(asset).map(EditorReferenceRecord::Asset), + _ => Err(SpacetimeClientError::missing_snapshot( + "编辑器引用资源或素材快照", + )), + } +} + pub(crate) fn map_editor_project_resource_list_procedure_result( result: EditorProjectResourceListProcedureResult, ) -> Result, SpacetimeClientError> { diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index e0100906a..d6e940b67 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -325,6 +325,8 @@ pub mod editor_project_snapshot_type; pub mod editor_project_table; pub mod editor_project_type; pub mod editor_project_viewport_snapshot_type; +pub mod editor_reference_procedure_result_type; +pub mod editor_reference_resolve_input_type; pub mod editor_showcase_asset_admin_list_input_type; pub mod editor_showcase_asset_admin_review_input_type; pub mod editor_showcase_asset_display_update_input_type; @@ -608,6 +610,7 @@ pub mod renew_external_generation_job_lease_and_return_procedure; pub mod repair_editor_asset_media_and_return_procedure; pub mod repair_editor_canvas_resources_and_return_procedure; pub mod repair_editor_project_resource_media_and_return_procedure; +pub mod resolve_editor_reference_and_return_procedure; pub mod resolve_profile_recharge_refund_manual_review_and_return_procedure; pub mod revoke_database_migration_operator_procedure; pub mod revoke_external_api_key_and_return_procedure; @@ -1148,6 +1151,8 @@ pub use editor_project_snapshot_type::EditorProjectSnapshot; pub use editor_project_table::*; pub use editor_project_type::EditorProject; pub use editor_project_viewport_snapshot_type::EditorProjectViewportSnapshot; +pub use editor_reference_procedure_result_type::EditorReferenceProcedureResult; +pub use editor_reference_resolve_input_type::EditorReferenceResolveInput; pub use editor_showcase_asset_admin_list_input_type::EditorShowcaseAssetAdminListInput; pub use editor_showcase_asset_admin_review_input_type::EditorShowcaseAssetAdminReviewInput; pub use editor_showcase_asset_display_update_input_type::EditorShowcaseAssetDisplayUpdateInput; @@ -1431,6 +1436,7 @@ pub use renew_external_generation_job_lease_and_return_procedure::renew_external pub use repair_editor_asset_media_and_return_procedure::repair_editor_asset_media_and_return; pub use repair_editor_canvas_resources_and_return_procedure::repair_editor_canvas_resources_and_return; pub use repair_editor_project_resource_media_and_return_procedure::repair_editor_project_resource_media_and_return; +pub use resolve_editor_reference_and_return_procedure::resolve_editor_reference_and_return; pub use resolve_profile_recharge_refund_manual_review_and_return_procedure::resolve_profile_recharge_refund_manual_review_and_return; pub use revoke_database_migration_operator_procedure::revoke_database_migration_operator; pub use revoke_external_api_key_and_return_procedure::revoke_external_api_key_and_return; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs index 34f4046be..54e8cb22d 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs @@ -114,6 +114,7 @@ impl __sdk::__query_builder::HasCols for EditorAsset { pub struct EditorAssetIxCols { pub asset_id: __sdk::__query_builder::IxCol, pub folder_id: __sdk::__query_builder::IxCol, + pub image_src: __sdk::__query_builder::IxCol, pub owner_user_id: __sdk::__query_builder::IxCol, } @@ -123,6 +124,7 @@ impl __sdk::__query_builder::HasIxCols for EditorAsset { EditorAssetIxCols { asset_id: __sdk::__query_builder::IxCol::new(table_name, "asset_id"), folder_id: __sdk::__query_builder::IxCol::new(table_name, "folder_id"), + image_src: __sdk::__query_builder::IxCol::new(table_name, "image_src"), owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_project_resource_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_project_resource_type.rs index 4ef4a0a43..7bae75f13 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_project_resource_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_project_resource_type.rs @@ -97,6 +97,7 @@ impl __sdk::__query_builder::HasCols for EditorProjectResource { /// /// Provides typed access to indexed columns for query building. pub struct EditorProjectResourceIxCols { + pub image_src: __sdk::__query_builder::IxCol, pub owner_user_id: __sdk::__query_builder::IxCol, pub project_id: __sdk::__query_builder::IxCol, pub resource_id: __sdk::__query_builder::IxCol, @@ -106,6 +107,7 @@ impl __sdk::__query_builder::HasIxCols for EditorProjectResource { type IxCols = EditorProjectResourceIxCols; fn ix_cols(table_name: &'static str) -> Self::IxCols { EditorProjectResourceIxCols { + image_src: __sdk::__query_builder::IxCol::new(table_name, "image_src"), owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), project_id: __sdk::__query_builder::IxCol::new(table_name, "project_id"), resource_id: __sdk::__query_builder::IxCol::new(table_name, "resource_id"), diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_reference_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_reference_procedure_result_type.rs new file mode 100644 index 000000000..c630e9ab0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_reference_procedure_result_type.rs @@ -0,0 +1,21 @@ +// 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::editor_asset_snapshot_type::EditorAssetSnapshot; +use super::editor_project_resource_snapshot_type::EditorProjectResourceSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorReferenceProcedureResult { + pub ok: bool, + pub resource: Option, + pub asset: Option, + pub error_message: Option, +} + +impl __sdk::InModule for EditorReferenceProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_reference_resolve_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_reference_resolve_input_type.rs new file mode 100644 index 000000000..310ca57db --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_reference_resolve_input_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 EditorReferenceResolveInput { + pub owner_user_id: String, + pub bucket: String, + pub reference_id: Option, + pub object_key: Option, +} + +impl __sdk::InModule for EditorReferenceResolveInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/resolve_editor_reference_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/resolve_editor_reference_and_return_procedure.rs new file mode 100644 index 000000000..e57f5cfb9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/resolve_editor_reference_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::editor_reference_procedure_result_type::EditorReferenceProcedureResult; +use super::editor_reference_resolve_input_type::EditorReferenceResolveInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ResolveEditorReferenceAndReturnArgs { + pub input: EditorReferenceResolveInput, +} + +impl __sdk::InModule for ResolveEditorReferenceAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `resolve_editor_reference_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait resolve_editor_reference_and_return { + fn resolve_editor_reference_and_return(&self, input: EditorReferenceResolveInput) { + self.resolve_editor_reference_and_return_then(input, |_, _| {}); + } + + fn resolve_editor_reference_and_return_then( + &self, + input: EditorReferenceResolveInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl resolve_editor_reference_and_return for super::RemoteProcedures { + fn resolve_editor_reference_and_return_then( + &self, + input: EditorReferenceResolveInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorReferenceProcedureResult>( + "resolve_editor_reference_and_return", + ResolveEditorReferenceAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index 5df4afb18..e1786506e 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -206,7 +206,8 @@ pub struct EditorCanvasLayoutMigration { #[spacetimedb::table( accessor = editor_project_resource, index(accessor = by_editor_project_resource_project_id, btree(columns = [project_id])), - index(accessor = by_editor_project_resource_owner_user_id, btree(columns = [owner_user_id])) + index(accessor = by_editor_project_resource_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_editor_project_resource_image_src, btree(columns = [image_src])) )] #[derive(Clone)] pub struct EditorProjectResource { @@ -255,7 +256,8 @@ pub struct EditorAssetFolder { #[spacetimedb::table( accessor = editor_asset, index(accessor = by_editor_asset_owner_user_id, btree(columns = [owner_user_id])), - index(accessor = by_editor_asset_folder_id, btree(columns = [folder_id])) + index(accessor = by_editor_asset_folder_id, btree(columns = [folder_id])), + index(accessor = by_editor_asset_image_src, btree(columns = [image_src])) )] pub struct EditorAsset { #[primary_key] @@ -590,6 +592,14 @@ pub struct EditorProjectResourceMediaRepairInput { pub updated_at_micros: i64, } +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorReferenceResolveInput { + pub owner_user_id: String, + pub bucket: String, + pub reference_id: Option, + pub object_key: Option, +} + #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct EditorProjectResourceShowcaseUpdateInput { pub resource_id: String, @@ -1080,6 +1090,14 @@ pub struct EditorProjectResourceProcedureResult { pub error_message: Option, } +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorReferenceProcedureResult { + pub ok: bool, + pub resource: Option, + pub asset: Option, + pub error_message: Option, +} + #[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct EditorProjectResourceListProcedureResult { pub ok: bool, @@ -1360,6 +1378,37 @@ pub fn create_editor_project_resource_and_return( } } +#[spacetimedb::procedure] +pub fn resolve_editor_reference_and_return( + ctx: &mut ProcedureContext, + input: EditorReferenceResolveInput, +) -> EditorReferenceProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + require_editor_generation_runtime_service_identity(tx, caller)?; + resolve_editor_reference(tx, &input) + }) { + Ok(ResolvedEditorReference::ProjectResource(resource)) => EditorReferenceProcedureResult { + ok: true, + resource: Some(resource), + asset: None, + error_message: None, + }, + Ok(ResolvedEditorReference::Asset(asset)) => EditorReferenceProcedureResult { + ok: true, + resource: None, + asset: Some(asset), + error_message: None, + }, + Err(message) => EditorReferenceProcedureResult { + ok: false, + resource: None, + asset: None, + error_message: Some(message), + }, + } +} + #[spacetimedb::procedure] pub fn repair_editor_project_resource_media_and_return( ctx: &mut ProcedureContext, @@ -5031,6 +5080,150 @@ fn require_editor_generation_pricing_bootstrap( .map_err(|_| "当前 identity 无权初始化模型定价服务身份".to_string()) } +enum ResolvedEditorReference { + ProjectResource(EditorProjectResourceSnapshot), + Asset(EditorAssetSnapshot), +} + +fn resolve_editor_reference( + ctx: &ReducerContext, + input: &EditorReferenceResolveInput, +) -> Result { + let owner_user_id = input.owner_user_id.trim(); + let bucket = input.bucket.trim(); + if owner_user_id.is_empty() || bucket.is_empty() { + return Err("编辑器引用的 owner_user_id 与 bucket 不能为空".to_string()); + } + let reference_id = input + .reference_id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()); + let object_key = input + .object_key + .as_deref() + .map(str::trim) + .map(|value| value.trim_start_matches('/')) + .filter(|value| !value.is_empty()); + if reference_id.is_some() == object_key.is_some() { + return Err("编辑器引用必须且只能提供 reference_id 或 object_key".to_string()); + } + + if let Some(reference_id) = reference_id { + if let Some(resource) = ctx + .db + .editor_project_resource() + .resource_id() + .find(&reference_id.to_string()) + { + return resolve_editor_reference_resource(ctx, bucket, owner_user_id, &resource); + } + if let Some(asset) = ctx + .db + .editor_asset() + .asset_id() + .find(&reference_id.to_string()) + { + return resolve_editor_reference_asset(ctx, bucket, owner_user_id, asset); + } + return Err("编辑器引用不存在".to_string()); + } + + let object_key = object_key.expect("reference shape checked above"); + let image_src = format!("/{object_key}"); + for resource in ctx + .db + .editor_project_resource() + .by_editor_project_resource_image_src() + .filter(&image_src) + { + return resolve_editor_reference_resource(ctx, bucket, owner_user_id, &resource); + } + for asset in ctx + .db + .editor_asset() + .by_editor_asset_image_src() + .filter(&image_src) + { + return resolve_editor_reference_asset(ctx, bucket, owner_user_id, asset); + } + Err("编辑器引用未关联资源或素材记录".to_string()) +} + +fn resolve_editor_reference_resource( + ctx: &ReducerContext, + bucket: &str, + owner_user_id: &str, + resource: &EditorProjectResource, +) -> Result { + let object_key = validate_editor_reference_record_fields( + "资源", + owner_user_id, + resource.owner_user_id.as_str(), + resource.object_key.as_deref(), + )?; + verify_editor_reference_asset_object(ctx, bucket, owner_user_id, object_key.as_str())?; + let mut snapshot = resource_snapshot_from_row(resource.clone()); + snapshot.object_key = Some(object_key); + Ok(ResolvedEditorReference::ProjectResource(snapshot)) +} + +fn resolve_editor_reference_asset( + ctx: &ReducerContext, + bucket: &str, + owner_user_id: &str, + asset: EditorAsset, +) -> Result { + let object_key = validate_editor_reference_record_fields( + "素材", + owner_user_id, + asset.owner_user_id.as_str(), + asset.object_key.as_deref(), + )?; + verify_editor_reference_asset_object(ctx, bucket, owner_user_id, object_key.as_str())?; + let mut snapshot = asset_snapshot_from_row(ctx, asset); + snapshot.object_key = Some(object_key); + Ok(ResolvedEditorReference::Asset(snapshot)) +} + +fn validate_editor_reference_record_fields( + record_label: &str, + expected_owner_user_id: &str, + actual_owner_user_id: &str, + object_key: Option<&str>, +) -> Result { + if actual_owner_user_id.trim() != expected_owner_user_id { + return Err(format!("编辑器{record_label}引用不属于当前用户")); + } + let object_key = object_key + .map(str::trim) + .map(|value| value.trim_start_matches('/')) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .ok_or_else(|| format!("编辑器{record_label}引用缺少已登记 object_key"))?; + Ok(object_key) +} + +fn verify_editor_reference_asset_object( + ctx: &ReducerContext, + bucket: &str, + owner_user_id: &str, + object_key: &str, +) -> Result<(), String> { + let asset_object = crate::find_asset_object_by_location( + ctx, + &AssetObjectLocationInput { + bucket: bucket.to_string(), + object_key: object_key.to_string(), + }, + )? + .ok_or_else(|| "编辑器引用对应的 asset_object 不存在".to_string())?; + if asset_object.owner_user_id.as_deref().map(str::trim) != Some(owner_user_id) { + return Err("编辑器引用对应的 asset_object 不属于当前用户".to_string()); + } + Ok(()) +} + pub(crate) fn is_editor_generation_runtime_service_identity( ctx: &ReducerContext, candidate: Identity, @@ -10689,4 +10882,33 @@ mod tests { Some("character-animation") ); } + + #[test] + fn editor_reference_record_validation_rejects_owner_and_object_key_failures() { + assert!( + validate_editor_reference_record_fields( + "资源", + "owner-1", + "owner-2", + Some("generated/icon-spec.png"), + ) + .unwrap_err() + .contains("不属于当前用户") + ); + assert!( + validate_editor_reference_record_fields("素材", "owner-1", "owner-1", None,) + .unwrap_err() + .contains("object_key") + ); + assert_eq!( + validate_editor_reference_record_fields( + "素材", + "owner-1", + "owner-1", + Some("/generated/icon-spec.png"), + ) + .unwrap(), + "generated/icon-spec.png" + ); + } }