1412 lines
48 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 {
createMediaAssetRelation,
getAiProvidersByType,
getMediaAssetsByUserAndUrls,
getSystemSetting,
upsertMediaAsset,
} 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";
import { executeConfiguredVip98kTask } from "@/lib/ai/vip98k-runtime";
import { createFalClient } from "@fal-ai/client";
// 变清晰API配置类型
type EnhanceApiConfig = {
provider?: "vip98k" | "aoscdn";
apiUrl?: string;
apiKey?: string;
type?: string; // "clean" | "face"
scaleFactor?: number | string; // 1 | 2 | 4 | "auto"
// 备用API配置 (Real-ESRGAN)
useBackupApi?: boolean;
backupApiUrl?: string;
backupApiKey?: string;
backupScale?: string; // "2" | "4" | "8"
backupModel?: string;
};
const AOS_MAX_RESOLUTION = 4096;
const ENHANCE_CREATE_TIMEOUT_MS = 120_000;
const ENHANCE_CREATE_RETRY_COUNT = 1;
const ENHANCE_SYNC_WAIT_TIMEOUT_MS = 20_000;
const ENHANCE_TASK_POLL_INTERVAL_MS = 1200;
function getAosFriendlyErrorMessage(detail: string): string | null {
const resolutionMatch = detail.match(/resolution excess,\s*width=(\d+),\s*height=(\d+)/i);
if (resolutionMatch) {
const [, width, height] = resolutionMatch;
return `图片分辨率超限(${width}x${height}),AOS要求宽高都不超过 ${AOS_MAX_RESOLUTION}px。请缩小后重试。`;
}
if (/invalid file/i.test(detail)) {
return "图片文件无法识别,请尝试转换为 JPG/PNG 后重试。";
}
return null;
}
function isRetryableNetworkError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
if (error.name === "AbortError") return true;
const message = error.message || "";
const cause = (error as Error & { cause?: { message?: string; code?: string } }).cause;
const causeMessage = cause?.message || "";
const causeCode = cause?.code || "";
const combined = `${message} ${causeMessage} ${causeCode}`;
return /EHOSTUNREACH|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|No route to host/i.test(combined);
}
async function ensureAosResolutionLimit(
buffer: Buffer,
logPrefix: string
): Promise<Buffer> {
try {
const sharp = await import("sharp");
const image = sharp.default(buffer);
const metadata = await image.metadata();
const width = metadata.width;
const height = metadata.height;
if (!width || !height) {
return buffer;
}
if (width <= AOS_MAX_RESOLUTION && height <= AOS_MAX_RESOLUTION) {
return buffer;
}
const resizedBuffer = await image
.resize(AOS_MAX_RESOLUTION, AOS_MAX_RESOLUTION, {
fit: "inside",
withoutEnlargement: true,
})
.toBuffer();
const resizedMeta = await sharp.default(resizedBuffer).metadata();
console.warn(
`[${logPrefix}] 输入图片分辨率超限,已自动缩放: ${width}x${height} -> ${resizedMeta.width}x${resizedMeta.height}`
);
return resizedBuffer;
} catch (error) {
console.error(`[${logPrefix}] 自动缩放失败,继续使用原图:`, error);
return buffer;
}
}
/**
* 压缩图片生成缩略图(使用 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;
/**
* 使用Real-ESRGAN备用API处理图片(通过URL方式)
* @param imageUrl 图片URL(需要公开可访问)
* @param config 备用API配置
* @returns 处理后的图片Base64和S3 URL
*/
async function processWithRealESRGAN(
imageUrl: string,
config: {
backupApiUrl: string;
backupApiKey: string;
backupScale: string;
backupModel: string;
}
): Promise<{ imageBase64: string; imageUrl: string; processingTime: number }> {
const startTime = Date.now();
const apiUrl = config.backupApiUrl.endsWith("/")
? config.backupApiUrl.slice(0, -1)
: config.backupApiUrl;
const upscaleUrl = `${apiUrl}/upscale/url`;
console.log(`[Enhance-RealESRGAN] 调用URL方式API: ${upscaleUrl}`);
console.log(`[Enhance-RealESRGAN] 图片URL: ${imageUrl}`);
console.log(`[Enhance-RealESRGAN] 放大倍数: ${config.backupScale}x, 模型: ${config.backupModel}`);
const response = await fetch(upscaleUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": config.backupApiKey,
},
body: JSON.stringify({
image_url: imageUrl,
scale: Number.parseInt(config.backupScale) || 4,
model_name: config.backupModel || "RealESRGAN_x4plus",
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[Enhance-RealESRGAN] API请求失败: ${response.status} ${response.statusText}`);
console.error(`[Enhance-RealESRGAN] 错误详情: ${errorText}`);
throw new Error(`Real-ESRGAN API请求失败: ${response.status} - ${errorText || response.statusText}`);
}
const result = await response.json();
console.log(`[Enhance-RealESRGAN] API响应:`, {
success: result.success,
hasImageUrl: !!result.image_url,
processingTime: result.processing_time,
totalTime: result.total_time,
});
if (!result.success || !result.image_url) {
throw new Error(`Real-ESRGAN处理失败: ${JSON.stringify(result)}`);
}
// Real-ESRGAN API直接返回S3 URL,我们需要下载图片并转换为Base64
console.log(`[Enhance-RealESRGAN] 下载处理后的图片: ${result.image_url}`);
const imageResponse = await fetch(result.image_url);
if (!imageResponse.ok) {
throw new Error(`无法下载Real-ESRGAN处理后的图片: ${imageResponse.status}`);
}
const imageBuffer = await imageResponse.arrayBuffer();
const imageBase64 = Buffer.from(imageBuffer).toString("base64");
const processingTime = (Date.now() - startTime) / 1000;
console.log(`[Enhance-RealESRGAN] ✅ 处理完成,总耗时: ${processingTime.toFixed(2)}秒`);
console.log(`[Enhance-RealESRGAN] ✅ 处理后图片大小: ${imageBuffer.byteLength} bytes`);
return {
imageBase64,
imageUrl: result.image_url,
processingTime,
};
}
/**
* 使用Real-ESRGAN备用API处理图片(通过Base64方式,用于没有公开URL的图片)
* @param imageBase64 图片Base64数据
* @param config 备用API配置
* @returns 处理后的图片Base64
*/
async function processWithRealESRGANBase64(
imageBase64: string,
config: {
backupApiUrl: string;
backupApiKey: string;
backupScale: string;
backupModel: string;
}
): Promise<{ imageBase64: string; processingTime: number }> {
const startTime = Date.now();
const apiUrl = config.backupApiUrl.endsWith("/")
? config.backupApiUrl.slice(0, -1)
: config.backupApiUrl;
const upscaleUrl = `${apiUrl}/upscale/base64`;
console.log(`[Enhance-RealESRGAN] 调用Base64方式API: ${upscaleUrl}`);
console.log(`[Enhance-RealESRGAN] 放大倍数: ${config.backupScale}x, 模型: ${config.backupModel}`);
console.log(`[Enhance-RealESRGAN] 输入图片Base64长度: ${imageBase64.length}`);
const response = await fetch(upscaleUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": config.backupApiKey,
},
body: JSON.stringify({
image_base64: imageBase64,
scale: Number.parseInt(config.backupScale) || 4,
model_name: config.backupModel || "RealESRGAN_x4plus",
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[Enhance-RealESRGAN] API请求失败: ${response.status} ${response.statusText}`);
console.error(`[Enhance-RealESRGAN] 错误详情: ${errorText}`);
throw new Error(`Real-ESRGAN API请求失败: ${response.status} - ${errorText || response.statusText}`);
}
const result = await response.json();
console.log(`[Enhance-RealESRGAN] API响应:`, {
success: result.success,
hasImageBase64: !!result.image_base64,
processingTime: result.processing_time,
});
if (!result.success || !result.image_base64) {
throw new Error(`Real-ESRGAN处理失败: ${JSON.stringify(result)}`);
}
const processingTime = (Date.now() - startTime) / 1000;
console.log(`[Enhance-RealESRGAN] ✅ 处理完成,总耗时: ${processingTime.toFixed(2)}秒`);
console.log(`[Enhance-RealESRGAN] ✅ 处理后图片Base64长度: ${result.image_base64.length}`);
return {
imageBase64: result.image_base64,
processingTime,
};
}
async function processWithFalEsrgan(
imageBase64: string,
requestedScaleFactor: number,
): Promise<{ imageBase64: string; processingTime: number }> {
const providers = await getAiProvidersByType({
type: "fal-image",
enabledOnly: true,
});
const provider = providers.find((item) => item.apiKey?.trim());
if (!provider?.apiKey) {
throw new Error("未配置可用的 FAL 专用高清服务");
}
const scale = Math.max(2, Math.min(8, requestedScaleFactor || 4));
const model = scale <= 2 ? "RealESRGAN_x2plus" : "RealESRGAN_x4plus";
const credentials = provider.apiKey
.trim()
.replace(/^(?:Key|Bearer)\s+/i, "");
const fal = createFalClient({ credentials });
const startedAt = Date.now();
const result: any = await fal.subscribe("fal-ai/esrgan", {
input: {
image_url: `data:image/jpeg;base64,${imageBase64}`,
scale,
model,
output_format: "png",
},
logs: true,
timeout: 180_000,
});
const outputUrl = result?.data?.image?.url || result?.image?.url;
if (!outputUrl) {
throw new Error("FAL ESRGAN 专用高清服务未返回图片");
}
const outputResponse = await fetch(outputUrl, {
cache: "no-store",
signal: AbortSignal.timeout(120_000),
});
if (!outputResponse.ok) {
throw new Error(
`FAL ESRGAN 高清结果下载失败(HTTP ${outputResponse.status})`,
);
}
return {
imageBase64: Buffer.from(await outputResponse.arrayBuffer()).toString(
"base64",
),
processingTime: (Date.now() - startedAt) / 1000,
};
}
async function processWithVip98kEnhance(params: {
userId: string;
imageBase64: string;
prompt: string;
chatId: string | null;
messageId: string | null;
sourceImageUrl: string | null;
scaleFactor: number;
}) {
const startedAt = Date.now();
const result = await executeConfiguredVip98kTask({
modelId: "enhance",
kind: "image-tool",
prompt: `${params.prompt || "Enhance image clarity"}. Upscale ${params.scaleFactor}x while preserving composition, color and texture.`,
imageUrls: [`data:image/jpeg;base64,${params.imageBase64}`],
imageParams: { scaleFactor: params.scaleFactor },
});
const outputUrl = result.outputUrls[0];
if (!outputUrl) throw new Error("VIP98K 变清晰未返回图片");
const response = await fetch(outputUrl, {
cache: "no-store",
signal: AbortSignal.timeout(120_000),
});
if (!response.ok) {
throw new Error(`VIP98K 变清晰结果下载失败(HTTP ${response.status})`);
}
const outputBuffer = Buffer.from(await response.arrayBuffer());
const outputBase64 = outputBuffer.toString("base64");
const uploaded = await uploadBase64ToS3(outputBase64, "image/png");
const [savedImage] = await db
.insert(generatedImage)
.values({
userId: params.userId,
chatId: params.chatId,
messageId: params.messageId,
thumbnailUrl: uploaded.thumbnail,
originalUrl: uploaded.original,
imageSize: `${params.scaleFactor}x放大`,
prompt: params.prompt,
modelId: "enhance",
})
.returning();
try {
const sourceAssets = params.sourceImageUrl
? await getMediaAssetsByUserAndUrls({
userId: params.userId,
urls: [params.sourceImageUrl],
})
: [];
const media = await upsertMediaAsset({
userId: params.userId,
chatId: params.chatId,
mediaType: "image",
sourceType: "edited",
status: "ready",
originalUrl: uploaded.original,
previewUrl: uploaded.original,
thumbnailUrl: uploaded.thumbnail,
displayName: params.prompt.slice(0, 80),
prompt: params.prompt,
modelId: "enhance",
sourceMessageId: params.messageId,
sourceTable: "GeneratedImage",
sourceRowId: savedImage?.id || null,
primaryParentAssetId: sourceAssets[0]?.id ?? null,
metadata: { provider: "vip98k", scaleFactor: params.scaleFactor },
});
if (media) {
for (const sourceAsset of sourceAssets) {
await createMediaAssetRelation({
userId: params.userId,
chatId: params.chatId,
fromAssetId: sourceAsset.id,
toAssetId: media.id,
relationType: "edited_from",
sourceMessageId: params.messageId,
metadata: { prompt: params.prompt, provider: "vip98k" },
});
}
}
} catch (error) {
console.warn("[Enhance-VIP98K] MediaAsset registration failed", error);
}
return {
success: true,
image_base64: outputBase64,
thumbnail_url: uploaded.thumbnail,
original_url: uploaded.original,
processing_time: (Date.now() - startedAt) / 1000,
api_type: "vip98k",
};
}
/**
* 轮询获取任务结果(根据官方示例)
*/
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) {
const errMessage = String(
result.data?.err_message || result.data?.err_info || result.data?.state_detail || `state=${state}`
);
throw new Error(`AOS任务失败: ${errMessage}`);
}
// 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);
}
}
function isEnhanceTaskStillProcessing(error: unknown): boolean {
if (!(error instanceof Error)) return false;
return /轮询超时/i.test(error.message || "");
}
function buildEnhanceTaskPendingResponse(taskId: string) {
return NextResponse.json(
{
success: true,
processing: true,
status: "running",
taskId,
pollAfterMs: ENHANCE_TASK_POLL_INTERVAL_MS,
},
{ status: 202 }
);
}
async function finalizeEnhanceTaskResult(params: {
userId: string;
taskResult: any;
prompt?: string;
chatId?: string | null;
messageId?: string | null;
startedAt?: number;
sourceImageUrls?: string[];
}) {
if (params.taskResult?.status !== 200 || !params.taskResult?.data) {
throw new Error(params.taskResult?.message || "未知错误");
}
let enhancedImageBase64: string | null = null;
if (typeof params.taskResult.data.image_base64 === "string" && params.taskResult.data.image_base64) {
enhancedImageBase64 = params.taskResult.data.image_base64;
} else if (typeof params.taskResult.data.image === "string" && params.taskResult.data.image) {
const imageData = params.taskResult.data.image;
if (/^https?:\/\//i.test(imageData)) {
const imageResponse = await fetch(imageData);
if (!imageResponse.ok) {
throw new Error(`无法下载处理后的图片: ${imageResponse.status}`);
}
const downloadedBuffer = await imageResponse.arrayBuffer();
enhancedImageBase64 = Buffer.from(downloadedBuffer).toString("base64");
} else {
enhancedImageBase64 = imageData;
}
}
if (!enhancedImageBase64) {
throw new Error("轮询结果未包含图片数据");
}
const uploadResult = await uploadBase64ToS3(enhancedImageBase64, "image/jpeg");
const finalPrompt = params.prompt?.trim() || "图片变清晰处理";
let saved = false;
let saveError: unknown = null;
let savedImage:
| {
id: string;
}
| null = null;
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
([savedImage] = await db.insert(generatedImage).values({
userId: params.userId,
chatId: params.chatId || null,
messageId: params.messageId || null,
thumbnailUrl: uploadResult.thumbnail,
originalUrl: uploadResult.original,
imageSize: "2K",
prompt: finalPrompt,
modelId: "enhance-1.0",
}).returning());
saved = true;
break;
} catch (dbError) {
saveError = dbError;
if (attempt < 3) {
await new Promise((resolve) => setTimeout(resolve, 120 * attempt));
}
}
}
if (!saved) {
console.error("[Enhance] 保存到相册失败:", saveError);
throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
}
if (savedImage) {
try {
const sourceAssets =
Array.isArray(params.sourceImageUrls) && params.sourceImageUrls.length > 0
? await getMediaAssetsByUserAndUrls({
userId: params.userId,
urls: params.sourceImageUrls,
})
: [];
const media = await upsertMediaAsset({
userId: params.userId,
chatId: params.chatId || null,
mediaType: "image",
sourceType: "edited",
status: "ready",
originalUrl: uploadResult.original,
previewUrl: uploadResult.original,
thumbnailUrl: uploadResult.thumbnail,
displayName: finalPrompt.slice(0, 80),
prompt: finalPrompt,
modelId: "enhance-1.0",
sourceMessageId: params.messageId || null,
sourceTable: "GeneratedImage",
sourceRowId: savedImage.id,
primaryParentAssetId: sourceAssets[0]?.id ?? null,
metadata: {
imageSize: "2K",
},
});
if (media) {
for (const sourceAsset of sourceAssets) {
await createMediaAssetRelation({
userId: params.userId,
chatId: params.chatId || null,
fromAssetId: sourceAsset.id,
toAssetId: media.id,
relationType: "edited_from",
sourceMessageId: params.messageId || null,
metadata: {
prompt: finalPrompt,
},
});
}
}
} catch (assetError) {
console.error("[Enhance] ⚠️ MediaAsset 注册失败:", assetError);
}
}
const processingTime =
typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined;
return {
success: true,
image_base64: enhancedImageBase64,
thumbnail_url: uploadResult.thumbnail,
original_url: uploadResult.original,
processing_time: processingTime,
};
}
async function executeFalEsrganFallback(params: {
userId: string;
imageBase64: string;
prompt: string;
chatId: string | null;
messageId: string | null;
sourceImageUrl: string | null;
requestedScaleFactor?: number;
reason: string;
}) {
const scaleFactor = params.requestedScaleFactor || 4;
console.warn("[Enhance-FAL-ESRGAN] 启用专用高清备用链路", {
reason: params.reason.slice(0, 500),
scaleFactor,
});
const result = await processWithFalEsrgan(params.imageBase64, scaleFactor);
const payload = await finalizeEnhanceTaskResult({
userId: params.userId,
taskResult: {
status: 200,
data: { image_base64: result.imageBase64 },
},
prompt: params.prompt || "图片变清晰处理",
chatId: params.chatId,
messageId: params.messageId,
sourceImageUrls: params.sourceImageUrl
? [params.sourceImageUrl]
: undefined,
});
return {
...payload,
processing_time: result.processingTime,
api_type: "fal-esrgan",
};
}
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "未授权,请先登录" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const taskId = String(searchParams.get("taskId") || "").trim();
if (!taskId) {
return NextResponse.json({ error: "缺少 taskId 参数" }, { status: 400 });
}
const prompt = searchParams.get("prompt") || "图片变清晰处理";
const chatId = searchParams.get("chatId");
const messageId = searchParams.get("messageId");
const sourceImageUrl = searchParams.get("sourceImageUrl");
const enhanceConfig = await getSystemSetting({ key: "enhance_api_config" });
if (!enhanceConfig?.value) {
return NextResponse.json(
{ error: "变清晰API配置未设置,请在系统设置中配置" },
{ status: 400 }
);
}
const config = enhanceConfig.value as EnhanceApiConfig;
if (config.provider === "vip98k") {
return NextResponse.json(
{ error: "VIP98K 变清晰任务在 POST 请求内完成,无需 taskId 轮询" },
{ status: 400 }
);
}
if (config.useBackupApi) {
return NextResponse.json(
{ error: "当前启用了Real-ESRGAN备用模式,不支持 taskId 轮询" },
{ status: 400 }
);
}
if (!config.apiUrl || !config.apiKey) {
return NextResponse.json(
{ error: "变清晰API地址或密钥未配置" },
{ status: 400 }
);
}
const apiUrl = config.apiUrl.endsWith("/") ? config.apiUrl.slice(0, -1) : config.apiUrl;
let taskResult: any;
try {
taskResult = await pollTaskResult(
apiUrl,
config.apiKey,
taskId,
ENHANCE_TASK_POLL_INTERVAL_MS,
ENHANCE_SYNC_WAIT_TIMEOUT_MS
);
} catch (pollError) {
if (isEnhanceTaskStillProcessing(pollError)) {
return buildEnhanceTaskPendingResponse(taskId);
}
const detail = pollError instanceof Error ? pollError.message : "轮询失败";
return NextResponse.json(
{ error: "变清晰处理失败", detail },
{ status: 500 }
);
}
const responsePayload = await finalizeEnhanceTaskResult({
userId: session.user.id,
taskResult,
prompt,
chatId,
messageId,
sourceImageUrls: sourceImageUrl ? [sourceImageUrl] : undefined,
});
return NextResponse.json(responsePayload);
} catch (error) {
console.error("[Enhance API][TaskStatus] 处理错误:", error);
return NextResponse.json(
{ error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
{ status: 500 }
);
}
}
/**
* 图片变清晰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 EnhanceApiConfig;
const useVip98k = config.provider === "vip98k";
// 检查是否使用备用API
const useBackupApi = !useVip98k && config.useBackupApi === true;
console.log(`[Enhance API] 使用${useBackupApi ? "备用API (Real-ESRGAN)" : "主API"}`);
if (useVip98k) {
// VIP98K 密钥只从 AiProvider 读取。
} else if (useBackupApi) {
// 验证备用API配置
if (!config.backupApiUrl || !config.backupApiKey) {
return NextResponse.json(
{ error: "备用变清晰API地址或密钥未配置" },
{ status: 400 }
);
}
} else {
// 验证主API配置
if (!config.apiUrl || !config.apiKey) {
return NextResponse.json(
{ error: "变清晰API地址或密钥未配置" },
{ status: 400 }
);
}
}
// 2. 获取请求体(支持 imageUrl、image_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(可选)
let sourceImageUrl: string | null = null;
let requestedScaleFactor: number | undefined;
if (contentType.includes("application/json")) {
console.log("[Enhance API] 解析JSON请求体...");
const body = await request.json();
prompt = body.prompt || "图片变清晰处理";
chatId = body.chatId || null;
messageId = body.messageId || null;
const parsedScaleFactor = Number.parseInt(String(body.scaleFactor || ""), 10);
requestedScaleFactor =
Number.isFinite(parsedScaleFactor) && [1, 2, 4, 8].includes(parsedScaleFactor)
? parsedScaleFactor
: undefined;
// 优先使用 imageUrl,其次使用 image_base64
if (body.imageUrl) {
sourceImageUrl = String(body.imageUrl);
// 从 URL 获取图片并转换为 Base64
console.log(`[Enhance API] 从 URL 获取图片: ${body.imageUrl}`);
try {
const imageResponse = await fetch(body.imageUrl);
if (!imageResponse.ok) {
return NextResponse.json(
{ error: `无法获取图片: ${imageResponse.status} ${imageResponse.statusText}` },
{ status: 400 }
);
}
const arrayBuffer = await imageResponse.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
imageBase64 = buffer.toString("base64");
console.log(`[Enhance API] 图片已转换为 Base64,大小: ${buffer.length} bytes`);
} catch (fetchError) {
console.error("[Enhance API] 获取图片失败:", fetchError);
return NextResponse.json(
{ error: "无法获取图片,请检查图片链接是否有效" },
{ status: 400 }
);
}
} else if (body.image_base64) {
console.log("[Enhance API] JSON请求体解析完成,image_base64长度:", body.image_base64?.length || 0);
imageBase64 = body.image_base64;
} else {
return NextResponse.json(
{ error: "缺少 imageUrl 或 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 }
);
}
if (useVip98k) {
try {
const scaleFactor = requestedScaleFactor || 4;
const payload = await processWithVip98kEnhance({
userId,
imageBase64,
prompt: prompt || "图片变清晰处理",
chatId,
messageId,
sourceImageUrl,
scaleFactor,
});
return NextResponse.json(payload);
} catch (error) {
console.error("[Enhance-VIP98K] Processing failed", error);
return NextResponse.json(
{
error: "VIP98K 变清晰处理失败",
detail: error instanceof Error ? error.message : "未知错误",
},
{ status: 500 }
);
}
}
// ========== 备用API处理路径 (Real-ESRGAN) ==========
if (useBackupApi) {
console.log("[Enhance API] 使用Real-ESRGAN备用API处理(URL方式)...");
try {
// 步骤1: 先将Base64图片上传到S3获取公开可访问的URL
console.log(`[Enhance-RealESRGAN] 📤 步骤1: 上传原图到S3获取URL...`);
const tempUploadResult = await uploadBase64ToS3(imageBase64, "image/jpeg");
const sourceImageUrl = tempUploadResult.original;
console.log(`[Enhance-RealESRGAN] ✅ 原图已上传,URL: ${sourceImageUrl}`);
// 步骤2: 使用URL方式调用Real-ESRGAN API
console.log(`[Enhance-RealESRGAN] 🔄 步骤2: 调用Real-ESRGAN API处理...`);
const result = await processWithRealESRGAN(sourceImageUrl, {
backupApiUrl: config.backupApiUrl!,
backupApiKey: config.backupApiKey!,
backupScale: String(requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4),
backupModel: config.backupModel || "RealESRGAN_x4plus",
});
// 步骤3: Real-ESRGAN API返回的URL就是处理后的图片
// 需要重新上传到我们的S3(因为Real-ESRGAN返回的URL可能在另一个S3桶)
console.log(`[Enhance-RealESRGAN] 📤 步骤3: 上传处理后的图片到S3...`);
let thumbnailUrl: string;
let originalUrl: string;
try {
const uploadResult = await uploadBase64ToS3(result.imageBase64, "image/png");
thumbnailUrl = uploadResult.thumbnail;
originalUrl = uploadResult.original;
console.log(`[Enhance-RealESRGAN] ✅ 处理后图片已上传: ${originalUrl}`);
} catch (uploadError) {
console.error("[Enhance-RealESRGAN] ❌ 上传到S3失败:", uploadError);
// 如果上传失败,直接使用Real-ESRGAN返回的URL
console.log(`[Enhance-RealESRGAN] ⚠️ 使用Real-ESRGAN返回的URL作为备选: ${result.imageUrl}`);
thumbnailUrl = result.imageUrl;
originalUrl = result.imageUrl;
}
// 步骤4: 保存到数据库
let saved = false;
let saveError: unknown = null;
let savedImage:
| {
id: string;
}
| null = null;
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
([savedImage] = await db.insert(generatedImage).values({
userId,
chatId: chatId || null,
messageId: messageId || null,
thumbnailUrl,
originalUrl,
imageSize: `${requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4}x放大`,
prompt: prompt || "Real-ESRGAN图片变清晰处理",
modelId: `realesrgan-${config.backupModel}`,
}).returning());
console.log(`[Enhance-RealESRGAN] ✅ 保存到数据库成功,ID: ${savedImage.id}`);
saved = true;
break;
} catch (dbError) {
saveError = dbError;
if (attempt < 3) {
await new Promise((resolve) => setTimeout(resolve, 120 * attempt));
}
}
}
if (!saved) {
console.error("[Enhance-RealESRGAN] ❌ 保存到数据库失败:", saveError);
throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
}
if (savedImage) {
try {
const sourceAssets =
sourceImageUrl
? await getMediaAssetsByUserAndUrls({
userId,
urls: [sourceImageUrl],
})
: [];
const media = await upsertMediaAsset({
userId,
chatId,
mediaType: "image",
sourceType: "edited",
status: "ready",
originalUrl,
previewUrl: originalUrl,
thumbnailUrl,
displayName: (prompt || "Real-ESRGAN图片变清晰处理").slice(0, 80),
prompt: prompt || "Real-ESRGAN图片变清晰处理",
modelId: `realesrgan-${config.backupModel}`,
sourceMessageId: messageId,
sourceTable: "GeneratedImage",
sourceRowId: savedImage.id,
primaryParentAssetId: sourceAssets[0]?.id ?? null,
metadata: {
imageSize: `${requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4}x放大`,
apiType: "realesrgan-url",
},
});
if (media) {
for (const sourceAsset of sourceAssets) {
await createMediaAssetRelation({
userId,
chatId,
fromAssetId: sourceAsset.id,
toAssetId: media.id,
relationType: "edited_from",
sourceMessageId: messageId,
metadata: {
prompt: prompt || "Real-ESRGAN图片变清晰处理",
},
});
}
}
} catch (assetError) {
console.error("[Enhance-RealESRGAN] ⚠️ MediaAsset 注册失败:", assetError);
}
}
// 返回结果
console.log(`[Enhance-RealESRGAN] ✅ 全部完成,总耗时: ${result.processingTime.toFixed(2)}秒`);
return NextResponse.json({
success: true,
image_base64: result.imageBase64,
thumbnail_url: thumbnailUrl,
original_url: originalUrl,
processing_time: result.processingTime,
api_type: "realesrgan-url",
});
} catch (error) {
console.error("[Enhance-RealESRGAN] ❌ 处理失败:", error);
try {
const fallbackPayload = await executeFalEsrganFallback({
userId,
imageBase64,
prompt,
chatId,
messageId,
sourceImageUrl,
requestedScaleFactor,
reason: error instanceof Error ? error.message : "RealESRGAN 备用服务失败",
});
return NextResponse.json(fallbackPayload);
} catch (falError) {
console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
}
return NextResponse.json(
{
error: "Real-ESRGAN变清晰处理失败",
detail: error instanceof Error ? error.message : "未知错误",
},
{ status: 500 }
);
}
}
// ========== 主API处理路径 ==========
// 3. 调用变清晰API(使用异步模式,根据官方示例)
console.log("[Enhance API] 开始准备主API调用...");
const apiUrl = config.apiUrl!.endsWith("/")
? config.apiUrl!.slice(0, -1)
: config.apiUrl!;
const enhanceUrl = `${apiUrl}/api/tasks/visual/scale`;
console.log("[Enhance API] API URL:", enhanceUrl);
// 将Base64转换为Buffer
console.log("[Enhance API] 转换Base64为Buffer,长度:", imageBase64.length);
const rawImageBuffer = Buffer.from(imageBase64, "base64");
const imageBuffer = await ensureAosResolutionLimit(rawImageBuffer, "Enhance API");
console.log("[Enhance API] Buffer创建完成,大小:", imageBuffer.length, "bytes");
// 创建FormData(使用form-data包,Node.js环境)
console.log("[Enhance API] 创建FormData...");
const FormDataLib = await import("form-data");
const formData = new FormDataLib.default();
console.log("[Enhance API] FormData创建完成");
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 =
requestedScaleFactor ||
(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 {
console.log("[Enhance API] 开始创建任务...");
// 将FormData转换为Buffer(form-data包是stream)
console.log("[Enhance API] 转换FormData为Buffer...");
const formDataBuffer = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
formData.on("data", (chunk: any) => {
// 确保chunk是Buffer类型
if (Buffer.isBuffer(chunk)) {
chunks.push(chunk);
} else if (typeof chunk === "string") {
chunks.push(Buffer.from(chunk, "utf-8"));
} else if (chunk instanceof Uint8Array) {
chunks.push(Buffer.from(chunk));
} else {
console.warn("[Enhance API] 未知的chunk类型:", typeof chunk, chunk);
chunks.push(Buffer.from(String(chunk), "utf-8"));
}
});
formData.on("end", () => {
console.log("[Enhance API] FormData stream结束,chunks数量:", chunks.length);
resolve(Buffer.concat(chunks));
});
formData.on("error", (error) => {
console.error("[Enhance API] FormData stream错误:", error);
reject(error);
});
// 重要:form-data包需要被pipe或手动触发才能开始流式传输
// 使用PassThrough来触发stream
const { PassThrough } = require("stream");
const passThrough = new PassThrough();
passThrough.on("data", () => {}); // 忽略数据,我们已经在formData上监听
formData.pipe(passThrough);
});
console.log("[Enhance API] FormData Buffer转换完成,大小:", formDataBuffer.length, "bytes");
const uint8Array = new Uint8Array(formDataBuffer);
let createResponse: Response | null = null;
let lastFetchError: unknown;
for (let attempt = 1; attempt <= ENHANCE_CREATE_RETRY_COUNT + 1; attempt++) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ENHANCE_CREATE_TIMEOUT_MS);
try {
console.log(`[Enhance API] 发送创建任务请求... attempt=${attempt}`);
createResponse = await fetch(enhanceUrl, {
method: "POST",
headers,
body: uint8Array,
signal: controller.signal,
});
console.log("[Enhance API] 创建任务响应状态:", createResponse.status, createResponse.statusText);
clearTimeout(timeoutId);
break;
} catch (fetchError) {
clearTimeout(timeoutId);
lastFetchError = fetchError;
const canRetry = attempt <= ENHANCE_CREATE_RETRY_COUNT && isRetryableNetworkError(fetchError);
if (canRetry) {
console.warn(
`[Enhance API] 创建任务失败,准备重试: attempt=${attempt}, error=${fetchError instanceof Error ? fetchError.message : String(fetchError)}`
);
await new Promise((resolve) => setTimeout(resolve, 1500));
continue;
}
throw fetchError;
}
}
if (!createResponse) {
throw lastFetchError instanceof Error ? lastFetchError : new Error("创建任务请求失败");
}
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,
});
try {
const fallbackPayload = await executeFalEsrganFallback({
userId,
imageBase64,
prompt,
chatId,
messageId,
sourceImageUrl,
requestedScaleFactor,
reason: fetchError instanceof Error ? fetchError.message : "AOS 高清任务创建失败",
});
return NextResponse.json(fallbackPayload);
} catch (falError) {
console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
}
// 检查是否是网络连接错误
if (fetchError instanceof Error) {
if (fetchError.name === "AbortError") {
return NextResponse.json(
{
error: "变清晰API请求超时",
detail: "创建任务请求超过120秒未响应,请检查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}`);
let taskResult: any;
try {
taskResult = await pollTaskResult(
apiUrl,
config.apiKey!,
taskId,
ENHANCE_TASK_POLL_INTERVAL_MS,
ENHANCE_SYNC_WAIT_TIMEOUT_MS
);
} catch (pollError) {
if (isEnhanceTaskStillProcessing(pollError)) {
return buildEnhanceTaskPendingResponse(taskId);
}
const detail = pollError instanceof Error ? pollError.message : "轮询失败";
try {
const fallbackPayload = await executeFalEsrganFallback({
userId,
imageBase64,
prompt,
chatId,
messageId,
sourceImageUrl,
requestedScaleFactor,
reason: detail,
});
return NextResponse.json(fallbackPayload);
} catch (falError) {
console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
}
const friendlyMessage = getAosFriendlyErrorMessage(detail);
return NextResponse.json(
{ error: "变清晰处理失败", detail: friendlyMessage || detail },
{ status: friendlyMessage ? 400 : 500 }
);
}
const responsePayload = await finalizeEnhanceTaskResult({
userId,
taskResult,
prompt,
chatId,
messageId,
startedAt: startTime,
sourceImageUrls: sourceImageUrl ? [sourceImageUrl] : undefined,
});
return NextResponse.json(responsePayload);
} catch (error) {
console.error("[Enhance API] 处理错误:", error);
return NextResponse.json(
{ error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
{ status: 500 }
);
}
}