From c4c0bd8f64dabfed2746fd233e486d1ed164e5d2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 8 May 2026 08:48:07 +0800 Subject: [PATCH] fix: streamline extend image payload flow --- app/api/extend/route.ts | 74 ++++-- .../agents/AgentPsdTemplateEditorDialog.tsx | 1 + components/chat.tsx | 232 ++++++++++-------- components/image-annotator.tsx | 5 +- lib/ai/extend/service.ts | 15 +- services/canvas/index.ts | 3 +- 6 files changed, 211 insertions(+), 119 deletions(-) diff --git a/app/api/extend/route.ts b/app/api/extend/route.ts index 369584639..2e44f06b1 100644 --- a/app/api/extend/route.ts +++ b/app/api/extend/route.ts @@ -12,6 +12,22 @@ import { export const maxDuration = 180; +function parseIncludeBase64(value: unknown): boolean { + if (typeof value === "boolean") return value; + const normalized = String(value ?? "").trim().toLowerCase(); + return normalized === "true" || normalized === "1" || normalized === "yes"; +} + +function buildExtendSuccessPayload(result: Awaited>) { + return { + success: true, + original_url: result.original_url, + thumbnail_url: result.thumbnail_url, + ...(result.image_base64 ? { base64: result.image_base64 } : {}), + processing_time: result.processing_time, + }; +} + export async function POST(request: NextRequest) { try { const session = await auth(); @@ -26,6 +42,8 @@ export async function POST(request: NextRequest) { let prompt = "图像拓展处理"; let chatId: string | null = null; let messageId: string | null = null; + let includeBase64 = false; + const requestId = `extend-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; if (contentType.includes("application/json")) { const body = await request.json(); @@ -34,6 +52,7 @@ export async function POST(request: NextRequest) { prompt = body.prompt || prompt; chatId = body.chatId || null; messageId = body.messageId || null; + includeBase64 = parseIncludeBase64(body.includeBase64); if (!imageUrl && !imageBase64) { return NextResponse.json( @@ -42,6 +61,16 @@ export async function POST(request: NextRequest) { ); } + console.log("[Extend API] Request received:", { + requestId, + source: imageUrl ? "url" : "base64", + includeBase64, + chatId, + messageId, + xScale: body.xScale, + yScale: body.yScale, + }); + const pointsConfig = await getPointsSystemConfig(); const pointsCharge = await prepareFeaturePointsCharge({ userId: session.user.id, @@ -64,6 +93,8 @@ export async function POST(request: NextRequest) { messageId, xScale: body.xScale ? Number(body.xScale) : undefined, yScale: body.yScale ? Number(body.yScale) : undefined, + includeBase64, + requestId, }); if (pointsCharge.chargeable) { @@ -74,13 +105,14 @@ export async function POST(request: NextRequest) { } } - return NextResponse.json({ - success: true, - original_url: result.original_url, - thumbnail_url: result.thumbnail_url, - base64: result.image_base64, - processing_time: result.processing_time, + console.log("[Extend API] Request completed:", { + requestId, + includeBase64, + processingTime: result.processing_time, + originalUrl: result.original_url.slice(0, 120), }); + + return NextResponse.json(buildExtendSuccessPayload(result)); } if (contentType.includes("multipart/form-data")) { @@ -92,6 +124,17 @@ export async function POST(request: NextRequest) { const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); + const multipartChatId = String(formData.get("chatId") || "") || null; + const multipartMessageId = String(formData.get("messageId") || "") || null; + includeBase64 = parseIncludeBase64(formData.get("includeBase64")); + console.log("[Extend API] Multipart request received:", { + requestId, + includeBase64, + chatId: multipartChatId, + messageId: multipartMessageId, + bytes: buffer.length, + }); + const pointsConfig = await getPointsSystemConfig(); const pointsCharge = await prepareFeaturePointsCharge({ userId: session.user.id, @@ -109,8 +152,10 @@ export async function POST(request: NextRequest) { userId: session.user.id, imageBase64: buffer.toString("base64"), prompt: String(formData.get("prompt") || prompt), - chatId: String(formData.get("chatId") || "") || null, - messageId: String(formData.get("messageId") || "") || null, + chatId: multipartChatId, + messageId: multipartMessageId, + includeBase64, + requestId, }); if (pointsCharge.chargeable) { @@ -121,13 +166,14 @@ export async function POST(request: NextRequest) { } } - return NextResponse.json({ - success: true, - original_url: result.original_url, - thumbnail_url: result.thumbnail_url, - base64: result.image_base64, - processing_time: result.processing_time, + console.log("[Extend API] Multipart request completed:", { + requestId, + includeBase64, + processingTime: result.processing_time, + originalUrl: result.original_url.slice(0, 120), }); + + return NextResponse.json(buildExtendSuccessPayload(result)); } return NextResponse.json( diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index 4410f8366..100cdaf07 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -2583,6 +2583,7 @@ export function AgentPsdTemplateEditorDialog({ body: JSON.stringify({ image_url: normalizedImageUrl, prompt: "PSD 套版图片拓展处理", + includeBase64: false, }), }); const payload = await parseActionJsonResponse<{ diff --git a/components/chat.tsx b/components/chat.tsx index 559e64546..ca4fb9d61 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -49,6 +49,23 @@ import type { VideoToolCapabilityContract } from "@/types/video-tool-capability" type ImageGenerationPhase = "idle" | "queued" | "processing" | "finalizing" | "failed"; +function getImageFileExtension(mimeType: string | null | undefined): string { + switch (String(mimeType || "").toLowerCase()) { + case "image/jpeg": + case "image/jpg": + return "jpg"; + case "image/webp": + return "webp"; + case "image/gif": + return "gif"; + case "image/svg+xml": + return "svg"; + case "image/png": + default: + return "png"; + } +} + function normalizeImageParamsForProvider( params: ImageGenerationParams, providerType?: string @@ -1821,122 +1838,139 @@ export function Chat({ }, 100); try { - // 获取图片并转换为Base64 - const response = await fetch(imageUrl); - const blob = await response.blob(); - const reader = new FileReader(); - - reader.onloadend = async () => { + let extendSourceUrl = String(imageUrl || "").trim(); + if (!extendSourceUrl) { + throw new Error("缺少可拓展的图片地址"); + } + + if (extendSourceUrl.startsWith("data:") || extendSourceUrl.startsWith("blob:")) { + const sourceResponse = await fetch(extendSourceUrl); + if (!sourceResponse.ok) { + throw new Error("读取待拓展图片失败"); + } + const blob = await sourceResponse.blob(); + const mimeType = + blob.type || sourceResponse.headers.get("content-type") || "image/png"; + const file = new File( + [blob], + `extend-source-${Date.now()}.${getImageFileExtension(mimeType)}`, + { type: mimeType }, + ); + const formData = new FormData(); + formData.append("file", file); + + const uploadResponse = await fetchUploadWithRetry(formData); + const uploadPayload = await uploadResponse + .json() + .catch(() => null as { url?: string; error?: string } | null); + if (!uploadResponse.ok) { + throw new Error(uploadPayload?.error || "上传待拓展图片失败"); + } + + const uploadedUrl = String(uploadPayload?.url || "").trim(); + if (!uploadedUrl) { + throw new Error("上传待拓展图片成功,但未返回图片地址"); + } + extendSourceUrl = uploadedUrl; + } else { + extendSourceUrl = new URL(extendSourceUrl, window.location.origin).toString(); + } + + const extendResponse = await fetch("/api/extend", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + image_url: extendSourceUrl, + prompt: "图片图像拓展处理", + chatId: id, + messageId, + includeBase64: false, + }), + }); + + const result = await extendResponse.json().catch( + () => + null as { + success?: boolean; + error?: string; + original_url?: string; + thumbnail_url?: string; + processing_time?: number; + } | null, + ); + + if (!extendResponse.ok) { + throw new Error(result?.error || "图像拓展处理失败"); + } + + if (result?.success && result.original_url) { + // 构造图片Markdown(使用与Gemini生成相同的格式) + const imageMarkdown = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`; + + // 创建最终消息(前端显示用) + const finalMessage: ChatMessage = { + id: messageId, + role: "assistant", + parts: [{ type: "text", text: imageMarkdown }], + metadata: { + createdAt: new Date().toISOString(), + }, + } as ChatMessage; + + // 更新消息 + setMessages((prev) => + prev.map((msg) => (msg.id === messageId ? finalMessage : msg)), + ); + + // 滚动到底部,显示处理结果 + setTimeout(() => { + const messagesContainer = document.querySelector('[role="log"]'); + if (messagesContainer) { + messagesContainer.scrollTo({ + top: messagesContainer.scrollHeight, + behavior: "smooth", + }); + } + }, 100); + + // 保存消息到数据库 try { - const base64Data = (reader.result as string).split(",")[1]; - - // 调用图像拓展API - const extendResponse = await fetch("/api/extend", { + await fetch("/api/messages", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - image_base64: base64Data, - prompt: "图片图像拓展处理", - }), - }); - - if (!extendResponse.ok) { - const error = await extendResponse.json(); - throw new Error(error.error || "图像拓展处理失败"); - } - - const result = await extendResponse.json(); - - if (result.success && result.original_url) { - // 构造图片Markdown(使用与Gemini生成相同的格式) - const imageMarkdown = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`; - - // 创建最终消息(前端显示用) - const finalMessage: ChatMessage = { - id: messageId, - role: "assistant", - parts: [{ type: "text", text: imageMarkdown }], - metadata: { + chatId: id, + message: { + id: messageId, + role: "assistant", + parts: [{ type: "text", text: imageMarkdown }], + attachments: [], createdAt: new Date().toISOString(), }, - } as ChatMessage; - - // 更新消息 - setMessages((prev) => - prev.map((msg) => (msg.id === messageId ? finalMessage : msg)) - ); - - // 滚动到底部,显示处理结果 - setTimeout(() => { - const messagesContainer = document.querySelector('[role="log"]'); - if (messagesContainer) { - messagesContainer.scrollTo({ - top: messagesContainer.scrollHeight, - behavior: "smooth", - }); - } - }, 100); - - // 保存消息到数据库 - try { - await fetch("/api/messages", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - chatId: id, - message: { - id: messageId, - role: "assistant", - parts: [{ type: "text", text: imageMarkdown }], - attachments: [], - createdAt: new Date().toISOString(), - }, - }), - }); - } catch (saveError) { - console.error("[Chat] Failed to save message:", saveError); - } - - toast.success(`图像拓展完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`); - } else { - throw new Error(result.error || "图像拓展处理失败"); - } - } catch (error) { - console.error("[图像拓展] 错误:", error); - toast.error( - getUserFacingRequestErrorMessage(error, "图像拓展失败,请稍后重试", { - rateLimitMessage: getFriendlyRateLimitMessage("当前图像拓展请求过于频繁,请稍后再试"), - }) - ); - // 移除加载消息 - setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); - } finally { - console.log("[图像拓展] finally 块执行,重置 isProcessingExtend"); - setIsProcessingExtend(false); + }), + }); + } catch (saveError) { + console.error("[Chat] Failed to save message:", saveError); } - }; - - reader.onerror = () => { - console.log("[图像拓展] reader.onerror 执行,重置 isProcessingExtend"); - toast.error("图片读取失败"); - setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); - setIsProcessingExtend(false); - }; - - reader.readAsDataURL(blob); + + toast.success(`图像拓展完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`); + } else { + throw new Error(result?.error || "图像拓展处理失败"); + } } catch (error) { console.error("[图像拓展] 错误:", error); - console.log("[图像拓展] 外层 catch 执行,重置 isProcessingExtend"); toast.error( getUserFacingRequestErrorMessage(error, "图像拓展失败,请稍后重试", { rateLimitMessage: getFriendlyRateLimitMessage("当前图像拓展请求过于频繁,请稍后再试"), }) ); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); + } finally { + console.log("[图像拓展] finally 块执行,重置 isProcessingExtend"); setIsProcessingExtend(false); } }; diff --git a/components/image-annotator.tsx b/components/image-annotator.tsx index c2231cf82..ebc7c0fad 100644 --- a/components/image-annotator.tsx +++ b/components/image-annotator.tsx @@ -1205,6 +1205,7 @@ export function ImageAnnotator({ body: JSON.stringify({ image_url: originalImageUrl, // 直接发送图片URL prompt: "画图标注-图像拓展处理", // 可选:添加提示词 + includeBase64: false, }), }); @@ -1228,9 +1229,7 @@ export function ImageAnnotator({ setProcessedImageUrl(null); } // 优先使用返回的base64,如果没有则使用thumbnail_url或original_url - if (result.base64) { - setProcessedImageBase64(result.base64); - } + setProcessedImageBase64(result.base64 || null); setProcessType("extend"); toast.success(`图像拓展完成!图片已保存到相册。处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`); diff --git a/lib/ai/extend/service.ts b/lib/ai/extend/service.ts index c5b45b702..bfa94370f 100644 --- a/lib/ai/extend/service.ts +++ b/lib/ai/extend/service.ts @@ -23,12 +23,14 @@ export interface ExecuteExtendJobParams { messageId?: string | null; xScale?: number; yScale?: number; + includeBase64?: boolean; skipGallerySave?: boolean; sourceImageUrl?: string | null; + requestId?: string | null; } export interface ExecuteExtendJobResult { - image_base64: string; + image_base64?: string; thumbnail_url: string; original_url: string; processing_time: number; @@ -221,8 +223,10 @@ export async function executeExtendJob( prompt = "图像拓展处理", chatId = null, messageId = null, + includeBase64 = false, skipGallerySave = false, sourceImageUrl = imageUrl || null, + requestId = null, } = params; if (!imageUrl && !imageBase64) { @@ -250,8 +254,15 @@ export async function executeExtendJob( } const buffer = Buffer.from(await imageResponse.arrayBuffer()); - const imageBase64Result = buffer.toString("base64"); + const imageBase64Result = includeBase64 ? buffer.toString("base64") : undefined; const uploadResult = await uploadBufferPairToS3(buffer); + console.log("[Extend Service] Result uploaded:", { + requestId, + includeBase64, + inputImageUrl: inputImageUrl.slice(0, 120), + extendedImageUrl: extendedImageUrl.slice(0, 120), + outputBytes: buffer.length, + }); let mediaAssetId: string | null = null; let generatedImageId: string | null = null; diff --git a/services/canvas/index.ts b/services/canvas/index.ts index f1a3e8c74..154ddb392 100644 --- a/services/canvas/index.ts +++ b/services/canvas/index.ts @@ -487,6 +487,7 @@ export const imageGenerationService = { body: JSON.stringify({ image_url: imageUrl, prompt: normalizedOptions.prompt || '图像拓展处理', + includeBase64: false, ...(typeof normalizedOptions.xScale === 'number' ? { xScale: normalizedOptions.xScale } : {}), @@ -499,7 +500,7 @@ export const imageGenerationService = { if (data.success && data.original_url) { return { success: true, data: { url: data.original_url } }; } - return { success: false, error: data.error || '图片扩展失败' }; + return { success: false, error: data.error || data.detail || '图片扩展失败' }; } catch (error) { console.error('[Canvas] 图片扩展失败:', error); return { success: false, error: '图片扩展失败' };