Your Name da99fbdc93
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: improve error handling for streaming response in scene fusion
2026-01-22 19:41:58 +08:00

813 lines
27 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.

/**
* 场景裂变 API
*
* 将一张产品图批量融合到多个场景图中
* 支持多种生图模型:阿里生图、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";
// 请求体 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<Buffer> {
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<string> {
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<string, number> = {
"1K": 1024,
"2K": 1536,
"4K": 2048,
};
const baseSize = baseSizeMap[imageSize] || 1536;
// 根据比例计算宽高
const ratioMap: Record<string, [number, number]> = {
"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<string> {
// 构建消息内容:产品图 + 场景图 + 提示词
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 "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 };
}
/**
* 使用既梦/火山模型处理
*/
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 > gemini > volcano-vision
const imageProviderTypes = ["alibaba-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 }
);
}
}