471 lines
16 KiB
TypeScript
471 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(`[Enhance] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`);
|
||
return thumbnailBuffer;
|
||
} catch (error) {
|
||
console.error("[Enhance] 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 = `enhance-thumb-${timestamp}-${randomStr}.${ext}`;
|
||
const originalFilename = `enhance-orig-${timestamp}-${randomStr}.${ext}`;
|
||
|
||
console.log(`[Enhance] 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(`[Enhance] Uploading thumbnail and original to S3...`);
|
||
const [thumbnailResult, originalResult] = await Promise.all([
|
||
uploadFile(thumbnailFilename, thumbnailArrayBuffer),
|
||
uploadFile(originalFilename, originalArrayBuffer),
|
||
]);
|
||
|
||
console.log(`[Enhance] Uploaded to S3:`);
|
||
console.log(` - Thumbnail: ${thumbnailResult.url}`);
|
||
console.log(` - Original: ${originalResult.url}`);
|
||
|
||
return {
|
||
thumbnail: thumbnailResult.url,
|
||
original: originalResult.url,
|
||
};
|
||
} catch (error) {
|
||
console.error("[Enhance] Failed to upload to S3:", error);
|
||
throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 轮询获取任务结果
|
||
*/
|
||
async function pollTaskResult(apiUrl: string, apiKey: string, taskId: string, maxAttempts: number = 30): Promise<any> {
|
||
const pollUrl = `${apiUrl}/api/tasks/visual/scale/${taskId}`;
|
||
|
||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||
try {
|
||
const response = await fetch(pollUrl, {
|
||
method: "GET",
|
||
headers: {
|
||
"X-API-KEY": apiKey,
|
||
},
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`轮询请求失败: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
// 检查任务状态
|
||
if (result.status === 200 && result.data) {
|
||
if (result.data.state === 1) {
|
||
// 任务完成
|
||
return result.data;
|
||
} else if (result.data.state === 0) {
|
||
// 任务进行中,继续轮询
|
||
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
||
continue;
|
||
} else {
|
||
// 任务失败
|
||
throw new Error(`任务失败: state=${result.data.state}`);
|
||
}
|
||
} else {
|
||
throw new Error(`API返回错误: ${result.message || "未知错误"}`);
|
||
}
|
||
} catch (error) {
|
||
if (attempt === maxAttempts - 1) {
|
||
throw error;
|
||
}
|
||
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒后重试
|
||
}
|
||
}
|
||
|
||
throw new Error("轮询超时,任务未在30秒内完成");
|
||
}
|
||
|
||
/**
|
||
* 图片变清晰API路由
|
||
* 从数据库读取变清晰API配置,调用外部API进行图片增强处理
|
||
* 将处理后的图片上传到S3并保存到数据库
|
||
*/
|
||
export async function POST(request: NextRequest) {
|
||
try {
|
||
// 0. 获取用户会话
|
||
const session = await auth();
|
||
if (!session?.user?.id) {
|
||
return NextResponse.json(
|
||
{ error: "未授权,请先登录" },
|
||
{ status: 401 }
|
||
);
|
||
}
|
||
|
||
const userId = session.user.id;
|
||
// 1. 从数据库获取变清晰API配置
|
||
const enhanceConfig = await getSystemSetting({ key: "enhance_api_config" });
|
||
|
||
if (!enhanceConfig || !enhanceConfig.value) {
|
||
return NextResponse.json(
|
||
{ error: "变清晰API配置未设置,请在系统设置中配置" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
const config = enhanceConfig.value as {
|
||
apiUrl?: string;
|
||
apiKey?: string;
|
||
type?: string; // "clean" | "face"
|
||
scaleFactor?: number | string; // 1 | 2 | 4 | "auto"
|
||
};
|
||
|
||
if (!config.apiUrl || !config.apiKey) {
|
||
return NextResponse.json(
|
||
{ error: "变清晰API地址或密钥未配置" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// 2. 获取请求体(Base64图片或FormData)
|
||
const contentType = request.headers.get("content-type") || "";
|
||
|
||
let imageBase64: string;
|
||
let prompt = ""; // 用户提示词(可选)
|
||
let chatId: string | null = null; // 聊天ID(可选)
|
||
let messageId: string | null = null; // 消息ID(可选)
|
||
|
||
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 }
|
||
);
|
||
}
|
||
|
||
// 3. 调用变清晰API(使用同步模式)
|
||
const apiUrl = config.apiUrl.endsWith("/")
|
||
? config.apiUrl.slice(0, -1)
|
||
: config.apiUrl;
|
||
|
||
const enhanceUrl = `${apiUrl}/api/tasks/visual/scale`;
|
||
|
||
// 将Base64转换为Buffer
|
||
const imageBuffer = Buffer.from(imageBase64, "base64");
|
||
|
||
// 创建FormData(使用form-data包,Node.js环境)
|
||
const FormDataLib = await import("form-data");
|
||
const formData = new FormDataLib.default();
|
||
formData.append("image_file", imageBuffer, {
|
||
filename: "image.jpg",
|
||
contentType: "image/jpeg",
|
||
});
|
||
formData.append("sync", "1"); // 同步模式
|
||
formData.append("type", config.type || "clean"); // 默认通用清晰化
|
||
const scaleFactorValue = typeof config.scaleFactor === "number" ? config.scaleFactor : (config.scaleFactor === "auto" ? undefined : Number.parseInt(String(config.scaleFactor)));
|
||
if (scaleFactorValue) {
|
||
formData.append("scale_factor", String(scaleFactorValue));
|
||
}
|
||
formData.append("return_type", "2"); // 返回base64
|
||
|
||
const headers: Record<string, string> = {
|
||
"X-API-KEY": config.apiKey,
|
||
...formData.getHeaders(), // 包含Content-Type和boundary
|
||
};
|
||
|
||
const startTime = Date.now();
|
||
let response: Response;
|
||
|
||
try {
|
||
// 添加超时控制(60秒,因为同步模式可能需要更长时间)
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 60000);
|
||
|
||
// 将FormData转换为Buffer(form-data包是stream)
|
||
const formDataBuffer = await new Promise<Buffer>((resolve, reject) => {
|
||
const chunks: Buffer[] = [];
|
||
formData.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||
formData.on("end", () => resolve(Buffer.concat(chunks)));
|
||
formData.on("error", reject);
|
||
});
|
||
|
||
// 将Buffer转换为Uint8Array用于fetch(更兼容)
|
||
const uint8Array = new Uint8Array(formDataBuffer);
|
||
|
||
response = await fetch(enhanceUrl, {
|
||
method: "POST",
|
||
headers,
|
||
body: uint8Array,
|
||
signal: controller.signal,
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
} catch (fetchError) {
|
||
const processingTime = (Date.now() - startTime) / 1000;
|
||
console.error("[Enhance API] 网络请求失败:", {
|
||
error: fetchError,
|
||
enhanceUrl,
|
||
processingTime,
|
||
});
|
||
|
||
// 检查是否是网络连接错误
|
||
if (fetchError instanceof Error) {
|
||
if (fetchError.name === 'AbortError') {
|
||
return NextResponse.json(
|
||
{
|
||
error: "变清晰API请求超时",
|
||
detail: "请求超过60秒未响应,请检查API服务器状态或网络连接",
|
||
},
|
||
{ status: 504 }
|
||
);
|
||
}
|
||
|
||
// 检查错误消息和cause中的网络错误
|
||
const errorMessage = fetchError.message || '';
|
||
const errorCause = (fetchError as any).cause;
|
||
const causeMessage = errorCause?.message || '';
|
||
const causeCode = errorCause?.code || '';
|
||
|
||
// 检查各种网络连接错误
|
||
if (errorMessage.includes('EHOSTUNREACH') ||
|
||
errorMessage.includes('ENOTFOUND') ||
|
||
errorMessage.includes('ECONNREFUSED') ||
|
||
errorMessage.includes('No route to host') ||
|
||
causeMessage.includes('EHOSTUNREACH') ||
|
||
causeMessage.includes('ENOTFOUND') ||
|
||
causeMessage.includes('ECONNREFUSED') ||
|
||
causeMessage.includes('No route to host') ||
|
||
causeCode === 'EHOSTUNREACH' ||
|
||
causeCode === 'ENOTFOUND' ||
|
||
causeCode === 'ECONNREFUSED') {
|
||
return NextResponse.json(
|
||
{
|
||
error: "无法连接到变清晰API服务器",
|
||
detail: `服务器无法访问 ${enhanceUrl},请检查:\n1. API服务器是否在线\n2. 网络连接是否正常\n3. 防火墙设置是否正确\n4. API地址和密钥是否正确`,
|
||
},
|
||
{ status: 503 }
|
||
);
|
||
}
|
||
}
|
||
|
||
return NextResponse.json(
|
||
{
|
||
error: "变清晰API请求失败",
|
||
detail: fetchError instanceof Error ? fetchError.message : "未知网络错误",
|
||
},
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
|
||
const processingTime = (Date.now() - startTime) / 1000;
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error("[Enhance API] 请求失败:", {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
error: errorText,
|
||
enhanceUrl,
|
||
});
|
||
|
||
return NextResponse.json(
|
||
{
|
||
error: "变清晰API请求失败",
|
||
detail: errorText || response.statusText,
|
||
},
|
||
{ status: response.status }
|
||
);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
// 4. 处理返回结果
|
||
if (result.status !== 200 || !result.data) {
|
||
return NextResponse.json(
|
||
{ error: "变清晰处理失败", detail: result.message || "未知错误" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
|
||
// 检查是否是异步任务(返回task_id)
|
||
let enhancedImageBase64: string;
|
||
let finalProcessingTime = processingTime;
|
||
|
||
if (result.data.task_id && !result.data.image && !result.data.image_base64) {
|
||
// 异步模式,需要轮询
|
||
console.log(`[Enhance] 异步任务,task_id: ${result.data.task_id}`);
|
||
const taskResult = await pollTaskResult(apiUrl, config.apiKey, result.data.task_id);
|
||
|
||
if (taskResult.image_base64) {
|
||
enhancedImageBase64 = taskResult.image_base64;
|
||
} else if (taskResult.image) {
|
||
// 返回的是URL,需要下载
|
||
const imageResponse = await fetch(taskResult.image);
|
||
const imageBuffer = await imageResponse.arrayBuffer();
|
||
enhancedImageBase64 = Buffer.from(imageBuffer).toString("base64");
|
||
} else {
|
||
return NextResponse.json(
|
||
{ error: "变清晰处理失败", detail: "轮询结果未包含图片数据" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
finalProcessingTime = (Date.now() - startTime) / 1000;
|
||
} else if (result.data.image_base64) {
|
||
// 同步模式,直接返回base64
|
||
enhancedImageBase64 = result.data.image_base64;
|
||
} else if (result.data.image) {
|
||
// 返回的是URL,需要下载
|
||
const imageResponse = await fetch(result.data.image);
|
||
if (!imageResponse.ok) {
|
||
return NextResponse.json(
|
||
{ error: "变清晰处理失败", detail: "无法下载处理后的图片" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
const imageBuffer = await imageResponse.arrayBuffer();
|
||
enhancedImageBase64 = Buffer.from(imageBuffer).toString("base64");
|
||
} else {
|
||
return NextResponse.json(
|
||
{ error: "变清晰处理失败", detail: "API未返回处理后的图片" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
|
||
if (!enhancedImageBase64) {
|
||
return NextResponse.json(
|
||
{ error: "变清晰处理失败", detail: "无法获取处理后的图片" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
|
||
// 5. 上传处理后的图片到S3存储桶
|
||
let thumbnailUrl: string;
|
||
let originalUrl: string;
|
||
|
||
try {
|
||
const uploadResult = await uploadBase64ToS3(enhancedImageBase64, "image/jpeg");
|
||
thumbnailUrl = uploadResult.thumbnail;
|
||
originalUrl = uploadResult.original;
|
||
console.log(`[Enhance] ✅ Images uploaded to S3 successfully`);
|
||
} catch (uploadError) {
|
||
console.error("[Enhance] ❌ Failed to upload to S3:", uploadError);
|
||
return NextResponse.json(
|
||
{
|
||
error: "图片上传到存储桶失败",
|
||
detail: uploadError instanceof Error ? uploadError.message : "未知错误"
|
||
},
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
|
||
// 6. 保存图片记录到数据库
|
||
try {
|
||
await db.insert(generatedImage).values({
|
||
userId,
|
||
chatId: chatId || null,
|
||
messageId: messageId || null,
|
||
thumbnailUrl,
|
||
originalUrl,
|
||
imageSize: "2K", // 默认尺寸
|
||
prompt: prompt || "图片变清晰处理",
|
||
modelId: "enhance-1.0", // 标记为变清晰处理
|
||
});
|
||
console.log(`[Enhance] ✅ Image record saved to database`);
|
||
} catch (dbError) {
|
||
console.error("[Enhance] ❌ Failed to save image record to database:", dbError);
|
||
// 即使数据库保存失败,也返回成功(因为图片已上传到S3)
|
||
}
|
||
|
||
// 7. 返回结果(包含S3 URL和Base64,前端可以选择使用)
|
||
return NextResponse.json({
|
||
success: true,
|
||
image_base64: enhancedImageBase64, // 保留Base64用于前端立即显示
|
||
thumbnail_url: thumbnailUrl, // S3缩略图URL
|
||
original_url: originalUrl, // S3原图URL
|
||
processing_time: finalProcessingTime,
|
||
});
|
||
} catch (error) {
|
||
console.error("[Enhance API] 处理错误:", error);
|
||
return NextResponse.json(
|
||
{ error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
|