116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
import { NextResponse, type NextRequest } from "next/server";
|
|
import { randomUUID } from "node:crypto";
|
|
import { requireApiUser } from "@/lib/api-auth";
|
|
import { appConfig } from "@/lib/config";
|
|
import { generateImagesWithOpenAI } from "@/lib/openai";
|
|
import { parseGenerateRequest } from "@/lib/request-schema";
|
|
import { assertCanGenerate, getQuotaSnapshotForUser } from "@/lib/quota";
|
|
import { getCurrentPeriodKey } from "@/lib/quota-core";
|
|
import { jsonError, normalizeError } from "@/lib/http";
|
|
import {
|
|
recordCompletedGeneration,
|
|
recordGenerationJob,
|
|
} from "@/lib/db";
|
|
import {
|
|
buildStoredImageUrl,
|
|
persistGeneratedImages,
|
|
removePersistedImageFiles,
|
|
} from "@/lib/image-storage";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const user = requireApiUser(request);
|
|
let prompt = "";
|
|
let payload:
|
|
| ReturnType<typeof parseGenerateRequest>
|
|
| null = null;
|
|
|
|
try {
|
|
if (!appConfig.openAiApiKey) {
|
|
return jsonError("服务端尚未配置 OPENAI_API_KEY。", 500);
|
|
}
|
|
|
|
assertCanGenerate(user);
|
|
|
|
const body = (await request.json()) as unknown;
|
|
payload = parseGenerateRequest(body, appConfig.openAiImageModel);
|
|
prompt = payload.prompt;
|
|
|
|
const result = await generateImagesWithOpenAI(payload);
|
|
const jobId = randomUUID();
|
|
const persistedImages = await persistGeneratedImages({
|
|
userId: user.id,
|
|
jobId,
|
|
images: result.images,
|
|
});
|
|
|
|
try {
|
|
recordCompletedGeneration({
|
|
jobId,
|
|
userId: user.id,
|
|
prompt,
|
|
imageCount: result.images.length,
|
|
durationMs: result.meta.durationMs,
|
|
model: result.meta.model,
|
|
size: result.meta.size,
|
|
quality: result.meta.quality,
|
|
outputFormat: result.meta.outputFormat,
|
|
background: result.meta.background,
|
|
moderation: result.meta.moderation,
|
|
outputCompression: payload.output_compression ?? null,
|
|
images: persistedImages.images,
|
|
usageMeta: {
|
|
model: result.meta.model,
|
|
size: result.meta.size,
|
|
quality: result.meta.quality,
|
|
outputFormat: result.meta.outputFormat,
|
|
background: result.meta.background,
|
|
},
|
|
periodKey: getCurrentPeriodKey(),
|
|
});
|
|
} catch (error) {
|
|
await removePersistedImageFiles(persistedImages.files);
|
|
throw error;
|
|
}
|
|
|
|
const images = result.images.map((image, index) => ({
|
|
...image,
|
|
id: persistedImages.images[index]?.id ?? image.id,
|
|
fileName: persistedImages.images[index]?.fileName ?? image.fileName,
|
|
url: persistedImages.images[index]
|
|
? buildStoredImageUrl(persistedImages.images[index].id)
|
|
: undefined,
|
|
}));
|
|
const nextQuota = getQuotaSnapshotForUser(user);
|
|
|
|
return NextResponse.json({
|
|
ok: true,
|
|
images,
|
|
meta: result.meta,
|
|
quota: nextQuota,
|
|
});
|
|
} catch (error) {
|
|
if (prompt) {
|
|
recordGenerationJob({
|
|
userId: user.id,
|
|
prompt,
|
|
status: "error",
|
|
model: payload?.model ?? null,
|
|
size: payload?.size ?? null,
|
|
quality: payload?.quality ?? null,
|
|
outputFormat: payload?.output_format ?? null,
|
|
background: payload?.background ?? null,
|
|
moderation: payload?.moderation ?? null,
|
|
outputCompression: payload?.output_compression ?? null,
|
|
errorMessage: error instanceof Error ? error.message : "unknown error",
|
|
});
|
|
}
|
|
|
|
const normalizedError = normalizeError(error, "图片生成失败。");
|
|
return jsonError(normalizedError.message, normalizedError.statusCode, {
|
|
detail: normalizedError.detail,
|
|
});
|
|
}
|
|
}
|