import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { consumeUserPoints, createMediaAssetRelation, getPointsSystemConfig, getMediaAssetsByUserAndUrls, getSystemSetting, getUserById, upsertMediaAsset, } from "@/lib/db/queries"; import { generatedImage } from "@/lib/db/schema"; import { ENHANCE_PRO_KREA_SETTING_KEY, normalizeEnhanceProKreaConfig, type EnhanceProKreaConfig, type EnhanceProKreaModel, type EnhanceProOutputFormat, type EnhanceProSubjectDetection, } from "@/lib/enhance-pro/config"; import { calculateEnhanceProPointsEstimate } from "@/lib/points/config"; import { uploadFile } from "@/lib/storage/service"; const ENHANCE_PRO_CREATE_TIMEOUT_MS = 120_000; const ENHANCE_PRO_SYNC_WAIT_TIMEOUT_MS = 20_000; const ENHANCE_PRO_TASK_POLL_INTERVAL_MS = 1_500; const ENHANCE_PRO_MODEL_ID = "krea-topaz-generative"; const ENHANCE_PRO_MAX_RESULT_IMAGES = 4; type EnhanceProRequestBody = { imageUrl?: string; image_base64?: string; mimeType?: string; prompt?: string; chatId?: string | null; messageId?: string | null; width?: number; height?: number; batchSize?: number; seed?: number; output_format?: EnhanceProOutputFormat; subject_detection?: EnhanceProSubjectDetection; face_enhancement?: boolean; face_enhancement_creativity?: number; face_enhancement_strength?: number; crop_to_fill?: boolean; model?: EnhanceProKreaModel; upscaling_activated?: boolean; image_scaling_factor?: number; creativity?: number; texture?: number; sharpen?: number; denoise?: number; detail?: number; }; type FinalizedImageResult = { thumbnailUrl: string; originalUrl: string; width?: number; height?: number; }; function clampNumber(value: unknown, min: number, max: number, fallback: number) { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.min(max, Math.max(min, parsed)); } function clampInteger(value: unknown, min: number, max: number, fallback: number) { return Math.round(clampNumber(value, min, max, fallback)); } function normalizeBoolean(value: unknown, fallback = false) { if (typeof value === "boolean") return value; if (value === "true") return true; if (value === "false") return false; return fallback; } function normalizeJobStatus(value: unknown) { return String(value || "").trim().toLowerCase(); } function buildEnhanceProTaskPendingResponse(taskId: string) { return NextResponse.json( { success: true, processing: true, status: "running", taskId, pollAfterMs: ENHANCE_PRO_TASK_POLL_INTERVAL_MS, }, { status: 202 } ); } function normalizeBaseUrl(baseUrl: string) { return String(baseUrl || "").trim().replace(/\/+$/, ""); } function sanitizeDimension(value: unknown, fallback: number) { return clampInteger(value, 1, 32000, fallback); } function clampDimensions(width: number, height: number, maxEdge: number) { const safeWidth = sanitizeDimension(width, 1024); const safeHeight = sanitizeDimension(height, 1024); const longestEdge = Math.max(safeWidth, safeHeight); if (longestEdge <= maxEdge) { return { width: safeWidth, height: safeHeight }; } const scale = maxEdge / longestEdge; return { width: Math.max(1, Math.round(safeWidth * scale)), height: Math.max(1, Math.round(safeHeight * scale)), }; } async function createThumbnail(buffer: Buffer): Promise { try { const sharp = await import("sharp"); return await sharp.default(buffer) .resize(500, 500, { fit: "inside", withoutEnlargement: true, }) .jpeg({ quality: 82 }) .toBuffer(); } catch (error) { console.error("[Enhance Pro] 生成缩略图失败,回退使用原图:", error); return buffer; } } async function uploadBufferPairToStorage( buffer: Buffer, mimeType = "image/jpeg", prefix = "enhance-pro", ): Promise<{ thumbnail: string; original: string }> { const ext = mimeType.split("/")[1] || "jpg"; const timestamp = Date.now(); const randomStr = Math.random().toString(36).slice(2, 8); const originalFilename = `${prefix}-orig-${timestamp}-${randomStr}.${ext}`; const thumbnailFilename = `${prefix}-thumb-${timestamp}-${randomStr}.jpg`; const thumbnailBuffer = await createThumbnail(buffer); const [thumbnailResult, originalResult] = await Promise.all([ uploadFile( thumbnailFilename, thumbnailBuffer.buffer.slice( thumbnailBuffer.byteOffset, thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength, ) as ArrayBuffer, ), uploadFile( originalFilename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer, ), ]); return { thumbnail: thumbnailResult.url, original: originalResult.url, }; } async function getImageDimensionsFromBuffer(buffer: Buffer) { try { const sharp = await import("sharp"); const metadata = await sharp.default(buffer).metadata(); return { width: metadata.width || 1024, height: metadata.height || 1024, }; } catch (error) { console.error("[Enhance Pro] 获取图片尺寸失败,回退默认值:", error); return { width: 1024, height: 1024, }; } } async function fetchBufferFromUrl(url: string) { const response = await fetch(url); if (!response.ok) { throw new Error(`无法下载图片资源: ${response.status} ${response.statusText}`); } const mimeType = response.headers.get("content-type") || "image/jpeg"; const arrayBuffer = await response.arrayBuffer(); return { buffer: Buffer.from(arrayBuffer), mimeType, }; } async function resolveSourceInput(body: EnhanceProRequestBody) { const imageUrl = String(body.imageUrl || "").trim(); const imageBase64 = String(body.image_base64 || "").trim(); const requestedMimeType = String(body.mimeType || "").trim() || "image/jpeg"; if (!imageUrl && !imageBase64) { throw new Error("缺少 imageUrl 或 image_base64 参数"); } if (imageUrl) { const hasRequestedDimensions = Number.isFinite(Number(body.width)) && Number.isFinite(Number(body.height)); if (hasRequestedDimensions) { return { sourceImageUrl: imageUrl, width: sanitizeDimension(body.width, 1024), height: sanitizeDimension(body.height, 1024), }; } const downloaded = await fetchBufferFromUrl(imageUrl); const dimensions = await getImageDimensionsFromBuffer(downloaded.buffer); return { sourceImageUrl: imageUrl, width: dimensions.width, height: dimensions.height, }; } const sourceBuffer = Buffer.from(imageBase64, "base64"); const stored = await uploadBufferPairToStorage(sourceBuffer, requestedMimeType, "enhance-pro-source"); const dimensions = await getImageDimensionsFromBuffer(sourceBuffer); return { sourceImageUrl: stored.original, width: dimensions.width, height: dimensions.height, }; } async function createEnhanceProJob(params: { config: EnhanceProKreaConfig; requestBody: EnhanceProRequestBody; sourceImageUrl: string; width: number; height: number; }) { const apiUrl = normalizeBaseUrl(params.config.baseUrl); const requestPayload = { width: params.width, height: params.height, image_url: params.sourceImageUrl, batchSize: params.requestBody.batchSize, seed: params.requestBody.seed, prompt: String(params.requestBody.prompt || "").trim(), output_format: params.requestBody.output_format || params.config.defaultOutputFormat, subject_detection: params.requestBody.subject_detection || "All", face_enhancement: normalizeBoolean(params.requestBody.face_enhancement, false), face_enhancement_creativity: clampNumber( params.requestBody.face_enhancement_creativity, 0, 1, 0.5, ), face_enhancement_strength: clampNumber( params.requestBody.face_enhancement_strength, 0, 1, 0.5, ), crop_to_fill: normalizeBoolean(params.requestBody.crop_to_fill, false), model: params.requestBody.model || params.config.defaultModel, upscaling_activated: normalizeBoolean(params.requestBody.upscaling_activated, false), image_scaling_factor: clampNumber(params.requestBody.image_scaling_factor, 1, 32, 1), creativity: clampInteger(params.requestBody.creativity, 1, 6, 3), texture: clampInteger(params.requestBody.texture, 1, 5, 3), sharpen: clampNumber(params.requestBody.sharpen, 0, 1, 0.5), denoise: clampNumber(params.requestBody.denoise, 0, 1, 0.5), detail: clampNumber(params.requestBody.detail, 0, 1, 0.5), }; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), ENHANCE_PRO_CREATE_TIMEOUT_MS); try { const response = await fetch(`${apiUrl}/generate/enhance/topaz/generative-enhance`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${params.config.apiKey}`, }, body: JSON.stringify(requestPayload), signal: controller.signal, }); if (!response.ok) { const detail = await response.text(); throw new Error(`创建任务失败: ${response.status} ${detail || response.statusText}`); } return await response.json(); } finally { clearTimeout(timeoutId); } } async function pollEnhanceProJob( config: EnhanceProKreaConfig, jobId: string, delay = ENHANCE_PRO_TASK_POLL_INTERVAL_MS, timeout = ENHANCE_PRO_SYNC_WAIT_TIMEOUT_MS, ): Promise { const apiUrl = normalizeBaseUrl(config.baseUrl); const response = await fetch(`${apiUrl}/jobs/${jobId}`, { method: "GET", headers: { Authorization: `Bearer ${config.apiKey}`, }, cache: "no-store", }); if (!response.ok) { const detail = await response.text(); throw new Error(`查询任务状态失败: ${response.status} ${detail || response.statusText}`); } const payload = await response.json(); const status = normalizeJobStatus(payload?.status); if (status === "completed") { return payload; } if (status === "failed" || status === "cancelled") { throw new Error(`Krea 任务${status === "failed" ? "失败" : "已取消"}`); } if (timeout <= 0) { throw new Error("轮询超时"); } await new Promise((resolve) => setTimeout(resolve, delay)); return pollEnhanceProJob(config, jobId, delay, timeout - delay); } function isEnhanceProTaskStillProcessing(error: unknown) { return error instanceof Error && /轮询超时/i.test(error.message || ""); } async function saveGeneratedImageRecord(params: { userId: string; chatId?: string | null; messageId?: string | null; prompt: string; originalUrl: string; thumbnailUrl: string; imageSize: string; }) { const [savedImage] = await db .insert(generatedImage) .values({ userId: params.userId, chatId: params.chatId || null, messageId: params.messageId || null, thumbnailUrl: params.thumbnailUrl, originalUrl: params.originalUrl, imageSize: params.imageSize, prompt: params.prompt, modelId: ENHANCE_PRO_MODEL_ID, }) .returning(); return savedImage; } async function finalizeEnhanceProTaskResult(params: { userId: string; taskResult: any; prompt?: string; chatId?: string | null; messageId?: string | null; startedAt?: number; sourceImageUrls?: string[]; width: number; height: number; }) { const resultUrls = Array.isArray(params.taskResult?.result?.urls) ? params.taskResult.result.urls.filter((url: unknown) => typeof url === "string" && url) : []; if (resultUrls.length === 0) { throw new Error("Krea 任务未返回结果图片"); } const limitedUrls = resultUrls.slice(0, ENHANCE_PRO_MAX_RESULT_IMAGES); const finalPrompt = String(params.prompt || "").trim() || "图像增强 Pro"; const imageSize = `${params.width}x${params.height}`; const sourceAssets = Array.isArray(params.sourceImageUrls) && params.sourceImageUrls.length > 0 ? await getMediaAssetsByUserAndUrls({ userId: params.userId, urls: params.sourceImageUrls, }) : []; const finalizedImages: FinalizedImageResult[] = []; for (const url of limitedUrls) { const downloaded = await fetchBufferFromUrl(url); const uploaded = await uploadBufferPairToStorage(downloaded.buffer, downloaded.mimeType, "enhance-pro"); const savedImage = await saveGeneratedImageRecord({ userId: params.userId, chatId: params.chatId, messageId: params.messageId, prompt: finalPrompt, originalUrl: uploaded.original, thumbnailUrl: uploaded.thumbnail, imageSize, }); try { const media = await upsertMediaAsset({ userId: params.userId, chatId: params.chatId || null, mediaType: "image", sourceType: "edited", status: "ready", originalUrl: uploaded.original, previewUrl: uploaded.original, thumbnailUrl: uploaded.thumbnail, displayName: finalPrompt.slice(0, 80), prompt: finalPrompt, modelId: ENHANCE_PRO_MODEL_ID, sourceMessageId: params.messageId || null, sourceTable: "GeneratedImage", sourceRowId: savedImage.id, primaryParentAssetId: sourceAssets[0]?.id ?? null, metadata: { imageSize, provider: "krea-topaz-generative", }, }); 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 Pro] MediaAsset 注册失败:", assetError); } finalizedImages.push({ thumbnailUrl: uploaded.thumbnail, originalUrl: uploaded.original, width: params.width, height: params.height, }); } const processingTime = typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined; return { success: true, images: finalizedImages.map((image, index) => ({ index, thumbnail_url: image.thumbnailUrl, original_url: image.originalUrl, width: image.width, height: image.height, })), thumbnail_url: finalizedImages[0]?.thumbnailUrl || null, original_url: finalizedImages[0]?.originalUrl || null, processing_time: processingTime, model_id: ENHANCE_PRO_MODEL_ID, }; } async function loadEnhanceProConfig() { const setting = await getSystemSetting({ key: ENHANCE_PRO_KREA_SETTING_KEY }); const existingValue = setting?.value && typeof setting.value === "object" ? (setting.value as Record) : {}; return normalizeEnhanceProKreaConfig(setting?.value, String(existingValue.apiKey || "")); } export const maxDuration = 180; 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 config = await loadEnhanceProConfig(); if (!config.enabled) { return NextResponse.json({ error: "图像增强 Pro 功能未开启" }, { status: 403 }); } if (!config.apiKey || !config.baseUrl) { return NextResponse.json({ error: "图像增强 Pro API 尚未配置" }, { status: 400 }); } let taskResult: any; try { taskResult = await pollEnhanceProJob(config, taskId); } catch (pollError) { if (isEnhanceProTaskStillProcessing(pollError)) { return buildEnhanceProTaskPendingResponse(taskId); } return NextResponse.json( { error: "图像增强 Pro 处理失败", detail: pollError instanceof Error ? pollError.message : "轮询失败", }, { status: 500 }, ); } const width = sanitizeDimension(searchParams.get("width"), 1024); const height = sanitizeDimension(searchParams.get("height"), 1024); const responsePayload = await finalizeEnhanceProTaskResult({ userId: session.user.id, taskResult, prompt: searchParams.get("prompt") || "图像增强 Pro", chatId: searchParams.get("chatId"), messageId: searchParams.get("messageId"), sourceImageUrls: searchParams.get("sourceImageUrl") ? [String(searchParams.get("sourceImageUrl"))] : undefined, width, height, }); return NextResponse.json(responsePayload); } catch (error) { console.error("[Enhance Pro API][TaskStatus] 错误:", error); return NextResponse.json( { error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" }, { status: 500 }, ); } } export async function POST(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "未授权,请先登录" }, { status: 401 }); } const config = await loadEnhanceProConfig(); if (!config.enabled) { return NextResponse.json({ error: "图像增强 Pro 功能未开启" }, { status: 403 }); } if (!config.apiKey || !config.baseUrl) { return NextResponse.json({ error: "图像增强 Pro API 尚未配置" }, { status: 400 }); } const body = (await request.json()) as EnhanceProRequestBody; const prompt = String(body.prompt || "").trim(); const chatId = body.chatId || null; const messageId = body.messageId || null; const sourceInput = await resolveSourceInput(body); const requestedWidth = sanitizeDimension(body.width, sourceInput.width); const requestedHeight = sanitizeDimension(body.height, sourceInput.height); const clampedDimensions = clampDimensions( requestedWidth, requestedHeight, config.maxOutputEdge, ); const batchSize = config.allowBatchSize ? clampInteger(body.batchSize, 1, config.maxBatchSize, 1) : 1; const selectedModel: EnhanceProKreaModel = body.model === "Recovery" || body.model === "Recovery V2" || body.model === "Reimagine" || body.model === "Redefine" ? body.model : config.defaultModel; const pointsConfig = await getPointsSystemConfig(); const pointsEstimate = calculateEnhanceProPointsEstimate(pointsConfig, { width: clampedDimensions.width, height: clampedDimensions.height, batchSize, model: selectedModel, }); if (pointsEstimate.chargeable && pointsConfig.blockOnInsufficient !== false) { const userData = await getUserById({ id: session.user.id }); if (!userData?.isHighVip) { const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0)); if (currentBalance < pointsEstimate.cost) { const message = `积分不足,当前余额 ${currentBalance},本次预计消耗 ${pointsEstimate.cost}。`; return NextResponse.json( { error: message, code: "insufficient_points", message, balance: currentBalance, expectedCost: pointsEstimate.cost, unitCost: pointsEstimate.cost, }, { status: 402 }, ); } } } const startTime = Date.now(); const createdJob = await createEnhanceProJob({ config, requestBody: { ...body, batchSize, model: selectedModel, }, sourceImageUrl: sourceInput.sourceImageUrl, width: clampedDimensions.width, height: clampedDimensions.height, }); const createdStatus = normalizeJobStatus(createdJob?.status); const taskId = String(createdJob?.job_id || "").trim(); if (!taskId) { return NextResponse.json( { error: "创建图像增强 Pro 任务失败", detail: "Krea 未返回 job_id" }, { status: 500 }, ); } if (pointsEstimate.chargeable) { try { await consumeUserPoints({ userId: session.user.id, amount: pointsEstimate.cost, reason: "enhance_pro_generation", modelId: `${ENHANCE_PRO_MODEL_ID}:${selectedModel}`, chatId, messageId, note: "图像增强 Pro 消耗积分", }); } catch (pointsError) { console.error("[Points] Failed to consume Enhance Pro points:", pointsError); } } if (createdStatus === "completed") { const responsePayload = await finalizeEnhanceProTaskResult({ userId: session.user.id, taskResult: createdJob, prompt, chatId, messageId, startedAt: startTime, sourceImageUrls: [sourceInput.sourceImageUrl], width: clampedDimensions.width, height: clampedDimensions.height, }); return NextResponse.json({ ...responsePayload, chargedPoints: pointsEstimate.chargeable ? pointsEstimate.cost : 0, }); } try { const taskResult = await pollEnhanceProJob(config, taskId); const responsePayload = await finalizeEnhanceProTaskResult({ userId: session.user.id, taskResult, prompt, chatId, messageId, startedAt: startTime, sourceImageUrls: [sourceInput.sourceImageUrl], width: clampedDimensions.width, height: clampedDimensions.height, }); return NextResponse.json({ ...responsePayload, chargedPoints: pointsEstimate.chargeable ? pointsEstimate.cost : 0, }); } catch (pollError) { if (isEnhanceProTaskStillProcessing(pollError)) { return buildEnhanceProTaskPendingResponse(taskId); } return NextResponse.json( { error: "图像增强 Pro 处理失败", detail: pollError instanceof Error ? pollError.message : "轮询失败", }, { status: 500 }, ); } } catch (error) { console.error("[Enhance Pro API] 错误:", error); return NextResponse.json( { error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" }, { status: 500 }, ); } }