lovart3/app/api/extend/route.ts
Your Name 7c253e04a0 feat: 添加图像拓展功能
- 新增 /api/extend 路由,集成阿里云Dashscope图像拓展API
- 在系统设置'本地AI设置'中添加图像拓展API配置(API密钥、X/Y轴放大倍数)
- 在图片标注组件中添加'图像拓展'按钮(橙色,Expand图标)
- 在聊天对话图片上添加'图像拓展'按钮,支持动态占位卡片
- 更新ImageProcessingLoading组件支持extend类型(橙色主题)
- 处理后的图片上传到S3,保存到数据库(modelId: extend-1.0)
- 同步到我的相册和出图记录
- 添加完整的API调用文档
2025-12-29 10:54:32 +08:00

353 lines
12 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.

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<Buffer> {
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;
/**
* 图像拓展核心实现
* @param imageBase64 - Base64编码的图片数据
* @param apiKey - Dashscope API Key
* @param xScale - X轴放大倍数(默认1.5)
* @param yScale - Y轴放大倍数(默认1.5)
* @returns 拓展后的图片URL
*/
async function extendImage(
imageBase64: string,
apiKey: string,
xScale: number = 1.5,
yScale: number = 1.5
): Promise<string> {
console.log("[Extend API] 开始图像拓展...");
// 1. 提交拓展任务
const requestBody = {
model: "image-out-painting",
input: {
image_base64: imageBase64,
},
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. 调用图像拓展API
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(imageBase64, 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 }
);
}
}