From 057014900c084ef6a9780a3e889c8c77a7528ef6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 1 Feb 2026 12:18:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=94=BB=E5=B8=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=8C=89=E9=92=AE=E4=B8=8E=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=20API=20=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 CanvasChatPanel 中的图片上传 - 使用 parts 数组而非 attachments 2. 扩展 chat schema 支持更多图片格式(gif, bmp, svg) 3. 实现尺寸调整功能 - 添加对话框选择尺寸 4. 更新 imageGenerationService.resizeImage 使用 canvas API 调整尺寸 5. 所有图片编辑功能已对接:抠图、变清晰、扩展、角度编辑、场景裂变、尺寸调整 Co-authored-by: Cursor --- app/(chat)/api/chat/schema.ts | 2 +- components/canvas/CanvasChatPanel.tsx | 30 +++++-- components/canvas/nodes/ImageNode.tsx | 124 +++++++++++++++++++++++++- services/canvas/index.ts | 51 +++++++++-- 4 files changed, 193 insertions(+), 14 deletions(-) diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts index e69649e..5969042 100644 --- a/app/(chat)/api/chat/schema.ts +++ b/app/(chat)/api/chat/schema.ts @@ -7,7 +7,7 @@ const textPartSchema = z.object({ const filePartSchema = z.object({ type: z.enum(["file"]), - mediaType: z.enum(["image/jpeg", "image/png", "image/webp"]), + mediaType: z.enum(["image/jpeg", "image/png", "image/webp", "image/gif", "image/jpg", "image/bmp", "image/svg+xml"]), name: z.string().min(1).max(100), url: z.string().url(), }); diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 2286956..16cb278 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -252,16 +252,32 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF return; } - const messageAttachments = attachments.map(a => ({ - contentType: a.contentType, - url: a.url, - name: a.name, - })); + // 构建 parts 数组:文本 + 文件 + const parts: any[] = []; + + // 添加文本部分 + if (input.trim()) { + parts.push({ type: "text", text: input }); + } + + // 添加文件部分(作为 file 类型的 part) + attachments.forEach(a => { + parts.push({ + type: "file", + mediaType: a.contentType, + name: a.name, + url: a.url, + }); + }); + + // 如果没有文本但有附件,添加一个空的文本提示 + if (!input.trim() && attachments.length > 0) { + parts.unshift({ type: "text", text: "请处理这张图片" }); + } sendMessage({ role: "user" as const, - parts: [{ type: "text", text: input }], - attachments: messageAttachments, + parts: parts, }); setInput(""); diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 3abcbae..085bb9a 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -10,6 +10,15 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { toast } from "sonner"; import { useCanvasStore } from "@/store/canvas"; import { imageGenerationService } from "@/services/canvas"; @@ -29,6 +38,9 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const [isProcessing, setIsProcessing] = useState(false); const [angleDialogOpen, setAngleDialogOpen] = useState(false); const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false); + const [resizeDialogOpen, setResizeDialogOpen] = useState(false); + const [resizeWidth, setResizeWidth] = useState(1024); + const [resizeHeight, setResizeHeight] = useState(1024); const [isUploading, setIsUploading] = useState(false); // 文件输入的 ref @@ -131,7 +143,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { return; } if (action === 'resize') { - toast.info("尺寸调整功能即将上线"); + setResizeDialogOpen(true); return; } @@ -237,6 +249,67 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { }); }; + // 尺寸调整执行 + const handleResize = async () => { + if (!data.src) return; + + setResizeDialogOpen(false); + + // 创建处理中节点 + const newNodeId = addNode({ + type: 'image', + position: { + x: node.position.x + node.size.width + 20, + y: node.position.y + }, + size: { width: resizeWidth / 3, height: resizeHeight / 3 }, // 按比例显示 + data: { + src: '', + status: 'generating', + prompt: `尺寸调整 ${resizeWidth}x${resizeHeight}`, + originalSrc: data.src, + }, + connections: [node.id], + }); + + try { + const result = await imageGenerationService.resizeImage(data.src, resizeWidth, resizeHeight); + + if (result?.success && result.data?.url) { + updateNode(newNodeId, { + data: { + src: result.data.url, + status: 'completed', + prompt: `尺寸调整 ${resizeWidth}x${resizeHeight}`, + originalSrc: data.src, + }, + }); + toast.success('尺寸调整完成'); + } else { + updateNode(newNodeId, { + data: { + src: '', + status: 'failed', + prompt: '尺寸调整失败', + originalSrc: data.src, + }, + }); + toast.error(result?.error || '尺寸调整失败'); + } + } catch (error) { + console.error('尺寸调整失败:', error); + updateNode(newNodeId, { + data: { + src: '', + status: 'failed', + prompt: '尺寸调整失败', + originalSrc: data.src, + }, + }); + toast.error('尺寸调整失败'); + } + }; + // 下载图片 const handleDownload = () => { if (data.src) { @@ -470,6 +543,55 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { open={sceneFusionDialogOpen} onOpenChange={setSceneFusionDialogOpen} /> + + {/* 尺寸调整对话框 */} + + + + + + 尺寸调整 + + +
+
+
+ + setResizeWidth(parseInt(e.target.value) || 1024)} + min={100} + max={4096} + /> +
+
+ + setResizeHeight(parseInt(e.target.value) || 1024)} + min={100} + max={4096} + /> +
+
+
+ + + + + +
+
+ + + + +
+
); } diff --git a/services/canvas/index.ts b/services/canvas/index.ts index dcf882f..511d1e6 100644 --- a/services/canvas/index.ts +++ b/services/canvas/index.ts @@ -217,17 +217,58 @@ export const imageGenerationService = { } }, - // 尺寸调整 + // 尺寸调整 - 通过 URL 下载图片,调整大小后上传到 S3 async resizeImage(imageUrl: string, width: number, height: number): Promise> { try { - const response = await fetch('/api/resize', { + // 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({ imageUrl, width, height }), + body: JSON.stringify({ + image_base64: base64, + canvasWidth: width, + canvasHeight: height, + prompt: '尺寸调整', + }), }); - const data = await response.json(); - return data; + + 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: '尺寸调整失败' }; } },