Your Name b8a59fe2a4
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: harden enhance pro task charging
2026-04-27 18:38:42 +08:00

1089 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { 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,
upsertMediaAsset,
} from "@/lib/db/queries";
import {
enhanceProTask,
generatedImage,
pointsLedger,
} 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_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<typeof finalizeEnhanceProTaskResult>
> & {
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<Buffer> {
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,
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) {
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,
};
}
function parseEnhanceProStoredRequest(task: EnhanceProTaskRecord): EnhanceProStoredRequest {
const request =
task.request && typeof task.request === "object"
? (task.request as Partial<EnhanceProStoredRequest>)
: {};
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:
request.model === "Recovery" ||
request.model === "Recovery V2" ||
request.model === "Reimagine" ||
request.model === "Redefine"
? 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 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;
}
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) {
await db
.update(enhanceProTask)
.set({
status: "failed",
error: errorMessage,
completedAt: new Date(),
updatedAt: new Date(),
})
.where(eq(enhanceProTask.id, taskId));
}
async function persistEnhanceProTaskSuccess(params: {
taskId: string;
payload: EnhanceProSuccessPayload;
pointsCharged: boolean;
}) {
await db
.update(enhanceProTask)
.set({
status: "succeeded",
result: params.payload as Record<string, unknown>,
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 };
}
const note = `图像增强 Pro 消耗积分(任务 ${task.id} / ${task.providerTaskId})`;
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 };
}
return 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,
});
}
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.ok || chargeResult.skipped;
} catch (pointsError) {
console.error("[Points] Failed to consume Enhance Pro points:", pointsError);
}
}
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) {
return NextResponse.json(
{
error: "图像增强 Pro 处理失败",
detail: String(task.error || "任务失败"),
},
{ 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<string, unknown>)
: {};
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") {
return buildEnhanceProTaskFailedResponse(task);
}
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" ? "失败" : "已取消"}`;
await markEnhanceProTaskFailed(task.id, detail);
return NextResponse.json(
{
error: "图像增强 Pro 处理失败",
detail,
},
{ status: 500 },
);
}
await markEnhanceProTaskRunning(task.id);
return buildEnhanceProTaskPendingResponse(task.id);
} 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 });
}
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") {
return buildEnhanceProTaskFailedResponse(existingTask);
}
return buildEnhanceProTaskPendingResponse(existingTask.id);
}
}
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 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<string, unknown>,
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") {
return buildEnhanceProTaskFailedResponse(existingTask);
}
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) {
console.error("[Enhance Pro API] 错误:", error);
return NextResponse.json(
{ error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
{ status: 500 },
);
}
}