Your Name be5b9aef12
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
chore: sync latest workspace changes
2026-04-05 02:08:26 +08:00

904 lines
26 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 {
CanvasProject,
CanvasState,
CanvasNode,
CanvasWorkspaceType,
} from '@/types/canvas';
import type { VideoGenerationMode, VideoRatio, VideoResolution } from '@/types/ai';
import { requestLongTaskResult } from '@/lib/client/long-task';
import type {
EnhanceProKreaModel,
EnhanceProOutputFormat,
EnhanceProSubjectDetection,
} from '@/lib/enhance-pro/config';
// API 响应类型
interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: string;
}
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;
action?: 'replace' | 'delete';
}
export type WorkflowAnalysisMode =
| "video-analysis"
| "character-scene-extraction"
| "agent-plan";
export interface EnhanceProImageResult {
originalUrl: string;
thumbnailUrl: string;
width?: number;
height?: number;
}
export interface EnhanceProRequestParams {
imageUrl: string;
prompt?: string;
chatId?: string | null;
messageId?: 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'
): Promise<ApiResponse<CanvasProject[]>> {
try {
const query =
workspaceType && workspaceType !== 'canvas'
? `?workspaceType=${encodeURIComponent(workspaceType)}`
: '';
const response = await fetch(`/api/canvas${query}`);
const data = await response.json();
return data;
} catch (error) {
return { success: false, error: '获取画布列表失败' };
}
},
// 获取单个画布
async getProject(id: string): Promise<ApiResponse<CanvasProject>> {
try {
const response = await fetch(`/api/canvas/${id}`);
const data = await response.json();
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();
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
): Promise<ApiResponse> {
try {
const requestBody = JSON.stringify(
workspaceType ? { canvasData, workspaceType } : { canvasData }
);
const doSave = async () =>
fetch(`/api/canvas/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: requestBody,
});
let response = await doSave();
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();
}
const data = await response.json().catch(() => ({}));
if (!response.ok) {
return { success: false, error: String(data?.error || data?.message || '保存画布状态失败') };
}
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);
const response = await fetch('/api/files/upload', {
method: 'POST',
body: 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) {
console.error('[Canvas] 图片上传失败:', error);
return { success: false, error: '图片上传失败' };
}
},
// AI 生成图片
async generateImage(params: {
prompt: string;
negativePrompt?: string;
model?: string;
aspectRatio?: string;
imageCount?: number;
referenceImage?: string;
}): Promise<ApiResponse<{ images: Array<{ thumbnailUrl: string; originalUrl: string }>; model?: string; prompt?: 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();
return data;
} catch (error) {
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) {
console.error('[Canvas] 抠图失败:', error);
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) {
console.error('[Canvas] 框选抠图失败:', error);
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) {
console.error('[Canvas] 去水印失败:', error);
return { success: false, error: '去水印失败' };
}
},
// 图片增强 - 使用图片URL
async enhanceImage(imageUrl: string): Promise<ApiResponse<{ url: string }>> {
try {
const response = await fetch('/api/enhance', {
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) {
console.error('[Canvas] 图片增强失败:', error);
return { success: false, error: '图片增强失败' };
}
},
async enhanceImagePro(
params: EnhanceProRequestParams,
): Promise<ApiResponse<{ images: EnhanceProImageResult[] }>> {
try {
const payload = await requestLongTaskResult({
endpoint: '/api/enhance/pro',
payload: params,
pollingParams: {
prompt: params.prompt || '图像增强 Pro',
chatId: params.chatId,
messageId: params.messageId,
sourceImageUrl: params.imageUrl,
width: params.width,
height: params.height,
},
});
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) {
console.error('[Canvas] 图像增强 Pro 失败:', 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 || '图像拓展处理',
...(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 || '图片扩展失败' };
} catch (error) {
console.error('[Canvas] 图片扩展失败:', error);
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) {
console.error('[Canvas] 图层拆分失败:', error);
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) {
console.error('[Canvas] 尺寸调整失败:', error);
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) {
console.error('[Canvas] 文字识别失败:', error);
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) {
console.error('[Canvas] 文字替换失败:', error);
return { success: false, error: '文字替换失败' };
}
},
};
// 视频生成服务
export const videoGenerationService = {
// 生成视频
async generateVideo(params: {
prompt: string;
mode?: VideoGenerationMode;
images?: string[];
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;
}): Promise<
ApiResponse<{
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): Promise<ApiResponse<{ imageToken?: string; file?: Record<string, any> }>> {
try {
const formData = new FormData();
formData.append('file', file);
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; 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): Promise<ApiResponse<{
taskId: string;
type: string;
status: Model3DTaskStatus;
progress?: number | null;
output?: Record<string, any>;
modelUrl?: string | null;
previewImageUrl?: string | null;
errorCode?: number | null;
errorMessage?: string | null;
}>> {
try {
const response = await fetch(`/api/canvas/3d/tasks/${taskId}`);
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 = {
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;
};
export const worldLabsGenerationService = {
async uploadMedia(
file: File,
kind: 'image' | 'video'
): Promise<ApiResponse<{ mediaAssetId?: string; mediaAsset?: Record<string, any>; uploadInfo?: Record<string, any> }>> {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('kind', kind);
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 }>> {
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): 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 response = await fetch(`/api/canvas/world/tasks/${taskId}`);
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,
};