/** * 场景裂变 API * * 将一张产品图批量融合到多个场景图中 * 支持多种生图模型:阿里生图、RUXA、Gemini、既梦 */ import { NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { generatedImage } from "@/lib/db/schema"; import { getEnabledAiModelsWithProvider, getSystemSetting } from "@/lib/db/queries"; import { uploadFile } from "@/lib/storage/service"; import sharp from "sharp"; import { createAlibabaImage } from "@/lib/ai/providers/alibaba-image"; import { createGeminiImage } from "@/lib/ai/providers/gemini-image"; import { createVolcanoVision } from "@/lib/ai/providers/volcano-vision"; import { createRuxaImage } from "@/lib/ai/providers/ruxa-image"; // 请求体 Schema const sceneFusionRequestSchema = z.object({ productImage: z.string().min(1, "产品图不能为空"), productName: z.string().optional(), // 产品名称(如"沙发") aspectRatio: z.string().optional(), // 图片比例(如"1:1", "4:3", "auto") imageSize: z.string().optional(), // 图片分辨率(如"1K", "2K", "4K") scenes: z.array(z.object({ id: z.string(), image: z.string().min(1, "场景图不能为空"), targetName: z.string().optional(), // 背景中需要被替换的物体名称(如"椅子") customPrompt: z.string().optional(), })).min(1, "至少需要一个场景图").max(20, "最多支持20个场景图"), }); // 默认提示词(当数据库未配置时使用)- 使用"参考"的表述方式 const FALLBACK_PROMPT = "请参考第一张图片中的产品,将其自然地融合到第二张图片的场景中。要求:1)保持产品的整体外观、颜色、形状和设计风格与第一张图一致;2)调整产品的大小和角度以适应场景;3)注意光影和透视关系使融合自然。"; /** * 获取场景裂变配置 */ async function getSceneFusionConfig() { try { const setting = await getSystemSetting({ key: "scene_fusion_config" }); if (setting?.value) { const config = setting.value as { enabled?: boolean; defaultPrompt?: string; maxScenes?: number; modelId?: string; }; return { enabled: config.enabled ?? true, defaultPrompt: config.defaultPrompt || FALLBACK_PROMPT, maxScenes: config.maxScenes ?? 10, modelId: config.modelId || "", }; } } catch (error) { console.error("[SceneFusion] Failed to load config:", error); } return { enabled: true, defaultPrompt: FALLBACK_PROMPT, maxScenes: 10, modelId: "", }; } /** * 创建缩略图 */ async function createThumbnail(buffer: Buffer): Promise { try { const thumbnailBuffer = await sharp(buffer) .resize(400, 400, { fit: "inside", withoutEnlargement: true }) .jpeg({ quality: 80 }) .toBuffer(); return thumbnailBuffer; } catch (error) { console.error("[SceneFusion] Failed to create thumbnail:", error); return buffer; } } /** * 生成唯一文件名 */ function generateFilename(prefix: string, extension: string): string { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `${prefix}-${timestamp}-${random}.${extension}`; } /** * 将 Base64 图片上传到 S3 获取 URL */ async function uploadBase64ToS3(base64Data: string, prefix: string = "scene-fusion"): Promise { try { // 移除 data URL 前缀 const base64Clean = base64Data.replace(/^data:image\/\w+;base64,/, ""); const buffer = Buffer.from(base64Clean, "base64"); const filename = generateFilename(`${prefix}-ref`, "png"); const result = await uploadFile(filename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)); console.log(`[SceneFusion] Uploaded reference image to: ${result.url}`); return result.url; } catch (error) { console.error("[SceneFusion] Failed to upload reference image:", error); throw error; } } /** * 下载图片并上传到 S3 */ async function downloadAndUploadToS3( imageUrl: string, prefix: string = "scene-fusion" ): Promise<{ thumbnail: string; original: string }> { try { console.log("[SceneFusion] Downloading image from:", imageUrl.substring(0, 100) + "..."); const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Failed to download image: ${response.status}`); } const buffer = Buffer.from(await response.arrayBuffer()); console.log(`[SceneFusion] Downloaded image, size: ${buffer.length} bytes`); // 创建缩略图 const thumbnailBuffer = await createThumbnail(buffer); // 上传原图 const originalFilename = generateFilename(`${prefix}-orig`, "png"); const originalResult = await uploadFile(originalFilename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)); console.log(`[SceneFusion] Uploaded original to: ${originalResult.url}`); // 上传缩略图 const thumbnailFilename = generateFilename(`${prefix}-thumb`, "jpg"); const thumbnailArrayBuffer = new Uint8Array(thumbnailBuffer).buffer; const thumbnailResult = await uploadFile(thumbnailFilename, thumbnailArrayBuffer); console.log(`[SceneFusion] Uploaded thumbnail to: ${thumbnailResult.url}`); return { original: originalResult.url, thumbnail: thumbnailResult.url, }; } catch (error) { console.error("[SceneFusion] Failed to download/upload image:", error); throw error; } } /** * 根据 aspectRatio 和 imageSize 计算实际尺寸 * @param aspectRatio - 宽高比,如 "1:1", "4:3", "16:9", "auto" * @param imageSize - 分辨率等级,如 "1K", "2K", "4K" * @returns 尺寸字符串,如 "1024*1024",或 undefined(auto 模式) */ function calculateSize(aspectRatio: string, imageSize: string): string | undefined { // auto 模式:不设置尺寸,让 API 根据输入图片自动决定 if (aspectRatio === "auto") { return undefined; } // 根据分辨率等级确定基准像素 const baseSizeMap: Record = { "1K": 1024, "2K": 1536, "4K": 2048, }; const baseSize = baseSizeMap[imageSize] || 1536; // 根据比例计算宽高 const ratioMap: Record = { "1:1": [1, 1], "4:3": [4, 3], "3:4": [3, 4], "16:9": [16, 9], "9:16": [9, 16], }; const ratio = ratioMap[aspectRatio]; if (!ratio) { return undefined; } const [w, h] = ratio; // 保持总像素接近 baseSize * baseSize,同时维持比例 const scale = Math.sqrt((baseSize * baseSize) / (w * h)); let width = Math.round(w * scale); let height = Math.round(h * scale); // 确保宽高在 512-2048 范围内 const clamp = (v: number) => Math.max(512, Math.min(2048, v)); width = clamp(width); height = clamp(height); return `${width}*${height}`; } /** * 调用阿里生图 API */ async function callAlibabaImageAPI( baseURL: string, apiKey: string, modelId: string, productImageUrl: string, sceneImageUrl: string, prompt: string, size?: string ): Promise { // 构建消息内容:产品图 + 场景图 + 提示词 const content = [ { image: productImageUrl }, { image: sceneImageUrl }, { text: prompt }, ]; const requestBody = { model: modelId, input: { messages: [ { role: "user", content, }, ], }, parameters: { n: 1, ...(size && { size }), prompt_extend: true, watermark: false, }, }; console.log(`[SceneFusion][${modelId}] Request Debug: - Index: Scene ${sceneImageUrl.substring(sceneImageUrl.length-15)} - Product URL: ${productImageUrl} - Scene URL: ${sceneImageUrl} - Size: ${size || 'auto'} - Prompt: ${prompt} `); const response = await fetch(baseURL, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify(requestBody), }); const data = await response.json(); // 打印完整响应用于调试 console.log(`[SceneFusion][${modelId}] Full API Response:`, JSON.stringify(data, null, 2)); if (!response.ok || data.code) { console.error("[SceneFusion] API Error Response:", JSON.stringify(data, null, 2)); throw new Error(data.message || (data.code ? `Error ${data.code}: ${data.message}` : `API request failed: ${response.status}`)); } // 提取生成的图片 URL const choices = data.output?.choices; if (!choices || choices.length === 0) { console.error("[SceneFusion] No choices in response, output:", data.output); throw new Error("No images in response"); } const imageContent = choices[0]?.message?.content; console.log(`[SceneFusion] Image content array:`, JSON.stringify(imageContent, null, 2)); if (!imageContent || imageContent.length === 0) { throw new Error("No image content in response"); } // 遍历所有内容项,找到 image 类型的 let generatedImageUrl: string | undefined; for (const item of imageContent) { if (item.image) { generatedImageUrl = item.image; console.log(`[SceneFusion] Found image URL in content item`); break; } } if (!generatedImageUrl) { console.error("[SceneFusion] No image URL found in content:", imageContent); throw new Error("No image URL in response"); } // 检查返回的图片 URL 是否与输入相同(可能是 API 返回了原图) if (generatedImageUrl === productImageUrl || generatedImageUrl === sceneImageUrl) { console.warn(`[SceneFusion] ⚠️ WARNING: API returned same URL as input!`); console.warn(`[SceneFusion] - Generated URL: ${generatedImageUrl}`); console.warn(`[SceneFusion] - Product URL: ${productImageUrl}`); console.warn(`[SceneFusion] - Scene URL: ${sceneImageUrl}`); } console.log("[SceneFusion] Generated image URL:", generatedImageUrl.substring(0, 100) + "..."); return generatedImageUrl; } /** * 构建智能提示词 * @param defaultPrompt - 默认提示词模板 * @param productName - 产品名称(如"沙发") * @param targetName - 背景中需要被替换的物体名称(如"椅子") * @param customPrompt - 用户补充的关键词 */ function buildPrompt( defaultPrompt: string, productName?: string, targetName?: string, customPrompt?: string ): string { let prompt = defaultPrompt; // 如果用户提供了具体的产品名称和被替换物体名称,构建更精确的提示词 // 使用"参考图1"的表述方式,并强调产品的视觉特征 if (productName && targetName) { prompt = `请参考第一张图片中的${productName},将其自然地融合到第二张图片的场景中,替换掉场景中的${targetName}。要求:1)保持${productName}的整体外观、颜色、形状和设计风格与第一张图一致;2)调整${productName}的大小和角度以适应场景;3)注意光影和透视关系使融合自然。`; } else if (productName) { prompt = `请参考第一张图片中的${productName},将其自然地融合到第二张图片的场景中。要求:1)保持${productName}的整体外观、颜色、形状和设计风格与第一张图一致;2)调整${productName}的大小和角度以适应场景;3)注意光影和透视关系使融合自然。`; } else if (targetName) { prompt = `请参考第一张图片中的产品,将其自然地融合到第二张图片的场景中,替换掉场景中的${targetName}。要求:1)保持产品的整体外观、颜色、形状和设计风格与第一张图一致;2)调整产品的大小和角度以适应场景;3)注意光影和透视关系使融合自然。`; } // 添加用户补充的关键词 if (customPrompt) { prompt = `${prompt} 补充要求:${customPrompt}`; } return prompt; } /** * 通用场景处理函数 - 根据 provider 类型调用不同的实现 */ async function processSceneWithProvider( productImageUrl: string, sceneImageUrl: string, productName: string | undefined, targetName: string | undefined, customPrompt: string | undefined, defaultPrompt: string, provider: { type: string; baseUrl: string; apiKey: string }, modelId: string, aspectRatio?: string, imageSize?: string ): Promise<{ thumbnail: string; original: string; prompt: string }> { // 构建智能提示词 const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt); console.log(`[SceneFusion] Built prompt: ${prompt}`); console.log(`[SceneFusion] Provider type: ${provider.type}`); // 根据 provider 类型选择不同的处理逻辑 switch (provider.type) { case "alibaba-image": return processWithAlibabaImage(productImageUrl, sceneImageUrl, prompt, provider, modelId, aspectRatio, imageSize); case "ruxa-image": return processWithRuxaImage(productImageUrl, sceneImageUrl, prompt, provider, modelId, aspectRatio, imageSize); case "gemini": return processWithGemini(productImageUrl, sceneImageUrl, prompt, provider, modelId, aspectRatio, imageSize); case "volcano-vision": return processWithVolcanoVision(productImageUrl, sceneImageUrl, prompt, provider, modelId, aspectRatio, imageSize); default: throw new Error(`不支持的模型类型: ${provider.type}`); } } /** * 使用阿里生图模型处理 */ async function processWithAlibabaImage( productImageUrl: string, sceneImageUrl: string, prompt: string, provider: { baseUrl: string; apiKey: string }, modelId: string, aspectRatio?: string, imageSize?: string ): Promise<{ thumbnail: string; original: string; prompt: string }> { // 计算尺寸 const size = calculateSize(aspectRatio || "auto", imageSize || "2K"); // 调用阿里生图 API const generatedImageUrl = await callAlibabaImageAPI( provider.baseUrl, provider.apiKey, modelId, productImageUrl, sceneImageUrl, prompt, size ); // 下载并上传到 S3 const uploaded = await downloadAndUploadToS3(generatedImageUrl, "scene-fusion-alibaba"); return { ...uploaded, prompt }; } /** * 使用 Gemini 模型处理 */ async function processWithGemini( productImageUrl: string, sceneImageUrl: string, prompt: string, provider: { baseUrl: string; apiKey: string }, modelId: string, aspectRatio?: string, imageSize?: string ): Promise<{ thumbnail: string; original: string; prompt: string }> { console.log(`[SceneFusion][Gemini] Processing with model: ${modelId}`); // 创建 Gemini provider const geminiProvider = createGeminiImage({ baseURL: provider.baseUrl, apiKey: provider.apiKey, }); // 构建消息格式(Gemini 使用 OpenAI 兼容格式) const messages = [ { role: "user", content: [ { type: "image_url", image_url: { url: productImageUrl } }, { type: "image_url", image_url: { url: sceneImageUrl } }, { type: "text", text: prompt }, ], }, ]; // 调用 Gemini API const result = await geminiProvider.chat(modelId).doGenerate({ messages, imageParams: { aspectRatio: aspectRatio || "auto", imageSize: imageSize || "2K", imageCount: 1, }, }); // 解析返回的 Markdown 格式图片 const { thumbnail, original } = parseImageFromMarkdown(result.text); return { thumbnail, original, prompt }; } /** * 使用 RUXA 模型处理 */ async function processWithRuxaImage( productImageUrl: string, sceneImageUrl: string, prompt: string, provider: { baseUrl: string; apiKey: string }, modelId: string, aspectRatio?: string, imageSize?: string ): Promise<{ thumbnail: string; original: string; prompt: string }> { console.log(`[SceneFusion][RUXA] Processing with model: ${modelId}`); const ruxaProvider = createRuxaImage({ baseURL: provider.baseUrl, apiKey: provider.apiKey, }); const messages = [ { role: "user", content: [ { type: "image_url", image_url: { url: productImageUrl } }, { type: "image_url", image_url: { url: sceneImageUrl } }, { type: "text", text: prompt }, ], }, ]; const result = await ruxaProvider.chat(modelId).doGenerate({ messages, imageParams: { aspectRatio: aspectRatio || "auto", imageSize: imageSize || "2K", imageCount: 1, }, }); const { thumbnail, original } = parseImageFromMarkdown(result.text); return { thumbnail, original, prompt }; } /** * 使用既梦/火山模型处理 */ async function processWithVolcanoVision( productImageUrl: string, sceneImageUrl: string, prompt: string, provider: { baseUrl: string; apiKey: string }, modelId: string, aspectRatio?: string, imageSize?: string ): Promise<{ thumbnail: string; original: string; prompt: string }> { console.log(`[SceneFusion][Volcano] Processing with model: ${modelId}`); // 创建 Volcano Vision provider const volcanoProvider = createVolcanoVision({ baseURL: provider.baseUrl, apiKey: provider.apiKey, }); // 构建消息格式 const messages = [ { role: "user", content: [ { type: "image_url", image_url: { url: productImageUrl } }, { type: "image_url", image_url: { url: sceneImageUrl } }, { type: "text", text: prompt }, ], }, ]; // 调用 Volcano Vision API const result = await volcanoProvider.chat(modelId).doGenerate({ messages, imageParams: { aspectRatio: aspectRatio || "auto", imageSize: imageSize || "2K", imageCount: 1, }, }); // 解析返回的 Markdown 格式图片 const { thumbnail, original } = parseImageFromMarkdown(result.text); return { thumbnail, original, prompt }; } /** * 从 Markdown 格式解析图片 URL * 格式: ![thumb|||originalUrl|||size](thumbnailUrl) */ function parseImageFromMarkdown(markdown: string): { thumbnail: string; original: string } { console.log(`[SceneFusion] Parsing markdown: ${markdown.substring(0, 200)}...`); // 匹配 ![thumb|||originalUrl|||size](thumbnailUrl) 格式 const match = markdown.match(/!\[thumb\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/); if (match) { return { original: match[1], thumbnail: match[2], }; } // 尝试匹配普通图片格式 ![alt](url) const simpleMatch = markdown.match(/!\[[^\]]*\]\(([^)]+)\)/); if (simpleMatch) { return { original: simpleMatch[1], thumbnail: simpleMatch[1], }; } throw new Error("无法从响应中解析图片 URL"); } /** * 处理单个场景 (保留旧函数用于兼容) */ async function processScene( productImageUrl: string, sceneImageUrl: string, productName: string | undefined, targetName: string | undefined, customPrompt: string | undefined, defaultPrompt: string, baseURL: string, apiKey: string, modelId: string, size?: string ): Promise<{ thumbnail: string; original: string; prompt: string }> { // 构建智能提示词 const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt); console.log(`[SceneFusion] Built prompt: ${prompt}`); // 调用阿里生图 API const generatedImageUrl = await callAlibabaImageAPI( baseURL, apiKey, modelId, productImageUrl, sceneImageUrl, prompt, size ); // 下载并上传到 S3 const uploaded = await downloadAndUploadToS3(generatedImageUrl, "scene-fusion"); return { ...uploaded, prompt }; } export async function POST(request: Request) { try { // 1. 认证 const session = await auth(); if (!session?.user?.id) { return NextResponse.json( { error: "未登录" }, { status: 401 } ); } // 2. 获取场景裂变配置 const config = await getSceneFusionConfig(); if (!config.enabled) { return NextResponse.json( { error: "场景裂变功能已被管理员禁用" }, { status: 403 } ); } // 3. 解析请求体 const json = await request.json(); const parseResult = sceneFusionRequestSchema.safeParse(json); if (!parseResult.success) { return NextResponse.json( { error: parseResult.error.errors[0].message }, { status: 400 } ); } const { productImage, productName, aspectRatio, imageSize, scenes } = parseResult.data; // 计算输出尺寸 const size = calculateSize(aspectRatio || "auto", imageSize || "2K"); console.log(`[SceneFusion] Output settings: aspectRatio=${aspectRatio || 'auto'}, imageSize=${imageSize || '2K'}, calculated size=${size || 'auto'}`); // 检查场景数量限制 if (scenes.length > config.maxScenes) { return NextResponse.json( { error: `最多只能添加 ${config.maxScenes} 个场景` }, { status: 400 } ); } console.log(`[SceneFusion] Processing ${scenes.length} scenes for user ${session.user.id}`); console.log(`[SceneFusion] Product name: ${productName || '(not specified)'}`); console.log(`[SceneFusion] Using default prompt: ${config.defaultPrompt.substring(0, 50)}...`); console.log(`[SceneFusion] Configured model ID: ${config.modelId || '(auto - default alibaba)'}`); // 4. 获取生图模型配置 const allModels = await getEnabledAiModelsWithProvider(); let selectedModel: typeof allModels[0] | undefined; // 如果配置了指定的模型ID,优先使用 if (config.modelId) { selectedModel = allModels.find( (m) => m.model.id === config.modelId && m.model.isEnabled ); if (!selectedModel) { console.warn(`[SceneFusion] Configured model ${config.modelId} not found or disabled, falling back to default`); } } // 如果没有配置或配置的模型不可用,使用默认的阿里生图模型 if (!selectedModel) { // 按优先级查找:alibaba-image > ruxa-image > gemini > volcano-vision const imageProviderTypes = ["alibaba-image", "ruxa-image", "gemini", "volcano-vision"]; for (const providerType of imageProviderTypes) { selectedModel = allModels.find( (m) => m.provider.type === providerType && m.model.isEnabled ); if (selectedModel) break; } } if (!selectedModel) { return NextResponse.json( { error: "未配置可用的生图模型,请联系管理员" }, { status: 500 } ); } const { provider, model } = selectedModel; console.log(`[SceneFusion] Using model: ${model.displayName} (${model.modelId}) - Provider: ${provider.type}`); // 4. 预处理:将 Base64 图片上传到 S3 let productImageUrl = productImage; if (productImage.startsWith("data:")) { productImageUrl = await uploadBase64ToS3(productImage, "scene-fusion-product"); } const processedScenes = await Promise.all( scenes.map(async (scene) => { let sceneImageUrl = scene.image; if (scene.image.startsWith("data:")) { sceneImageUrl = await uploadBase64ToS3(scene.image, "scene-fusion-scene"); } return { ...scene, imageUrl: sceneImageUrl, }; }) ); // 6. 串行处理所有场景,每次间隔5秒 const results = []; // 6. 创建流式响应,每完成一张图就立即发送给前端 const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); try { // 发送初始消息 controller.enqueue(encoder.encode(JSON.stringify({ type: "start", total: processedScenes.length }) + "\n")); let successCount = 0; let failCount = 0; for (let i = 0; i < processedScenes.length; i++) { const scene = processedScenes[i]; console.log(`[SceneFusion] ========== Processing scene ${i + 1}/${processedScenes.length} ==========`); console.log(`[SceneFusion] Scene ID: ${scene.id}`); console.log(`[SceneFusion] Product Image URL: ${productImageUrl.substring(0, 80)}...`); console.log(`[SceneFusion] Scene Image URL: ${scene.imageUrl.substring(0, 80)}...`); console.log(`[SceneFusion] Target Name: ${scene.targetName || '(not specified)'}`); console.log(`[SceneFusion] Custom Prompt: ${scene.customPrompt || '(none)'}`); console.log(`[SceneFusion] Provider Type: ${provider.type}`); // 如果不是第一张图,等待5秒 if (i > 0) { console.log(`[SceneFusion] Waiting 5 seconds before processing...`); await new Promise((resolve) => setTimeout(resolve, 5000)); } try { // 使用新的通用处理函数,支持多种模型类型 const result = await processSceneWithProvider( productImageUrl, scene.imageUrl, productName, scene.targetName, scene.customPrompt, config.defaultPrompt, { type: provider.type, baseUrl: provider.baseUrl, apiKey: provider.apiKey }, model.modelId, aspectRatio, imageSize ); // 将分辨率等级用于数据库存储 const dbImageSize = imageSize || "2K"; // 保存到数据库(chatId 和 messageId 设为 null,因为场景裂变不关联聊天) try { await db.insert(generatedImage).values({ userId: session.user.id, chatId: null, // 场景裂变不关联聊天 messageId: null, // 场景裂变不关联消息 thumbnailUrl: result.thumbnail, originalUrl: result.original, imageSize: dbImageSize, prompt: `[场景裂变] ${result.prompt}`, modelId: model.modelId, }); console.log(`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`); } catch (dbError) { console.error("[SceneFusion] ❌ Failed to save to gallery:", dbError); } successCount++; // 立即发送完成的结果给前端 controller.enqueue(encoder.encode(JSON.stringify({ type: "result", data: { id: scene.id, status: "completed" as const, result: result.thumbnail, // 返回缩略图用于预览 originalUrl: result.original, // 原图用于下载 }, }) + "\n")); } catch (error) { console.error(`[SceneFusion] Failed to process scene ${scene.id}:`, error); failCount++; // 立即发送失败的结果给前端 controller.enqueue(encoder.encode(JSON.stringify({ type: "result", data: { id: scene.id, status: "failed" as const, error: error instanceof Error ? error.message : "处理失败", }, }) + "\n")); } } // 发送完成消息 console.log(`[SceneFusion] Completed: ${successCount} success, ${failCount} failed`); controller.enqueue(encoder.encode(JSON.stringify({ type: "done", summary: { total: processedScenes.length, success: successCount, failed: failCount, }, }) + "\n")); } catch (error) { console.error("[SceneFusion] Stream error:", error); // 发送错误消息 controller.enqueue(encoder.encode(JSON.stringify({ type: "error", error: error instanceof Error ? error.message : "处理失败", }) + "\n")); } finally { controller.close(); } }, }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", }, }); } catch (error) { console.error("[SceneFusion] Unexpected error:", error); return NextResponse.json( { error: error instanceof Error ? error.message : "处理失败" }, { status: 500 } ); } }