1275 lines
44 KiB
TypeScript
1275 lines
44 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
import {
|
||
createMediaAssetRelation,
|
||
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";
|
||
|
||
// 变清晰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 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,
|
||
};
|
||
}
|
||
|
||
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);
|
||
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,
|
||
});
|
||
|
||
// 检查是否是网络连接错误
|
||
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 : "轮询失败";
|
||
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 }
|
||
);
|
||
}
|
||
}
|