import { NextRequest, NextResponse } from "next/server"; import { getSystemSetting } from "@/lib/db/queries"; import { auth } from "@/app/(auth)/auth"; import { uploadFile } from "@/lib/storage/service"; import { db } from "@/lib/db"; import { generatedImage } from "@/lib/db/schema"; /** * 压缩图片生成缩略图(使用 sharp) */ async function createThumbnail(buffer: Buffer, mimeType: string): Promise { try { const sharp = await import("sharp"); // 创建缩略图(宽度 500px,高度按比例,质量 80%) const thumbnailBuffer = await sharp.default(buffer) .resize(500, null, { fit: "inside", withoutEnlargement: true, }) .jpeg({ quality: 80 }) // JPEG格式 .toBuffer(); console.log(`[Extend] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`); return thumbnailBuffer; } catch (error) { console.error("[Extend] Failed to create thumbnail:", error); // 如果失败,返回原图 return buffer; } } /** * 将 base64 图片上传到 S3,返回缩略图和原图的 URL */ async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jpeg"): Promise<{ thumbnail: string; original: string }> { try { // 解析 base64 数据 const buffer = Buffer.from(base64Data, "base64"); // 生成文件名 const ext = mimeType.split("/")[1] || "jpg"; const timestamp = Date.now(); const randomStr = Math.random().toString(36).slice(2, 8); const thumbnailFilename = `extend-thumb-${timestamp}-${randomStr}.${ext}`; const originalFilename = `extend-orig-${timestamp}-${randomStr}.${ext}`; console.log(`[Extend] Original size: ${buffer.length} bytes`); // 生成缩略图 const thumbnailBuffer = await createThumbnail(buffer, mimeType); // 转换为 ArrayBuffer const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice( thumbnailBuffer.byteOffset, thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength ) as ArrayBuffer; const originalArrayBuffer = buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; // 并行上传缩略图和原图到 S3 console.log(`[Extend] Uploading thumbnail and original to S3...`); const [thumbnailResult, originalResult] = await Promise.all([ uploadFile(thumbnailFilename, thumbnailArrayBuffer), uploadFile(originalFilename, originalArrayBuffer), ]); console.log(`[Extend] Uploaded to S3:`); console.log(` - Thumbnail: ${thumbnailResult.url}`); console.log(` - Original: ${originalResult.url}`); return { thumbnail: thumbnailResult.url, original: originalResult.url, }; } catch (error) { console.error("[Extend] Failed to upload to S3:", error); throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`); } } // 设置API路由最大执行时间为3分钟(180秒) export const maxDuration = 180; /** * 将图片上传到S3并返回公网URL(用于API调用) */ async function uploadImageToS3ForAPI(imageBase64: string, mimeType: string = "image/jpeg"): Promise { try { // 解析 base64 数据 const buffer = Buffer.from(imageBase64, "base64"); // 生成文件名 const ext = mimeType.split("/")[1] || "jpg"; const timestamp = Date.now(); const randomStr = Math.random().toString(36).slice(2, 8); const filename = `extend-input-${timestamp}-${randomStr}.${ext}`; console.log(`[Extend API] 上传原图到S3,大小: ${(buffer.length / 1024).toFixed(2)}KB`); // 转换为 ArrayBuffer const arrayBuffer = buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; // 上传到 S3 const result = await uploadFile(filename, arrayBuffer); console.log(`[Extend API] ✅ 原图已上传到S3: ${result.url}`); return result.url; } catch (error) { console.error("[Extend API] 上传到S3失败:", error); throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`); } } /** * 图像拓展核心实现(使用URL方式,避免Base64长度限制) * @param imageUrl - 图片的公网URL * @param apiKey - Dashscope API Key * @param xScale - X轴放大倍数(默认1.5) * @param yScale - Y轴放大倍数(默认1.5) * @returns 拓展后的图片URL */ async function extendImage( imageUrl: string, apiKey: string, xScale: number = 1.5, yScale: number = 1.5 ): Promise { console.log("[Extend API] 开始图像拓展..."); console.log("[Extend API] 使用图片URL:", imageUrl); // 1. 提交拓展任务(使用URL方式,避免Base64长度限制) const requestBody = { model: "image-out-painting", input: { image_url: imageUrl, }, parameters: { x_scale: xScale, y_scale: yScale, best_quality: false, limit_image_size: true, add_watermark: false, }, }; console.log("[Extend API] 提交拓展任务..."); const response = await fetch( "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, "X-DashScope-Async": "enable", }, body: JSON.stringify(requestBody), } ); const initialResult = await response.json(); if (initialResult.code) { console.error("[Extend API] API调用失败:", initialResult); throw new Error( `图像拓展API错误: ${initialResult.message || JSON.stringify(initialResult)}` ); } // 2. 获取任务ID const taskId = initialResult.output.task_id; const taskStatusUrl = `https://dashscope.aliyuncs.com/api/v1/tasks/${taskId}`; console.log(`[Extend API] 任务已提交,任务ID: ${taskId}`); // 3. 等待3秒后开始轮询 console.log("[Extend API] 等待API初始化..."); await new Promise((resolve) => setTimeout(resolve, 3000)); // 4. 轮询获取结果 let attempts = 0; const maxAttempts = 60; // 最多轮询60次 const pollInterval = 2000; // 每2秒轮询一次 console.log("[Extend API] 开始轮询任务状态..."); while (attempts < maxAttempts) { const statusResponse = await fetch(taskStatusUrl, { headers: { Authorization: `Bearer ${apiKey}`, }, }); const result = await statusResponse.json(); if (attempts === 0 || attempts % 5 === 0) { const elapsedTime = attempts * 2 + 3; console.log(`[Extend API] [${elapsedTime}秒] 任务状态: ${result.output?.task_status || "UNKNOWN"}`); } if (result.output?.task_status === "SUCCEEDED") { const elapsedTime = attempts * 2 + 3; console.log(`[Extend API] ✅ 任务完成(总用时 ${elapsedTime}秒)`); return result.output.output_image_url; } else if (result.output?.task_status === "FAILED") { console.error("[Extend API] 任务失败:", result.output); throw new Error( `图像拓展任务失败: ${result.output.message || result.output.code || "未知错误"}` ); } attempts++; if (attempts < maxAttempts) { await new Promise((resolve) => setTimeout(resolve, pollInterval)); } } throw new Error(`图像拓展任务超时(等待${maxAttempts * 2 + 3}秒)`); } /** * 图像拓展API路由 * 从数据库读取图像拓展API配置,调用Dashscope API进行图像拓展处理 * 将处理后的图片上传到S3并保存到数据库 */ export async function POST(request: NextRequest) { console.log("[Extend API] ===== 收到请求 ====="); try { // 0. 获取用户会话 console.log("[Extend API] 开始获取用户会话..."); const session = await auth(); console.log("[Extend API] 用户会话获取完成:", session?.user?.id ? "已登录" : "未登录"); if (!session?.user?.id) { return NextResponse.json( { error: "未授权,请先登录" }, { status: 401 } ); } const userId = session.user.id; console.log("[Extend API] 用户ID:", userId); // 1. 从数据库获取图像拓展API配置 console.log("[Extend API] 开始获取配置..."); const extendConfig = await getSystemSetting({ key: "extend_api_config" }); console.log("[Extend API] 配置获取完成:", extendConfig ? "已找到" : "未找到"); if (!extendConfig || !extendConfig.value) { return NextResponse.json( { error: "图像拓展API配置未设置,请在系统设置中配置" }, { status: 400 } ); } const config = extendConfig.value as { apiKey?: string; xScale?: number; yScale?: number; }; if (!config.apiKey) { return NextResponse.json( { error: "图像拓展API密钥未配置" }, { status: 400 } ); } // 2. 获取请求体(Base64图片或FormData) console.log("[Extend API] 开始获取请求体..."); const contentType = request.headers.get("content-type") || ""; console.log("[Extend API] Content-Type:", contentType); let imageBase64: string; let prompt = ""; // 用户提示词(可选) let chatId: string | null = null; // 聊天ID(可选) let messageId: string | null = null; if (contentType.includes("application/json")) { // JSON格式:Base64图片 const body = await request.json(); imageBase64 = body.image_base64; prompt = body.prompt || "图像拓展处理"; chatId = body.chatId || null; messageId = body.messageId || null; if (!imageBase64) { return NextResponse.json( { error: "缺少image_base64参数" }, { status: 400 } ); } } else if (contentType.includes("multipart/form-data")) { // FormData格式:文件上传 const formData = await request.formData(); const file = formData.get("file") as File; if (!file) { return NextResponse.json( { error: "缺少file参数" }, { status: 400 } ); } // 将文件转换为Base64 const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); imageBase64 = buffer.toString("base64"); prompt = (formData.get("prompt") as string) || "图像拓展处理"; chatId = (formData.get("chatId") as string) || null; messageId = (formData.get("messageId") as string) || null; } else { return NextResponse.json( { error: "不支持的Content-Type,请使用application/json或multipart/form-data" }, { status: 400 } ); } console.log(`[Extend API] 图片Base64长度: ${imageBase64.length} 字符`); // 3. 先将图片上传到S3获取公网URL(使用URL方式避免Base64长度限制) console.log("[Extend API] 上传原图到S3获取公网URL..."); const inputImageUrl = await uploadImageToS3ForAPI(imageBase64, "image/jpeg"); console.log("[Extend API] 原图URL:", inputImageUrl); // 4. 调用图像拓展API(使用URL方式) const startTime = Date.now(); const xScale = config.xScale || 1.5; const yScale = config.yScale || 1.5; console.log(`[Extend API] 拓展参数: X=${xScale}, Y=${yScale}`); const extendedImageUrl = await extendImage(inputImageUrl, config.apiKey, xScale, yScale); console.log("[Extend API] 拓展后图片URL:", extendedImageUrl); // 4. 下载拓展后的图片 console.log("[Extend API] 下载拓展后的图片..."); const imageResponse = await fetch(extendedImageUrl); if (!imageResponse.ok) { throw new Error(`下载拓展图片失败: ${imageResponse.status} ${imageResponse.statusText}`); } const arrayBuffer = await imageResponse.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const extendedImageBase64 = buffer.toString("base64"); // 5. 上传到S3 console.log("[Extend API] 上传到S3..."); const { thumbnail, original } = await uploadBase64ToS3(extendedImageBase64, "image/jpeg"); // 6. 保存到数据库 console.log("[Extend API] 保存到数据库..."); const [savedImage] = await db .insert(generatedImage) .values({ userId, chatId, messageId, thumbnailUrl: thumbnail, originalUrl: original, prompt, modelId: "extend-1.0", imageSize: "2K", }) .returning(); const processingTime = ((Date.now() - startTime) / 1000).toFixed(2); console.log(`[Extend API] ✅ 图像拓展完成,处理时间: ${processingTime}秒`); // 7. 返回结果 return NextResponse.json({ success: true, original_url: original, thumbnail_url: thumbnail, base64: extendedImageBase64, processing_time: parseFloat(processingTime), }); } catch (error) { console.error("[Extend API] 错误:", error); return NextResponse.json( { error: "图像拓展失败", detail: error instanceof Error ? error.message : String(error), }, { status: 500 } ); } }