新增图片画布编辑器

新增 /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,209 @@
import { requestJson } from '../apiClient';
const EDITOR_PROJECT_API_BASE = '/api/editor/projects';
const EDITOR_IMAGE_GENERATION_API = '/api/editor/images/generations';
const EDITOR_IMAGE_EDIT_API = '/api/editor/images/edits';
const DEFAULT_PROJECT_TITLE = '未命名画布';
const EDITOR_PROJECT_REQUEST_OPTIONS = {
authImpact: 'local' as const,
};
export type EditorCanvasViewport = {
x: number;
y: number;
scale: number;
};
export type EditorProjectLayerSnapshot = Record<string, unknown> & {
layerId: string;
resourceId: string;
};
export type EditorProjectResourceSourceType =
| 'uploaded'
| 'generated'
| 'mock_generated';
export type EditorProjectResourceSnapshot = {
resourceId: string;
projectId: string;
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: EditorProjectResourceSourceType;
prompt?: string | null;
actualPrompt?: string | null;
model?: string | null;
provider?: string | null;
taskId?: string | null;
sourceResourceId?: string | null;
createdAt?: string;
updatedAt?: string;
};
export type EditorImageGenerationInput = {
prompt: string;
};
export type EditorImageEditInput = {
prompt: string;
sourceImageSrc: string;
};
export type EditorImageGenerationResult = {
imageSrc: string;
width: number;
height: number;
sourceType: 'generated';
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
};
export type EditorProjectSnapshot = {
projectId: string;
title: string;
viewport: EditorCanvasViewport;
layers: EditorProjectLayerSnapshot[];
resources: EditorProjectResourceSnapshot[];
updatedAt: string;
};
export type EditorProjectCreateInput = {
title?: string;
};
export type EditorProjectLayoutSaveInput = {
viewport: EditorCanvasViewport;
layers: EditorProjectLayerSnapshot[];
};
export type EditorProjectResourceCreateInput = {
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: EditorProjectResourceSourceType;
prompt?: string | null;
actualPrompt?: string | null;
model?: string | null;
provider?: string | null;
taskId?: string | null;
sourceResourceId?: string | null;
};
type EditorProjectResponse = {
project: EditorProjectSnapshot;
};
type EditorProjectRecentResponse = {
project: EditorProjectSnapshot | null;
};
type EditorProjectResourceResponse = {
resource: EditorProjectResourceSnapshot;
};
type EditorImageGenerationResponse = EditorImageGenerationResult;
function jsonRequest(method: 'POST' | 'PATCH', body: Record<string, unknown>) {
return {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
};
}
export async function loadRecentEditorProject() {
return requestJson<EditorProjectRecentResponse>(
`${EDITOR_PROJECT_API_BASE}/recent`,
{ method: 'GET' },
'读取图片画布工程失败',
EDITOR_PROJECT_REQUEST_OPTIONS,
);
}
export async function createEditorProject(input: EditorProjectCreateInput = {}) {
const response = await requestJson<EditorProjectResponse>(
EDITOR_PROJECT_API_BASE,
jsonRequest('POST', { title: input.title?.trim() || DEFAULT_PROJECT_TITLE }),
'创建图片画布工程失败',
EDITOR_PROJECT_REQUEST_OPTIONS,
);
return response.project;
}
export async function loadOrCreateRecentEditorProject() {
const response = await loadRecentEditorProject();
if (response.project) {
return response.project;
}
return createEditorProject({ title: DEFAULT_PROJECT_TITLE });
}
export async function saveEditorProjectLayout(
projectId: string,
input: EditorProjectLayoutSaveInput,
) {
const response = await requestJson<EditorProjectResponse>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}`,
jsonRequest('PATCH', {
viewport: input.viewport,
layers: input.layers,
}),
'保存图片画布工程失败',
EDITOR_PROJECT_REQUEST_OPTIONS,
);
return response.project;
}
export async function createEditorProjectResource(
projectId: string,
input: EditorProjectResourceCreateInput,
) {
const response = await requestJson<EditorProjectResourceResponse>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}/resources`,
jsonRequest('POST', { ...input }),
'创建图片画布资源失败',
EDITOR_PROJECT_REQUEST_OPTIONS,
);
return response.resource;
}
export async function generateEditorImage(input: EditorImageGenerationInput) {
return requestJson<EditorImageGenerationResponse>(
EDITOR_IMAGE_GENERATION_API,
jsonRequest('POST', { prompt: input.prompt }),
'生成图片失败',
{
...EDITOR_PROJECT_REQUEST_OPTIONS,
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}
export async function editEditorImage(input: EditorImageEditInput) {
return requestJson<EditorImageGenerationResponse>(
EDITOR_IMAGE_EDIT_API,
jsonRequest('POST', {
prompt: input.prompt,
sourceImageSrc: input.sourceImageSrc,
}),
'修改图片失败',
{
...EDITOR_PROJECT_REQUEST_OPTIONS,
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}