新增图片画布编辑器

新增 /editor 图片画布入口与 Lovart 风格画布交互

新增图片画布工程和资源持久化的 SpacetimeDB 表、绑定与 api-server BFF

接入图片生成和修改的 VectorEngine gpt-image-2 后端通道

完善素材库文件夹、重命名、上传删除、图层和元数据交互

补充图片画布技术方案、领域词、执行跟踪和浏览器 smoke 截图
This commit is contained in:
2026-06-13 16:22:18 +08:00
parent f8a80cd795
commit 747473024d
53 changed files with 6694 additions and 29 deletions

View File

@@ -0,0 +1,226 @@
use super::*;
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct EditorCanvasViewportRecord {
pub x: f64,
pub y: f64,
pub scale: f64,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct EditorProjectRecord {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub viewport: EditorCanvasViewportRecord,
pub layers: serde_json::Value,
pub resources: Vec<EditorProjectResourceRecord>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EditorProjectResourceRecord {
pub resource_id: String,
pub project_id: String,
pub asset_object_id: Option<String>,
pub image_src: String,
pub object_key: Option<String>,
pub width: u32,
pub height: u32,
pub source_type: String,
pub prompt: Option<String>,
pub actual_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub source_resource_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorProjectCreateRecordInput {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorProjectGetRecordInput {
pub project_id: String,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorProjectLayoutSaveRecordInput {
pub project_id: String,
pub owner_user_id: String,
pub viewport: EditorCanvasViewportRecord,
pub layers_json: String,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorProjectResourceCreateRecordInput {
pub resource_id: String,
pub project_id: String,
pub owner_user_id: String,
pub asset_object_id: Option<String>,
pub image_src: String,
pub object_key: Option<String>,
pub width: u32,
pub height: u32,
pub source_type: String,
pub prompt: Option<String>,
pub actual_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub source_resource_id: Option<String>,
pub updated_at_micros: i64,
}
impl From<EditorProjectCreateRecordInput> for crate::module_bindings::EditorProjectCreateInput {
fn from(input: EditorProjectCreateRecordInput) -> Self {
Self {
project_id: input.project_id,
owner_user_id: input.owner_user_id,
title: input.title,
now_micros: input.now_micros,
}
}
}
impl From<EditorProjectGetRecordInput> for crate::module_bindings::EditorProjectGetInput {
fn from(input: EditorProjectGetRecordInput) -> Self {
Self {
project_id: input.project_id,
owner_user_id: input.owner_user_id,
}
}
}
impl From<EditorProjectLayoutSaveRecordInput>
for crate::module_bindings::EditorProjectLayoutSaveInput
{
fn from(input: EditorProjectLayoutSaveRecordInput) -> Self {
Self {
project_id: input.project_id,
owner_user_id: input.owner_user_id,
viewport: crate::module_bindings::EditorProjectViewportSnapshot {
x: input.viewport.x,
y: input.viewport.y,
scale: input.viewport.scale,
},
layers_json: input.layers_json,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<EditorProjectResourceCreateRecordInput>
for crate::module_bindings::EditorProjectResourceCreateInput
{
fn from(input: EditorProjectResourceCreateRecordInput) -> Self {
Self {
resource_id: input.resource_id,
project_id: input.project_id,
owner_user_id: input.owner_user_id,
asset_object_id: input.asset_object_id,
image_src: input.image_src,
object_key: input.object_key,
width: input.width,
height: input.height,
source_type: input.source_type,
prompt: input.prompt,
actual_prompt: input.actual_prompt,
model: input.model,
provider: input.provider,
task_id: input.task_id,
source_resource_id: input.source_resource_id,
updated_at_micros: input.updated_at_micros,
}
}
}
pub(crate) fn map_editor_project_optional_procedure_result(
result: EditorProjectProcedureResult,
) -> Result<Option<EditorProjectRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result.project.map(map_editor_project_snapshot).transpose()
}
pub(crate) fn map_editor_project_required_procedure_result(
result: EditorProjectProcedureResult,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
map_editor_project_optional_procedure_result(result)?
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布工程快照"))
}
pub(crate) fn map_editor_project_resource_procedure_result(
result: EditorProjectResourceProcedureResult,
) -> Result<EditorProjectResourceRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.resource
.map(map_editor_project_resource_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布资源快照"))
}
fn map_editor_project_snapshot(
snapshot: EditorProjectSnapshot,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
let layers = serde_json::from_str(&snapshot.layers_json).map_err(|error| {
SpacetimeClientError::validation_failed(format!("图片画布图层布局 JSON 无法解析:{error}"))
})?;
Ok(EditorProjectRecord {
project_id: snapshot.project_id,
owner_user_id: snapshot.owner_user_id,
title: snapshot.title,
viewport: EditorCanvasViewportRecord {
x: snapshot.viewport.x,
y: snapshot.viewport.y,
scale: snapshot.viewport.scale,
},
layers,
resources: snapshot
.resources
.into_iter()
.map(map_editor_project_resource_snapshot)
.collect(),
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
})
}
fn map_editor_project_resource_snapshot(
snapshot: EditorProjectResourceSnapshot,
) -> EditorProjectResourceRecord {
EditorProjectResourceRecord {
resource_id: snapshot.resource_id,
project_id: snapshot.project_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,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}