// 画布服务层 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 ): 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 { success: boolean; data?: T; error?: string; status?: number; code?: string; canvasUpdatedAt?: string; pagination?: { page: number; limit: number; hasMore: boolean; }; } 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> { 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> { 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, workspaceType: CanvasWorkspaceType = 'canvas' ): Promise> { 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, workspaceType?: CanvasWorkspaceType ): Promise> { 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 { 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 { 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; }; const response = await doSaveWithRateLimitRetry(buildRequestBody(options)); const 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> { 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: { 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 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> { 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> { 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> { 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> { 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> { 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> { 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((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> { 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> { 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> { 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> { 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[]; resolution?: VideoResolution; ratio?: VideoRatio; duration?: number; watermark?: boolean; cameraFixed?: boolean; returnLastFrame?: boolean; preferredModelRef?: string; }): Promise< ApiResponse<{ url: string; metadata?: Record | 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>; scenes?: Array>; 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; 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): Promise> { 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; modelUrl?: string | null; previewImageUrl?: string | null; assetPayload?: Record | 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>> { 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 | null; semanticsMetadata?: Record | 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; uploadInfo?: Record; 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; uploadInfo?: Record; 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): 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; 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, };