lovart3/app/api/extend/route.ts
Your Name 60082aca6f 修复时间显示和图像拓展功能
- 添加formatBeijingTime函数,将所有时间显示转换为北京时间(UTC+8)
- 更新相册、管理后台、精选页面的时间显示
- 图像拓展功能改为直接发送图片URL而不是base64
- 优化API性能,减少数据传输量
2025-12-30 20:16:47 +08:00

466 lines
16 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;
/**
* 将图片上传到S3并返回公网URL(用于API调用)
*/
async function uploadImageToS3ForAPI(imageBase64: string, mimeType: string = "image/jpeg"): Promise<string> {
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<string> {
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图片或图片URL
const body = await request.json();
const imageUrl = body.image_url; // 优先使用URL
imageBase64 = body.image_base64; // 如果没有URL,使用Base64
prompt = body.prompt || "图像拓展处理";
chatId = body.chatId || null;
messageId = body.messageId || null;
// 如果提供了URL,直接使用;否则需要Base64
if (imageUrl) {
console.log("[Extend API] 使用图片URL:", imageUrl);
// 直接使用URL,跳过Base64转换和上传步骤
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(imageUrl, config.apiKey, xScale, yScale);
console.log("[Extend API] 拓展后图片URL:", extendedImageUrl);
// 下载拓展后的图片
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");
// 上传到S3
console.log("[Extend API] 上传到S3...");
const { thumbnail, original } = await uploadBase64ToS3(extendedImageBase64, "image/jpeg");
// 保存到数据库(相册和生图记录)
console.log("[Extend API] 保存到数据库...");
let savedImage;
try {
[savedImage] = await db
.insert(generatedImage)
.values({
userId,
chatId: chatId || null,
messageId: messageId || null,
thumbnailUrl: thumbnail,
originalUrl: original,
prompt: prompt || "图像拓展处理",
modelId: "extend-1.0",
imageSize: "2K",
})
.returning();
console.log(`[Extend API] ✅ 图片已保存到相册和生图记录,ID: ${savedImage.id}`);
} catch (dbError) {
console.error("[Extend API] ❌ 保存到数据库失败:", dbError);
}
const processingTime = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`[Extend API] ✅ 图像拓展完成,处理时间: ${processingTime}秒`);
// 返回结果
return NextResponse.json({
success: true,
original_url: original,
thumbnail_url: thumbnail,
base64: extendedImageBase64,
processing_time: parseFloat(processingTime),
});
}
if (!imageBase64) {
return NextResponse.json(
{ error: "缺少image_base64或image_url参数" },
{ 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] 保存到数据库...");
let savedImage;
try {
[savedImage] = await db
.insert(generatedImage)
.values({
userId,
chatId: chatId || null,
messageId: messageId || null,
thumbnailUrl: thumbnail,
originalUrl: original,
prompt: prompt || "图像拓展处理",
modelId: "extend-1.0",
imageSize: "2K",
})
.returning();
console.log(`[Extend API] ✅ 图片已保存到相册和生图记录,ID: ${savedImage.id}`);
} catch (dbError) {
console.error("[Extend API] ❌ 保存到数据库失败:", dbError);
// 数据库保存失败时仍然返回成功,但记录错误(图片已上传到S3,用户可以访问)
}
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 }
);
}
}