import { createHash } from "node:crypto"; import { and, eq, or } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { consumeUserPoints, createMediaAssetRelation, getPointsSystemConfig, getMediaAssetsByUserAndUrls, getSystemSetting, getUserById, refundUserPoints, upsertMediaAsset, } from "@/lib/db/queries"; import { enhanceProTask, generatedImage, pointsLedger, } from "@/lib/db/schema"; import { ENHANCE_PRO_KREA_SETTING_KEY, isEnhanceProKreaModel, isEnhanceProKreaModelHidden, normalizeEnhanceProKreaConfig, type EnhanceProKreaConfig, type EnhanceProKreaModel, type EnhanceProOutputFormat, type EnhanceProSubjectDetection, } from "@/lib/enhance-pro/config"; import { createLogger } from "@/lib/observability/logger"; import { calculateEnhanceProPointsEstimate } from "@/lib/points/config"; import { uploadFile } from "@/lib/storage/service"; const logger = createLogger("enhance-pro-api"); const ENHANCE_PRO_CREATE_TIMEOUT_MS = 120_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; let enhanceProKeyPoolCursor = 0; type EnhanceProRequestBody = { imageUrl?: string; image_base64?: string; mimeType?: string; prompt?: string; chatId?: string | null; messageId?: string | null; clientRequestId?: 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; }; type EnhanceProStoredRequest = { prompt: string; sourceImageUrl: string; width: number; height: number; batchSize: number; model: EnhanceProKreaModel; upscalingActivated: boolean; imageScalingFactor: number; chatId: string | null; messageId: string | null; }; type EnhanceProSuccessPayload = Awaited< ReturnType > & { chargedPoints: number; }; type EnhanceProTaskRecord = typeof enhanceProTask.$inferSelect; 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 normalizeClientRequestId(value: unknown) { return String(value || "").trim().slice(0, 64); } function getEnhanceProKeyFingerprint(apiKey: string) { return createHash("sha256") .update(String(apiKey || "").trim()) .digest("hex") .slice(0, 16); } function getEnhanceProEffectiveApiKeys(config: EnhanceProKreaConfig) { const pooledKeys = Array.isArray(config.apiKeys) ? config.apiKeys : []; if (pooledKeys.length > 0) return pooledKeys; return config.apiKey ? [config.apiKey] : []; } function resolveEnhanceProApiKey( config: EnhanceProKreaConfig, preferredKeyId?: string | null, ) { const keys = getEnhanceProEffectiveApiKeys(config); if (keys.length === 0) return null; const normalizedPreferredKeyId = String(preferredKeyId || "").trim(); if (normalizedPreferredKeyId) { const matchedKey = keys.find( (apiKey) => getEnhanceProKeyFingerprint(apiKey) === normalizedPreferredKeyId, ); if (matchedKey) { return { apiKey: matchedKey, keyId: normalizedPreferredKeyId, }; } } const selectedKey = keys[enhanceProKeyPoolCursor % keys.length]; enhanceProKeyPoolCursor = (enhanceProKeyPoolCursor + 1) % keys.length; return { apiKey: selectedKey, keyId: getEnhanceProKeyFingerprint(selectedKey), }; } 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) { logger.warn("thumbnail_creation_failed", 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) { logger.warn("image_dimension_detection_failed", 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, params.config.maxUpscaleFactor, 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 fetchEnhanceProJobStatus(config: EnhanceProKreaConfig, jobId: string) { 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}`); } return await response.json(); } 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) { logger.warn("media_asset_registration_failed", 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, }; } function parseEnhanceProStoredRequest(task: EnhanceProTaskRecord): EnhanceProStoredRequest { const request = task.request && typeof task.request === "object" ? (task.request as Partial) : {}; const width = sanitizeDimension(request.width, 1024); const height = sanitizeDimension(request.height, 1024); return { prompt: String(request.prompt || task.prompt || "").trim() || "图像增强 Pro", sourceImageUrl: String(request.sourceImageUrl || task.sourceImageUrl || "").trim(), width, height, batchSize: clampInteger(request.batchSize, 1, 8, 1), model: isEnhanceProKreaModel(request.model) ? request.model : "Recovery V2", upscalingActivated: normalizeBoolean(request.upscalingActivated, false), imageScalingFactor: clampNumber(request.imageScalingFactor, 1, 8, 1), chatId: request.chatId ? String(request.chatId) : task.chatId || null, messageId: request.messageId ? String(request.messageId) : task.messageId || null, }; } async function getEnhanceProTaskForUser(params: { taskId: string; userId: string }) { const rows = await db .select() .from(enhanceProTask) .where(and(eq(enhanceProTask.id, params.taskId), eq(enhanceProTask.userId, params.userId))) .limit(1); return rows[0] ?? null; } async function getEnhanceProTaskById(taskId: string) { const rows = await db .select() .from(enhanceProTask) .where(eq(enhanceProTask.id, taskId)) .limit(1); return rows[0] ?? null; } async function getEnhanceProTaskByClientRequestId(params: { userId: string; clientRequestId: string; }) { const rows = await db .select() .from(enhanceProTask) .where( and( eq(enhanceProTask.userId, params.userId), eq(enhanceProTask.clientRequestId, params.clientRequestId), ), ) .limit(1); return rows[0] ?? null; } function getEnhanceProPointsChargeNote(task: EnhanceProTaskRecord) { return `图像增强 Pro 消耗积分(任务 ${task.id} / ${task.providerTaskId})`; } function getEnhanceProPointsRefundNote(task: EnhanceProTaskRecord) { return `图像增强 Pro 失败退回积分(任务 ${task.id} / ${task.providerTaskId})`; } async function refundEnhanceProPointsForFailedTask( task: EnhanceProTaskRecord, errorMessage: string, ) { if (task.pointsCost <= 0) { return 0; } const chargeLedger = await db .select({ id: pointsLedger.id }) .from(pointsLedger) .where( and( eq(pointsLedger.userId, task.userId), eq(pointsLedger.reason, "enhance_pro_generation"), eq(pointsLedger.note, getEnhanceProPointsChargeNote(task)), ), ) .limit(1); if (!chargeLedger[0]) { return 0; } const refundNote = getEnhanceProPointsRefundNote(task); const existingRefundLedger = await db .select({ id: pointsLedger.id }) .from(pointsLedger) .where( and( eq(pointsLedger.userId, task.userId), eq(pointsLedger.reason, "enhance_pro_refund"), eq(pointsLedger.note, refundNote), ), ) .limit(1); if (existingRefundLedger[0]) { await db .update(enhanceProTask) .set({ pointsCharged: false, chargedAt: null, updatedAt: new Date(), }) .where(eq(enhanceProTask.id, task.id)); return task.pointsCost; } const request = parseEnhanceProStoredRequest(task); const refundResult = await refundUserPoints({ userId: task.userId, amount: task.pointsCost, reason: "enhance_pro_refund", modelId: `${ENHANCE_PRO_MODEL_ID}:${request.model}`, chatId: request.chatId, messageId: request.messageId, note: refundNote, }); if (!refundResult.ok) { logger.error("points_refund_failed", { taskId: task.id, errorLength: errorMessage.length, }); return 0; } await db .update(enhanceProTask) .set({ pointsCharged: false, chargedAt: null, updatedAt: new Date(), }) .where(eq(enhanceProTask.id, task.id)); return task.pointsCost; } async function markEnhanceProTaskRunning(taskId: string) { await db .update(enhanceProTask) .set({ status: "running", updatedAt: new Date(), error: null, }) .where(eq(enhanceProTask.id, taskId)); } async function claimEnhanceProTaskFinalization(taskId: string) { const rows = await db .update(enhanceProTask) .set({ status: "finalizing", updatedAt: new Date(), error: null, }) .where( and( eq(enhanceProTask.id, taskId), or(eq(enhanceProTask.status, "pending"), eq(enhanceProTask.status, "running")), ), ) .returning(); return rows[0] ?? null; } async function markEnhanceProTaskFailed(taskId: string, errorMessage: string) { const task = await getEnhanceProTaskById(taskId); let refundedPoints = 0; if (task) { try { refundedPoints = await refundEnhanceProPointsForFailedTask(task, errorMessage); } catch (refundError) { logger.error("failed_task_refund_error", refundError, { taskId }); } } await db .update(enhanceProTask) .set({ status: "failed", error: errorMessage, completedAt: new Date(), updatedAt: new Date(), }) .where(eq(enhanceProTask.id, taskId)); return { refundedPoints }; } async function persistEnhanceProTaskSuccess(params: { taskId: string; payload: EnhanceProSuccessPayload; pointsCharged: boolean; }) { await db .update(enhanceProTask) .set({ status: "succeeded", result: params.payload as Record, error: null, pointsCharged: params.pointsCharged, chargedAt: params.pointsCharged ? new Date() : null, completedAt: new Date(), updatedAt: new Date(), }) .where(eq(enhanceProTask.id, params.taskId)); } async function consumeEnhanceProPointsOnce( task: EnhanceProTaskRecord, request: EnhanceProStoredRequest, ) { if (task.pointsCost <= 0) { return { ok: true, skipped: true, chargeApplied: false }; } const note = getEnhanceProPointsChargeNote(task); const existingLedger = await db .select({ id: pointsLedger.id }) .from(pointsLedger) .where( and( eq(pointsLedger.userId, task.userId), eq(pointsLedger.reason, "enhance_pro_generation"), eq(pointsLedger.note, note), ), ) .limit(1); if (existingLedger[0]) { return { ok: true, skipped: true, chargeApplied: true }; } const result = await consumeUserPoints({ userId: task.userId, amount: task.pointsCost, reason: "enhance_pro_generation", modelId: `${ENHANCE_PRO_MODEL_ID}:${request.model}`, chatId: request.chatId, messageId: request.messageId, note, }); return { ...result, chargeApplied: result.ok && !result.skipped, }; } function getStoredEnhanceProSuccessPayload(task: EnhanceProTaskRecord) { if (!task.result || typeof task.result !== "object") { return null; } return task.result as EnhanceProSuccessPayload; } async function finalizeEnhanceProTask( task: EnhanceProTaskRecord, taskResult: any, startedAt?: number, ) { const existingPayload = getStoredEnhanceProSuccessPayload(task); if (task.status === "succeeded" && existingPayload) { return existingPayload; } const claimedTask = task.status === "finalizing" ? task : await claimEnhanceProTaskFinalization(task.id); if (!claimedTask) { return null; } const request = parseEnhanceProStoredRequest(claimedTask); try { const responsePayload = await finalizeEnhanceProTaskResult({ userId: claimedTask.userId, taskResult, prompt: request.prompt, chatId: request.chatId, messageId: request.messageId, startedAt, sourceImageUrls: request.sourceImageUrl ? [request.sourceImageUrl] : undefined, width: request.width, height: request.height, }); let pointsCharged = claimedTask.pointsCharged; if (!pointsCharged && claimedTask.pointsCost > 0) { try { const chargeResult = await consumeEnhanceProPointsOnce(claimedTask, request); pointsCharged = chargeResult.chargeApplied === true; } catch (pointsError) { logger.error("points_charge_failed", pointsError, { taskId: claimedTask.id }); } } const payload: EnhanceProSuccessPayload = { ...responsePayload, chargedPoints: pointsCharged ? claimedTask.pointsCost : 0, }; await persistEnhanceProTaskSuccess({ taskId: claimedTask.id, payload, pointsCharged, }); return payload; } catch (error) { const errorMessage = error instanceof Error ? error.message : "未知错误"; await markEnhanceProTaskFailed(claimedTask.id, errorMessage); throw error; } } function buildEnhanceProTaskFailedResponse( task: EnhanceProTaskRecord, refundedPoints = 0, ) { return NextResponse.json( { error: "图像增强 Pro 处理失败", detail: String(task.error || "任务失败"), refundedPoints, }, { status: 500 }, ); } 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 task = await getEnhanceProTaskForUser({ taskId, userId: session.user.id, }); if (!task) { return NextResponse.json({ error: "任务不存在" }, { status: 404 }); } const storedPayload = getStoredEnhanceProSuccessPayload(task); if (task.status === "succeeded" && storedPayload) { return NextResponse.json(storedPayload); } if (task.status === "failed") { const failureUpdate = await markEnhanceProTaskFailed( task.id, String(task.error || "任务失败"), ); return buildEnhanceProTaskFailedResponse(task, failureUpdate.refundedPoints); } if (task.status === "finalizing") { return buildEnhanceProTaskPendingResponse(task.id); } const config = await loadEnhanceProConfig(); if (!config.enabled) { return NextResponse.json({ error: "图像增强 Pro 功能未开启" }, { status: 403 }); } const keySelection = resolveEnhanceProApiKey(config, task.taskKeyId); if (!keySelection || !config.baseUrl) { return NextResponse.json({ error: "图像增强 Pro API 尚未配置" }, { status: 400 }); } const taskResult = await fetchEnhanceProJobStatus( { ...config, apiKey: keySelection.apiKey }, task.providerTaskId, ); const providerStatus = normalizeJobStatus(taskResult?.status); if (providerStatus === "completed") { const completedPayload = await finalizeEnhanceProTask(task, taskResult); if (completedPayload) { return NextResponse.json(completedPayload); } const refreshedTask = await getEnhanceProTaskForUser({ taskId, userId: session.user.id, }); const refreshedPayload = refreshedTask ? getStoredEnhanceProSuccessPayload(refreshedTask) : null; if (refreshedTask?.status === "succeeded" && refreshedPayload) { return NextResponse.json(refreshedPayload); } if (refreshedTask?.status === "failed") { return buildEnhanceProTaskFailedResponse(refreshedTask); } return buildEnhanceProTaskPendingResponse(task.id); } if (providerStatus === "failed" || providerStatus === "cancelled") { const detail = `Krea 任务${providerStatus === "failed" ? "失败" : "已取消"}`; const failureUpdate = await markEnhanceProTaskFailed(task.id, detail); return NextResponse.json( { error: "图像增强 Pro 处理失败", detail, refundedPoints: failureUpdate.refundedPoints, }, { status: 500 }, ); } await markEnhanceProTaskRunning(task.id); return buildEnhanceProTaskPendingResponse(task.id); } catch (error) { logger.error("task_status_request_failed", 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 }); } const keySelection = resolveEnhanceProApiKey(config); if (!keySelection || !config.baseUrl) { return NextResponse.json({ error: "图像增强 Pro API 尚未配置" }, { status: 400 }); } const requestConfig = { ...config, apiKey: keySelection.apiKey }; const body = (await request.json()) as EnhanceProRequestBody; const clientRequestId = normalizeClientRequestId(body.clientRequestId); if (clientRequestId) { const existingTask = await getEnhanceProTaskByClientRequestId({ userId: session.user.id, clientRequestId, }); if (existingTask) { const existingPayload = getStoredEnhanceProSuccessPayload(existingTask); if (existingTask.status === "succeeded" && existingPayload) { return NextResponse.json(existingPayload); } if (existingTask.status === "failed") { const failureUpdate = await markEnhanceProTaskFailed( existingTask.id, String(existingTask.error || "任务失败"), ); return buildEnhanceProTaskFailedResponse( existingTask, failureUpdate.refundedPoints, ); } return buildEnhanceProTaskPendingResponse(existingTask.id); } } const prompt = String(body.prompt || "").trim(); const chatId = body.chatId || null; const messageId = body.messageId || null; const requestedModel = String(body.model || "").trim(); const selectedModel: EnhanceProKreaModel = isEnhanceProKreaModel(requestedModel) ? requestedModel : config.defaultModel; if (isEnhanceProKreaModelHidden(config, selectedModel)) { return NextResponse.json( { error: "当前图像增强 Pro 模型已隐藏,请切换其他模型", code: "enhance_pro_model_hidden", model: selectedModel, }, { status: 403 }, ); } 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 upscalingActivated = body.upscaling_activated === true; const imageScalingFactor = clampNumber( body.image_scaling_factor, 1, config.maxUpscaleFactor, 1, ); const pointsConfig = await getPointsSystemConfig(); const pointsEstimate = calculateEnhanceProPointsEstimate(pointsConfig, { width: clampedDimensions.width, height: clampedDimensions.height, batchSize, model: selectedModel, upscalingActivated, scaleFactor: imageScalingFactor, }); 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: requestConfig, requestBody: { ...body, batchSize, model: selectedModel, upscaling_activated: upscalingActivated, image_scaling_factor: imageScalingFactor, }, sourceImageUrl: sourceInput.sourceImageUrl, width: clampedDimensions.width, height: clampedDimensions.height, }); const createdStatus = normalizeJobStatus(createdJob?.status); const providerTaskId = String(createdJob?.job_id || "").trim(); if (!providerTaskId) { return NextResponse.json( { error: "创建图像增强 Pro 任务失败", detail: "Krea 未返回 job_id" }, { status: 500 }, ); } const storedRequest: EnhanceProStoredRequest = { prompt: prompt || "图像增强 Pro", sourceImageUrl: sourceInput.sourceImageUrl, width: clampedDimensions.width, height: clampedDimensions.height, batchSize, model: selectedModel, upscalingActivated, imageScalingFactor, chatId, messageId, }; let createdTask: EnhanceProTaskRecord | null = null; try { const inserted = await db .insert(enhanceProTask) .values({ userId: session.user.id, chatId, messageId, clientRequestId: clientRequestId || null, providerTaskId, taskKeyId: keySelection.keyId, status: createdStatus === "completed" ? "running" : createdStatus === "failed" || createdStatus === "cancelled" ? "failed" : "pending", prompt: storedRequest.prompt, sourceImageUrl: storedRequest.sourceImageUrl, request: storedRequest as unknown as Record, error: createdStatus === "failed" || createdStatus === "cancelled" ? `Krea 任务${createdStatus === "failed" ? "失败" : "已取消"}` : null, pointsCost: pointsEstimate.chargeable ? pointsEstimate.cost : 0, pointsCharged: false, completedAt: createdStatus === "failed" || createdStatus === "cancelled" ? new Date() : null, createdAt: new Date(), updatedAt: new Date(), }) .returning(); createdTask = inserted[0] ?? null; } catch (insertError) { if (clientRequestId) { const existingTask = await getEnhanceProTaskByClientRequestId({ userId: session.user.id, clientRequestId, }); if (existingTask) { const existingPayload = getStoredEnhanceProSuccessPayload(existingTask); if (existingTask.status === "succeeded" && existingPayload) { return NextResponse.json(existingPayload); } if (existingTask.status === "failed") { const failureUpdate = await markEnhanceProTaskFailed( existingTask.id, String(existingTask.error || "任务失败"), ); return buildEnhanceProTaskFailedResponse( existingTask, failureUpdate.refundedPoints, ); } return buildEnhanceProTaskPendingResponse(existingTask.id); } } throw insertError; } if (!createdTask) { return NextResponse.json( { error: "创建图像增强 Pro 任务失败", detail: "任务入库失败" }, { status: 500 }, ); } if (createdStatus === "completed") { const responsePayload = await finalizeEnhanceProTask(createdTask, createdJob, startTime); if (responsePayload) { return NextResponse.json(responsePayload); } return buildEnhanceProTaskPendingResponse(createdTask.id); } if (createdStatus === "failed" || createdStatus === "cancelled") { return buildEnhanceProTaskFailedResponse(createdTask); } return buildEnhanceProTaskPendingResponse(createdTask.id); } catch (error) { logger.error("create_request_failed", error); return NextResponse.json( { error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" }, { status: 500 }, ); } }