- 添加formatBeijingTime函数,将所有时间显示转换为北京时间(UTC+8) - 更新相册、管理后台、精选页面的时间显示 - 图像拓展功能改为直接发送图片URL而不是base64 - 优化API性能,减少数据传输量
466 lines
16 KiB
TypeScript
466 lines
16 KiB
TypeScript
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 }
|
||
);
|
||
}
|
||
}
|
||
|
||
|