1486 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 画布服务层
import type {
CanvasInitialProject,
CanvasProject,
CanvasProjectListItem,
CanvasState,
CanvasNode,
CanvasWorkspaceType,
} from '@/types/canvas';
import type { VideoGenerationMode, VideoRatio, VideoResolution } from '@/types/ai';
import { fetchUploadWithRetry } from '@/lib/client/upload-with-retry';
import { requestLongTaskResult } from '@/lib/client/long-task';
import {
buildCanvasSaveRequestBody,
type CanvasSaveRevisionOptions,
} from '@/lib/canvas/canvas-save-request';
import type {
EnhanceProKreaModel,
EnhanceProOutputFormat,
EnhanceProSubjectDetection,
} from '@/lib/enhance-pro/config';
import { createLogger } from '@/lib/observability/logger';
import { useCanvasStore } from '@/store/canvas';
const log = createLogger("Canvas");
function logCatch(
err: unknown,
context: string,
extra?: Record<string, unknown>
): void {
const error = err instanceof Error ? err : new Error(String(err));
log.error(`${context} failed`, error, extra);
}
function getCanvasSaveRevisionFallback(): CanvasSaveRevisionOptions {
if (typeof window === "undefined") return {};
const { canvasRevision, canvasUpdatedAt } = useCanvasStore.getState();
return {
baseCanvasRevision: canvasRevision,
baseCanvasUpdatedAt: canvasUpdatedAt,
};
}
// API 响应类型
interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: string;
status?: number;
code?: string;
canvasUpdatedAt?: string;
pagination?: {
page: number;
limit: number;
hasMore: boolean;
};
}
export interface CanvasLayoutReplaceBatchItemSnapshot {
id: string;
taskId: string;
position: number;
sourceNodeId: string | null;
sourceImageUrl: string;
sourceTitle: string | null;
sourceKind: string;
status: string;
taskStatus: string;
resultImageUrl: string | null;
resultThumbnailUrl: string | null;
resultModelId: string | null;
error: string | null;
addedToCanvas: boolean;
addedCanvasNodeId: string | null;
}
export interface CanvasLayoutReplaceBatchSnapshot {
id: string;
projectId: string;
status: string;
referenceImageUrl: string;
supplementalPrompt: string | null;
imageSize: string;
totalCount: number;
completedCount: number;
failedCount: number;
completedAt: string | null;
createdAt: string;
updatedAt: string;
items: CanvasLayoutReplaceBatchItemSnapshot[];
}
function normalizeRevisionText(value: unknown) {
return String(value || "").trim();
}
function syncCanvasStoreRevisionFromUpdatedAt(updatedAt: unknown): void {
if (typeof window === "undefined") return;
const revision = normalizeRevisionText(updatedAt);
if (!revision) return;
useCanvasStore.getState().setCanvasRevision(revision, revision);
}
function syncCanvasStoreRevisionFromProject(
project?: { canvasData?: CanvasState | null; updatedAt?: Date | string } | null
): void {
if (!project?.canvasData) return;
syncCanvasStoreRevisionFromUpdatedAt(project.updatedAt);
}
export interface NormalizedCropRect {
x: number;
y: number;
width: number;
height: number;
}
export interface CanvasTextDetectItem {
id: string;
text: string;
confidence?: number;
}
export interface CanvasTextEditPayloadItem {
id: string;
originalText: string;
editedText?: string;
textColor?: string;
action?: 'replace' | 'delete';
}
export interface CanvasTextSuggestionItem {
text: string;
reason?: string;
}
export type WorkflowAnalysisMode =
| "video-analysis"
| "character-scene-extraction"
| "story-script"
| "agent-plan";
export interface EnhanceProImageResult {
originalUrl: string;
thumbnailUrl: string;
width?: number;
height?: number;
}
export interface EnhanceProRequestParams {
imageUrl?: string;
image_base64?: string;
mimeType?: string;
prompt?: string;
chatId?: string | null;
messageId?: string | null;
clientRequestId?: string | null;
width: number;
height: number;
batchSize?: number;
seed?: number;
output_format?: EnhanceProOutputFormat;
subject_detection?: EnhanceProSubjectDetection;
face_enhancement?: boolean;
face_enhancement_creativity?: number;
face_enhancement_strength?: number;
crop_to_fill?: boolean;
model?: EnhanceProKreaModel;
upscaling_activated?: boolean;
image_scaling_factor?: number;
creativity?: number;
texture?: number;
sharpen?: number;
denoise?: number;
detail?: number;
}
// 画布服务
export const canvasService = {
// 获取画布列表
async getProjects(
workspaceType: CanvasWorkspaceType = 'canvas',
options: { page?: number; limit?: number } = {}
): Promise<ApiResponse<CanvasProjectListItem[]>> {
try {
const params = new URLSearchParams();
if (workspaceType && workspaceType !== 'canvas') {
params.set('workspaceType', workspaceType);
}
params.set('page', String(Math.max(1, Math.round(options.page || 1))));
params.set('limit', String(Math.max(1, Math.round(options.limit || 24))));
const response = await fetch(`/api/canvas?${params.toString()}`);
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '获取画布列表失败' };
}
},
// 获取单个画布
async getProject(id: string): Promise<ApiResponse<CanvasInitialProject>> {
try {
const response = await fetch(`/api/canvas/${id}`);
const data = await response.json();
if (data?.success) {
syncCanvasStoreRevisionFromProject(data.data);
}
return data;
} catch (error) {
return { success: false, error: '获取画布失败' };
}
},
// 创建画布
async createProject(
project: Partial<CanvasProject>,
workspaceType: CanvasWorkspaceType = 'canvas'
): Promise<ApiResponse<CanvasProject>> {
try {
const response = await fetch('/api/canvas', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...project, workspaceType }),
});
const data = await response.json();
if (data?.success) {
syncCanvasStoreRevisionFromProject(data.data);
}
return data;
} catch (error) {
return { success: false, error: '创建画布失败' };
}
},
// 更新画布
async updateProject(
id: string,
updates: Partial<CanvasProject>,
workspaceType?: CanvasWorkspaceType
): Promise<ApiResponse<CanvasProject>> {
try {
const response = await fetch(`/api/canvas/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(
workspaceType ? { ...updates, workspaceType } : updates
),
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '更新画布失败' };
}
},
// 删除画布
async deleteProject(id: string): Promise<ApiResponse> {
try {
const response = await fetch(`/api/canvas/${id}`, {
method: 'DELETE',
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '删除画布失败' };
}
},
// 保存画布状态
async saveCanvasState(
id: string,
canvasData: CanvasState,
workspaceType?: CanvasWorkspaceType,
options?: CanvasSaveRevisionOptions
): Promise<ApiResponse> {
try {
const fallbackOptions = getCanvasSaveRevisionFallback();
const buildRequestBody = (
revisionOptions?: CanvasSaveRevisionOptions,
shouldUseFallback = true
) =>
JSON.stringify(
buildCanvasSaveRequestBody({
canvasData,
workspaceType,
options: revisionOptions,
fallbackOptions: shouldUseFallback ? fallbackOptions : undefined,
})
);
const doSave = async (requestBody: string) =>
fetch(`/api/canvas/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: requestBody,
});
const doSaveWithRateLimitRetry = async (requestBody: string) => {
let response = await doSave(requestBody);
if (response.status === 429) {
const retryPayload = await response.json().catch(() => ({}));
const retryAfter = Number(retryPayload?.retryAfter || 0);
const waitMs = retryAfter > 0 ? retryAfter * 1000 : 800;
await new Promise((resolve) => setTimeout(resolve, waitMs));
response = await doSave(requestBody);
}
return response;
};
let response = await doSaveWithRateLimitRetry(buildRequestBody(options));
let data = await response.json().catch(() => ({}));
const serverRevision = normalizeRevisionText(data?.canvasUpdatedAt);
if (
response.status === 409 &&
data?.code === "STALE_CANVAS_REVISION" &&
serverRevision
) {
response = await doSaveWithRateLimitRetry(
buildRequestBody(
{
baseCanvasRevision: serverRevision,
baseCanvasUpdatedAt: serverRevision,
},
false
)
);
data = await response.json().catch(() => ({}));
}
if (!response.ok) {
return {
success: false,
error: String(data?.error || data?.message || '保存画布状态失败'),
status: response.status,
code: typeof data?.code === "string" ? data.code : undefined,
canvasUpdatedAt:
typeof data?.canvasUpdatedAt === "string"
? data.canvasUpdatedAt
: undefined,
};
}
if (data?.success) {
syncCanvasStoreRevisionFromUpdatedAt(data?.data?.updatedAt);
}
return data as ApiResponse;
} catch (error) {
return { success: false, error: '保存画布状态失败' };
}
},
// 自动保存(防抖)
createAutoSave(projectId: string, delay = 2000) {
let timeoutId: NodeJS.Timeout | null = null;
return (canvasData: CanvasState) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(async () => {
await this.saveCanvasState(projectId, canvasData);
}, delay);
};
},
};
// 图片生成服务
export const imageGenerationService = {
// 上传图片到存储桶
async uploadImage(file: File): Promise<ApiResponse<{ url: string; pathname: string; assetId?: string }>> {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('purpose', 'canvas-upload');
const response = await fetchUploadWithRetry(formData);
if (!response.ok) {
const errorData = await response.json();
return { success: false, error: errorData.error || '上传失败' };
}
const data = await response.json();
return {
success: true,
data: {
url: data.url,
pathname: data.pathname,
assetId: typeof data.assetId === "string" ? data.assetId : undefined,
}
};
} catch (error) {
logCatch(error, "image_upload");
return { success: false, error: '图片上传失败' };
}
},
// AI 生成图片
async generateImage(params: {
allowFailover?: boolean;
prompt: string;
negativePrompt?: string;
model?: string;
aspectRatio?: string;
imageSize?: string;
imageCount?: number;
promptExtend?: boolean;
referenceImage?: string;
referenceImages?: string[];
billingFeature?: 'multi-view' | 'repaint';
async?: boolean;
}): Promise<
ApiResponse<{
images: Array<{ thumbnailUrl: string; originalUrl: string }>;
model?: string;
prompt?: string;
queueStatus?: unknown;
status?: string;
taskId?: string;
}>
> {
try {
const response = await fetch('/api/canvas/generate/image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
if (data?.success && data?.taskId) {
return {
success: true,
data: {
images: [],
queueStatus: data.queueStatus,
status: data.status,
taskId: data.taskId,
},
};
}
return data;
} catch (error) {
return { success: false, error: '生成图片失败' };
}
},
async layoutReplace(params: {
sourceImageUrl: string;
referenceImageUrl: string;
supplementalPrompt?: string;
model?: string;
imageSize?: string;
sourceNodeId?: string;
agentProjectId?: string;
referenceLabel?: string;
debugMeta?: Record<string, unknown>;
}): Promise<
ApiResponse<{
status?: string;
taskId: string;
}>
> {
try {
const response = await fetch('/api/canvas/layout-replace', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
if (data.success && data.taskId) {
return {
success: true,
data: {
status: data.status,
taskId: data.taskId,
},
};
}
return { success: false, error: data.error || '排版替换提交失败' };
} catch (error) {
logCatch(error, "layout_replace_submit");
return { success: false, error: '排版替换提交失败' };
}
},
async createLayoutReplaceBatch(params: {
projectId: string;
sources: Array<{
sourceImageUrl: string;
sourceKind: "canvas" | "upload";
sourceNodeId?: string;
sourceTitle?: string;
}>;
referenceImageUrl: string;
supplementalPrompt?: string;
model?: string;
imageSize?: string;
}): Promise<ApiResponse<{ batchId: string; firstTaskId: string; pointsCost?: number }>> {
try {
const response = await fetch("/api/canvas/layout-replace/batches", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
});
const data = await response.json().catch(() => ({}));
if (response.ok && data?.success && data?.batchId) {
return {
success: true,
data: {
batchId: data.batchId,
firstTaskId: data.firstTaskId,
pointsCost: data.pointsCost,
},
};
}
return { success: false, error: data?.error || "创建批量排版替换失败" };
} catch (error) {
logCatch(error, "layout_replace_batch_create");
return { success: false, error: "创建批量排版替换失败" };
}
},
async getLayoutReplaceBatch(batchId: string): Promise<
ApiResponse<{ batch: CanvasLayoutReplaceBatchSnapshot }>
> {
try {
const response = await fetch(
`/api/canvas/layout-replace/batches/${encodeURIComponent(batchId)}`,
{ cache: "no-store" }
);
const data = await response.json().catch(() => ({}));
if (response.ok && data?.success && data?.batch) {
return { success: true, data: { batch: data.batch } };
}
return { success: false, error: data?.error || "查询批次失败" };
} catch (error) {
logCatch(error, "layout_replace_batch_fetch", { batchId });
return { success: false, error: "查询批次失败" };
}
},
async getLatestLayoutReplaceBatch(params: {
projectId: string;
sourceNodeId?: string;
}): Promise<ApiResponse<{ batch: CanvasLayoutReplaceBatchSnapshot | null }>> {
try {
const query = new URLSearchParams({ projectId: params.projectId });
if (params.sourceNodeId) query.set("sourceNodeId", params.sourceNodeId);
const response = await fetch(
`/api/canvas/layout-replace/batches?${query.toString()}`,
{ cache: "no-store" }
);
const data = await response.json().catch(() => ({}));
if (response.ok && data?.success) {
return { success: true, data: { batch: data.batch || null } };
}
return { success: false, error: data?.error || "查询批次失败" };
} catch (error) {
logCatch(error, "layout_replace_batch_latest", params);
return { success: false, error: "查询批次失败" };
}
},
async markLayoutReplaceBatchItemsAdded(params: {
batchId: string;
items: Array<{ itemId: string; canvasNodeId: string }>;
}): Promise<ApiResponse> {
try {
const response = await fetch(
`/api/canvas/layout-replace/batches/${encodeURIComponent(params.batchId)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: params.items }),
}
);
const data = await response.json().catch(() => ({}));
return response.ok && data?.success
? { success: true }
: { success: false, error: data?.error || "更新结果状态失败" };
} catch (error) {
logCatch(error, "layout_replace_batch_mark_added", {
batchId: params.batchId,
});
return { success: false, error: "更新结果状态失败" };
}
},
async retryLayoutReplaceBatchItem(params: {
batchId: string;
itemId: string;
}): Promise<ApiResponse<{ taskId: string }>> {
try {
const response = await fetch(
`/api/canvas/layout-replace/batches/${encodeURIComponent(params.batchId)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "retry", itemId: params.itemId }),
}
);
const data = await response.json().catch(() => ({}));
return response.ok && data?.success && data?.taskId
? { success: true, data: { taskId: data.taskId } }
: { success: false, error: data?.error || "重试失败" };
} catch (error) {
logCatch(error, "layout_replace_batch_retry", params);
return { success: false, error: "重试失败" };
}
},
async localEdit(params: {
sourceImageUrl: string;
sourceNodeId?: string;
regions: Array<{
id: string;
index?: number;
color: string;
prompt: string;
rect: NormalizedCropRect;
targetRect?: NormalizedCropRect | null;
referenceImages?: Array<{
id?: string;
url: string;
name?: string;
source?: "upload" | "canvas";
}>;
}>;
model?: string;
imageSize?: string;
aspectRatio?: string;
operation?: "scale-adjust";
scalePercent?: number;
}): Promise<
ApiResponse<{
annotatedReferenceUrl: string;
aspectRatio?: string;
displayPrompt?: string;
prompt: string;
queueStatus?: unknown;
sourceHeight?: number;
sourceWidth?: number;
status?: string;
taskId: string;
}>
> {
try {
const response = await fetch('/api/canvas/local-edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
if (data.success && data.taskId) {
return {
success: true,
data: {
annotatedReferenceUrl: data.annotatedReferenceUrl,
aspectRatio: data.aspectRatio,
displayPrompt: data.displayPrompt,
prompt: data.prompt,
queueStatus: data.queueStatus,
sourceHeight: data.sourceHeight,
sourceWidth: data.sourceWidth,
status: data.status,
taskId: data.taskId,
},
};
}
return { success: false, error: data.error || '局部编辑提交失败' };
} catch (error) {
logCatch(error, "local_edit_submit");
return { success: false, error: '局部编辑提交失败' };
}
},
async getImageTask(taskId: string): Promise<
ApiResponse<{
task: {
id: string;
status: string;
error?: string | null;
result?: {
images?: Array<{ thumbnailUrl: string; originalUrl: string }>;
model?: string;
modelRef?: string;
prompt?: string;
} | null;
queueStatus?: unknown;
};
}>
> {
try {
const response = await fetch(`/api/canvas/generate/image?taskId=${encodeURIComponent(taskId)}`);
const data = await response.json();
if (data.success && data.task) {
return { success: true, data: { task: data.task } };
}
return { success: false, error: data.error || '查询生图任务失败' };
} catch (error) {
logCatch(error, "image_task_fetch", { taskId });
return { success: false, error: '查询生图任务失败' };
}
},
// 图片抠图 - 使用图片URL
async removeBackground(imageUrl: string): Promise<ApiResponse<{ url: string }>> {
try {
const response = await fetch('/api/rmbg', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageUrl }), // 使用 imageUrl 参数
});
const data = await response.json();
if (data.success && data.original_url) {
return { success: true, data: { url: data.original_url } };
}
return { success: false, error: data.error || '抠图失败' };
} catch (error) {
logCatch(error, "remove_background");
return { success: false, error: '抠图失败' };
}
},
async boxCutout(params: {
imageUrl: string;
cropRect: NormalizedCropRect;
paddingRatio?: number;
sourceNodeId?: string;
}): Promise<
ApiResponse<{
url: string;
cropRectApplied?: NormalizedCropRect;
paddedCropRectApplied?: NormalizedCropRect;
}>
> {
try {
const response = await fetch('/api/canvas/box-cutout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
if (data.success && data.original_url) {
return {
success: true,
data: {
url: data.original_url,
cropRectApplied: data.cropRectApplied,
paddedCropRectApplied: data.paddedCropRectApplied,
},
};
}
return { success: false, error: data.error || '框选抠图失败' };
} catch (error) {
logCatch(error, "box_cutout");
return { success: false, error: '框选抠图失败' };
}
},
// 去水印 - 使用图片URL
async removeWatermark(imageUrl: string): Promise<ApiResponse<{ url: string }>> {
try {
const response = await fetch('/api/watermark-remove', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image_url: imageUrl }),
});
const data = await response.json();
if (data.success && data.original_url) {
return { success: true, data: { url: data.original_url } };
}
return { success: false, error: data.error || '去水印失败' };
} catch (error) {
logCatch(error, "remove_watermark");
return { success: false, error: '去水印失败' };
}
},
// 图片增强 - 使用图片URL
async enhanceImage(
imageUrl: string,
options?: {
scaleFactor?: number;
}
): Promise<ApiResponse<{ url: string }>> {
try {
const response = await fetch('/api/enhance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
imageUrl,
scaleFactor: options?.scaleFactor,
}),
});
const data = await response.json();
if (data.success && data.original_url) {
return { success: true, data: { url: data.original_url } };
}
return { success: false, error: data.error || '图片增强失败' };
} catch (error) {
logCatch(error, "enhance_image");
return { success: false, error: '图片增强失败' };
}
},
async enhanceImagePro(
params: EnhanceProRequestParams,
): Promise<ApiResponse<{ images: EnhanceProImageResult[] }>> {
try {
const payload = await requestLongTaskResult({
endpoint: '/api/enhance/pro',
payload: params,
});
if (!payload?.success) {
return { success: false, error: payload?.error || payload?.detail || '图像增强 Pro 失败' };
}
const images: EnhanceProImageResult[] = [];
if (Array.isArray(payload.images)) {
for (const item of payload.images) {
const originalUrl = String(item?.original_url || '').trim();
const thumbnailUrl = String(item?.thumbnail_url || item?.original_url || '').trim();
if (!originalUrl) {
continue;
}
images.push({
originalUrl,
thumbnailUrl,
width: typeof item?.width === 'number' ? item.width : undefined,
height: typeof item?.height === 'number' ? item.height : undefined,
});
}
}
if (images.length === 0 && payload.original_url) {
images.push({
originalUrl: payload.original_url,
thumbnailUrl: payload.thumbnail_url || payload.original_url,
width: typeof payload.width === 'number' ? payload.width : undefined,
height: typeof payload.height === 'number' ? payload.height : undefined,
});
}
if (images.length === 0) {
return { success: false, error: '图像增强 Pro 未返回有效结果' };
}
return {
success: true,
data: {
images,
},
};
} catch (error) {
logCatch(error, "enhance_image_pro");
if (error instanceof Error && /failed to fetch|network/i.test(error.message)) {
return {
success: false,
error: '任务可能已提交,网络连接暂时中断,请稍后重试查看结果,避免重复提交。',
};
}
return {
success: false,
error: error instanceof Error ? error.message : '图像增强 Pro 失败',
};
}
},
// 图片扩展 - 使用图片URL
async extendImage(
imageUrl: string,
options?:
| string
| {
prompt?: string;
xScale?: number;
yScale?: number;
}
): Promise<ApiResponse<{ url: string }>> {
try {
const normalizedOptions =
typeof options === 'string'
? {
prompt: options.trim() ? `图像拓展处理:${options.trim()}` : undefined,
}
: options || {};
const response = await fetch('/api/extend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_url: imageUrl,
prompt: normalizedOptions.prompt || '图像拓展处理',
includeBase64: false,
...(typeof normalizedOptions.xScale === 'number'
? { xScale: normalizedOptions.xScale }
: {}),
...(typeof normalizedOptions.yScale === 'number'
? { yScale: normalizedOptions.yScale }
: {}),
}),
});
const data = await response.json();
if (data.success && data.original_url) {
return { success: true, data: { url: data.original_url } };
}
return { success: false, error: data.error || data.detail || '图片扩展失败' };
} catch (error) {
logCatch(error, "extend_image");
return { success: false, error: '图片扩展失败' };
}
},
async splitImageLayers(imageUrl: string): Promise<
ApiResponse<{
layers: Array<{
originalUrl: string;
thumbnailUrl: string;
title: string;
layerIndex: number;
totalLayers: number;
bounds?: {
x: number;
y: number;
width: number;
height: number;
originalWidth: number;
originalHeight: number;
coverage: number;
} | null;
}>;
model?: string;
provider?: string;
}>
> {
try {
const response = await fetch('/api/layer-split', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageUrl }),
});
const data = await response.json();
if (data.success && Array.isArray(data.layers)) {
return {
success: true,
data: {
layers: data.layers,
model: data.model,
provider: data.provider,
},
};
}
return { success: false, error: data.error || '图层拆分失败' };
} catch (error) {
logCatch(error, "split_image_layers");
return { success: false, error: '图层拆分失败' };
}
},
// 尺寸调整 - 通过 URL 下载图片,调整大小后上传到 S3
async resizeImage(imageUrl: string, width: number, height: number): Promise<ApiResponse<{ url: string }>> {
try {
// 1. 下载原图
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error('下载图片失败');
}
const blob = await response.blob();
// 2. 使用 canvas 调整尺寸
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('无法创建 canvas context');
}
const img = new Image();
img.crossOrigin = 'anonymous';
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error('图片加载失败'));
img.src = URL.createObjectURL(blob);
});
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// 3. 转换为 base64
const base64 = canvas.toDataURL('image/png').split(',')[1];
// 4. 上传到 S3
const uploadResponse = await fetch('/api/resize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_base64: base64,
canvasWidth: width,
canvasHeight: height,
prompt: '尺寸调整',
}),
});
const data = await uploadResponse.json();
if (data.success && data.original_url) {
return { success: true, data: { url: data.original_url } };
}
return { success: false, error: data.error || '尺寸调整失败' };
} catch (error) {
logCatch(error, "resize_image", { width, height });
return { success: false, error: '尺寸调整失败' };
}
},
// 角度编辑
async editAngle(params: {
imageUrl: string;
horizontalAngle: number;
verticalAngle: number;
zoom: number;
}): Promise<
ApiResponse<{
url: string;
thumbnailUrl?: string;
prompt?: string;
angles?: {
horizontal_angle?: number;
vertical_angle?: number;
zoom?: number;
};
}>
> {
try {
const response = await fetch('/api/angle-edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_url: params.imageUrl,
horizontal_angle: params.horizontalAngle,
vertical_angle: params.verticalAngle,
zoom: params.zoom,
}),
});
const data = await response.json();
if (data.success && data.image_url) {
return {
success: true,
data: {
url: data.image_url,
thumbnailUrl: data.thumbnail_url,
prompt: data.prompt,
angles: data.angles,
},
};
}
return { success: false, error: data.error || '角度编辑失败' };
} catch (error) {
return { success: false, error: '角度编辑失败' };
}
},
// 场景裂变
async sceneFusion(params: {
productImage: string;
backgroundImages: string[];
productName?: string;
}): Promise<ApiResponse<{ urls: string[] }>> {
try {
const response = await fetch('/api/scene-fusion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '场景裂变失败' };
}
},
// 识别图片中的文字(OCR)
async detectCanvasText(imageUrl: string): Promise<ApiResponse<{ items: CanvasTextDetectItem[]; model?: string }>> {
try {
const response = await fetch('/api/canvas/text-detect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageUrl }),
});
const data = await response.json();
return data;
} catch (error) {
logCatch(error, "detect_canvas_text");
return { success: false, error: '文字识别失败' };
}
},
// 生成文字改写建议
async generateCanvasTextSuggestions(params: {
imageUrl: string;
originalText: string;
currentText?: string;
title?: string;
pageTexts?: string[];
audience?: string;
usageScenario?: string;
coreNeed?: string;
productName?: string;
workflowSummary?: string;
workflowPrompt?: string;
userPrompt?: string;
presetName?: string;
}): Promise<ApiResponse<{ suggestions: CanvasTextSuggestionItem[]; model?: string }>> {
try {
const response = await fetch('/api/canvas/text-copy-suggestions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (error) {
logCatch(error, "text_suggestions");
return { success: false, error: '文案建议生成失败' };
}
},
// 应用文字替换并生成新图
async editCanvasText(params: {
imageUrl: string;
items: CanvasTextEditPayloadItem[];
}): Promise<ApiResponse<{ thumbnailUrl: string; originalUrl: string; model?: string }>> {
try {
const response = await fetch('/api/canvas/text-edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (error) {
logCatch(error, "edit_canvas_text", { itemCount: params.items.length });
return { success: false, error: '文字替换失败' };
}
},
};
// 视频生成服务
export const videoGenerationService = {
// 生成视频
async generateVideo(params: {
prompt: string;
mode?: VideoGenerationMode;
images?: string[];
audioUrls?: string[];
generateAudio?: boolean;
resolution?: VideoResolution;
ratio?: VideoRatio;
duration?: number;
watermark?: boolean;
cameraFixed?: boolean;
returnLastFrame?: boolean;
preferredModelRef?: string;
}): Promise<
ApiResponse<{
url: string;
metadata?: Record<string, any> | null;
modelDisplayName?: string;
modelId?: string;
modelRef?: string;
mode?: VideoGenerationMode;
images?: string[];
}>
> {
try {
const response = await fetch('/api/canvas/generate/video', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '生成视频失败' };
}
},
};
export const workflowAnalysisService = {
async analyze(params: {
mode: WorkflowAnalysisMode;
prompt?: string;
imageUrls?: string[];
videoUrls?: string[];
nodeTitle?: string;
preferredModelRef?: string;
}): Promise<
ApiResponse<{
analysisRows?: Array<{
shot?: string | number;
startTime?: string | number;
endTime?: string | number;
duration?: string | number;
sceneDescription?: string;
narrative?: string;
story?: string;
shotSize?: string;
cameraAngle?: string;
cameraMovement?: string;
focusDepth?: string;
lighting?: string;
backgroundMusic?: string;
voiceAndSfx?: string;
imagePrompt?: string;
motionPrompt?: string;
keyframeUrl?: string;
}>;
framePreviews?: string[];
summary?: string;
highlights?: string[];
shotIdeas?: Array<{ title: string; description: string }>;
visualMotifs?: string[];
recommendedNextSteps?: string[];
characters?: Array<Record<string, any>>;
scenes?: Array<Record<string, any>>;
relationships?: string[];
agentPlan?: string[];
deliverables?: string[];
recommendedNodes?: string[];
model?: string | null;
raw?: string;
}>
> {
try {
const response = await fetch('/api/canvas/workflow/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '工作流分析失败' };
}
},
};
export type Model3DTaskStatus =
| 'queued'
| 'running'
| 'success'
| 'failed'
| 'banned'
| 'expired'
| 'cancelled'
| 'unknown';
export const model3DGenerationService = {
async uploadImage(
file: File,
options?: { providerId?: string | null },
): Promise<ApiResponse<{ imageToken?: string; file?: Record<string, any>; providerId?: string | null; providerName?: string | null }>> {
try {
const formData = new FormData();
formData.append('file', file);
if (options?.providerId) {
formData.append('providerId', options.providerId);
}
const response = await fetch('/api/canvas/3d/upload-image', {
method: 'POST',
body: formData,
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '上传参考图失败' };
}
},
async createTask(params: Record<string, any>): Promise<ApiResponse<{ taskId: string; status: Model3DTaskStatus; providerId?: string | null; providerName?: string | null; raw?: any }>> {
try {
const response = await fetch('/api/canvas/3d/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '创建 3D 任务失败' };
}
},
async getTask(taskId: string, options?: { providerId?: string | null }): Promise<ApiResponse<{
taskId: string;
type: string;
status: Model3DTaskStatus;
progress?: number | null;
output?: Record<string, any>;
modelUrl?: string | null;
previewImageUrl?: string | null;
assetPayload?: Record<string, any> | null;
errorCode?: number | null;
errorMessage?: string | null;
refundedPoints?: number | null;
}>> {
try {
const params = new URLSearchParams();
if (options?.providerId) {
params.set('providerId', options.providerId);
}
const query = params.toString();
const response = await fetch(
`/api/canvas/3d/tasks/${taskId}${query ? `?${query}` : ''}`
);
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '查询 3D 任务失败' };
}
},
async getBalance(): Promise<ApiResponse<Record<string, any>>> {
try {
const response = await fetch('/api/canvas/3d/balance');
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '查询余额失败' };
}
},
};
export type WorldLabsTaskMode = 'text' | 'image' | 'multi-image' | 'video';
export type WorldLabsTaskAssetPayload = {
providerId?: string | null;
providerName?: string | null;
worldId?: string | null;
worldUrl?: string | null;
displayName?: string | null;
model?: string | null;
caption?: string | null;
thumbnailUrl?: string | null;
panoUrl?: string | null;
meshUrl?: string | null;
spzUrls?: Record<string, string> | null;
semanticsMetadata?: Record<string, any> | null;
storageArchivedAt?: string | null;
sourceModelUrl?: string | null;
sourcePreviewImageUrl?: string | null;
sourceWorldUrl?: string | null;
sourceMeshUrl?: string | null;
sourceThumbnailUrl?: string | null;
sourcePanoUrl?: string | null;
assetsPending?: boolean | null;
};
export const worldLabsGenerationService = {
async uploadMedia(
file: File,
kind: 'image' | 'video',
options?: { providerId?: string | null }
): Promise<
ApiResponse<{
mediaAssetId?: string;
sourceUrl?: string;
mediaAsset?: Record<string, any>;
uploadInfo?: Record<string, any>;
providerId?: string | null;
providerName?: string | null;
uploadFallback?: string | null;
}>
> {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('kind', kind);
if (options?.providerId) {
formData.append('providerId', options.providerId);
}
const response = await fetch('/api/canvas/world/upload', {
method: 'POST',
body: formData,
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '上传 World Labs 媒体失败' };
}
},
async uploadMediaFromUrl(
sourceUrl: string,
kind: 'image' | 'video',
options?: { providerId?: string | null }
): Promise<
ApiResponse<{
mediaAssetId?: string;
sourceUrl?: string;
mediaAsset?: Record<string, any>;
uploadInfo?: Record<string, any>;
providerId?: string | null;
providerName?: string | null;
uploadFallback?: string | null;
}>
> {
try {
const formData = new FormData();
formData.append('sourceUrl', sourceUrl);
formData.append('kind', kind);
if (options?.providerId) {
formData.append('providerId', options.providerId);
}
const response = await fetch('/api/canvas/world/upload', {
method: 'POST',
body: formData,
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '上传 World Labs 媒体失败' };
}
},
async createTask(params: Record<string, any>): Promise<
ApiResponse<{
taskId: string;
status: Model3DTaskStatus;
raw?: any;
assetPayload?: WorldLabsTaskAssetPayload | null;
}>
> {
try {
const response = await fetch('/api/canvas/world/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '创建 World Labs 任务失败' };
}
},
async getTask(
taskId: string,
options?: { providerId?: string | null }
): Promise<ApiResponse<{
taskId: string;
type: string;
status: Model3DTaskStatus;
progress?: number | null;
output?: Record<string, any>;
modelUrl?: string | null;
previewImageUrl?: string | null;
worldId?: string | null;
worldUrl?: string | null;
panoUrl?: string | null;
caption?: string | null;
assetPayload?: WorldLabsTaskAssetPayload | null;
errorMessage?: string | null;
}>> {
try {
const url = new URL(`/api/canvas/world/tasks/${taskId}`, window.location.origin);
if (options?.providerId) {
url.searchParams.set('providerId', options.providerId);
}
const response = await fetch(url.toString());
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '查询 World Labs 任务失败' };
}
},
};
export default {
canvas: canvasService,
image: imageGenerationService,
video: videoGenerationService,
model3d: model3DGenerationService,
world: worldLabsGenerationService,
};