Your Name e9cbca3042
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
debug: 添加轮询进度日志,监控任务处理状态
2025-12-29 01:50:18 +08:00

483 lines
17 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(`[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 : "未知错误"}。请检查系统设置中的存储配置。`);
}
}
// 设置API路由最大执行时间为3分钟(180秒)
export const maxDuration = 180;
/**
* 轮询获取任务结果(根据官方示例)
*/
async function pollTaskResult(
apiUrl: string,
apiKey: string,
taskId: string,
delay: number = 1000,
timeout: number = 120000 // 增加到120秒(2分钟)
): Promise<any> {
const pollUrl = `${apiUrl}/api/tasks/visual/scale/${taskId}`;
const getTaskResult = async (): Promise<any> => {
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.data) {
throw result;
}
const { progress, state } = result.data;
console.log(`[Enhance] 轮询进度: progress=${progress}%, state=${state}, task_id=${taskId}`);
// state < 0 表示失败
if (state < 0) {
throw result;
}
// progress >= 100 表示完成
if (progress >= 100) {
console.log(`[Enhance] 任务完成,task_id=${taskId}`);
return result;
}
// 否则继续轮询
throw null;
};
try {
const result = await getTaskResult();
return result;
} catch (error) {
if (error && typeof error === 'object' && 'data' in error) {
throw new Error(JSON.stringify(error, null, 2));
}
if (timeout <= 0) {
throw new Error('轮询超时');
}
await new Promise((resolve) => {
setTimeout(() => {
resolve(undefined);
}, delay);
});
return pollTaskResult(apiUrl, apiKey, taskId, delay, timeout - delay);
}
}
/**
* 图片变清晰API路由
* 从数据库读取变清晰API配置,调用外部API进行图片增强处理
* 将处理后的图片上传到S3并保存到数据库
*/
export async function POST(request: NextRequest) {
console.log("[Enhance API] ===== 收到请求 =====");
try {
// 0. 获取用户会话
console.log("[Enhance API] 开始获取用户会话...");
const session = await auth();
console.log("[Enhance API] 用户会话获取完成:", session?.user?.id ? "已登录" : "未登录");
if (!session?.user?.id) {
return NextResponse.json(
{ error: "未授权,请先登录" },
{ status: 401 }
);
}
const userId = session.user.id;
console.log("[Enhance API] 用户ID:", userId);
// 1. 从数据库获取变清晰API配置
console.log("[Enhance API] 开始获取配置...");
const enhanceConfig = await getSystemSetting({ key: "enhance_api_config" });
console.log("[Enhance API] 配置获取完成:", enhanceConfig ? "已找到" : "未找到");
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)
console.log("[Enhance API] 开始获取请求体...");
const contentType = request.headers.get("content-type") || "";
console.log("[Enhance API] Content-Type:", contentType);
let imageBase64: string;
let prompt = ""; // 用户提示词(可选)
let chatId: string | null = null; // 聊天ID(可选)
let messageId: string | null = null; // 消息ID(可选)
if (contentType.includes("application/json")) {
// JSON格式:Base64图片
console.log("[Enhance API] 解析JSON请求体...");
const body = await request.json();
console.log("[Enhance API] JSON请求体解析完成,image_base64长度:", body.image_base64?.length || 0);
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",
});
// 使用异步模式(sync=0),根据官方示例
formData.append("sync", "0");
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 taskId: string;
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);
console.log("[Enhance API] 发送创建任务请求...");
const createResponse = await fetch(enhanceUrl, {
method: "POST",
headers,
body: uint8Array,
signal: controller.signal,
});
console.log("[Enhance API] 创建任务响应状态:", createResponse.status, createResponse.statusText);
clearTimeout(timeoutId);
if (!createResponse.ok) {
const errorText = await createResponse.text();
throw new Error(`创建任务失败: ${errorText || createResponse.statusText}`);
}
const createResult = await createResponse.json();
if (!createResult.data || !createResult.data.task_id) {
throw new Error(`API未返回task_id: ${JSON.stringify(createResult)}`);
}
taskId = createResult.data.task_id;
console.log(`[Enhance] 任务已创建,task_id: ${taskId}`);
} 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 }
);
}
// 4. 轮询获取任务结果(根据官方示例)
console.log(`[Enhance] 开始轮询任务结果,task_id: ${taskId}`);
const taskResult = await pollTaskResult(apiUrl, config.apiKey, taskId);
const finalProcessingTime = (Date.now() - startTime) / 1000;
// 5. 处理返回结果
if (taskResult.status !== 200 || !taskResult.data) {
return NextResponse.json(
{ error: "变清晰处理失败", detail: taskResult.message || "未知错误" },
{ status: 500 }
);
}
let enhancedImageBase64: string;
if (taskResult.data.image_base64) {
// 返回的是base64
enhancedImageBase64 = taskResult.data.image_base64;
} else if (taskResult.data.image) {
// 返回的是URL,需要下载
const imageResponse = await fetch(taskResult.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: "轮询结果未包含图片数据" },
{ 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 }
);
}
}