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"; // 变清晰API配置类型 type EnhanceApiConfig = { 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 { 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 { 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 pollTaskResult( apiUrl: string, apiKey: string, taskId: string, delay: number = 1000, timeout: number = 120000 // 增加到120秒(2分钟) ): Promise { const pollUrl = `${apiUrl}/api/tasks/visual/scale/${taskId}`; const getTaskResult = async (): Promise => { 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.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; // 检查是否使用备用API const useBackupApi = config.useBackupApi === true; console.log(`[Enhance API] 使用${useBackupApi ? "备用API (Real-ESRGAN)" : "主API"}`); 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; 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; // 优先使用 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 } ); } // ========== 备用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: config.backupScale || "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: `${config.backupScale}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: `${config.backupScale}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 = 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 = { "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((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 } ); } }