import type { GenerateOutputFormat, GeneratePayload, HttpError, } from "@/lib/types"; const SIZE_OPTIONS = ["auto", "1024x1024", "1536x1024", "1024x1536"] as const; const QUALITY_OPTIONS = ["auto", "low", "medium", "high"] as const; const OUTPUT_FORMAT_OPTIONS = ["png", "jpeg", "webp"] as const; const BACKGROUND_OPTIONS = ["auto", "transparent", "opaque"] as const; const MODERATION_OPTIONS = ["auto", "low"] as const; export function parseGenerateRequest( body: unknown, model: string, ): GeneratePayload { if (!isRecord(body)) { throw createRequestError(400, "请求体必须是 JSON 对象。"); } const prompt = normalizePrompt(body.prompt); const size = normalizeEnum(body.size, SIZE_OPTIONS, "size"); const quality = normalizeEnum(body.quality, QUALITY_OPTIONS, "quality"); const outputFormat = normalizeEnum( body.outputFormat, OUTPUT_FORMAT_OPTIONS, "outputFormat", ); const background = normalizeEnum( body.background, BACKGROUND_OPTIONS, "background", ); const moderation = normalizeEnum( body.moderation, MODERATION_OPTIONS, "moderation", ); const count = normalizeCount(body.count); const outputCompression = normalizeCompression( body.outputCompression, outputFormat, ); const user = normalizeUser(body.user); if (outputFormat === "jpeg" && background === "transparent") { throw createRequestError( 400, "JPEG 不支持透明背景,请改为 PNG / WEBP 或将背景改成不透明。", ); } const payload: GeneratePayload = { model, prompt, size, quality, n: count, moderation, output_format: outputFormat, background, }; if (typeof outputCompression === "number") { payload.output_compression = outputCompression; } if (user) { payload.user = user; } return payload; } export function createRequestError( statusCode: number, message: string, ): HttpError { const error = new Error(message) as HttpError; error.statusCode = statusCode; return error; } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function normalizePrompt(value: unknown): string { const prompt = String(value ?? "").trim(); if (!prompt) { throw createRequestError(400, "Prompt 不能为空。"); } if (prompt.length > 32000) { throw createRequestError(400, "Prompt 不能超过 32000 个字符。"); } return prompt; } function normalizeEnum( value: unknown, options: readonly T[], fieldName: string, ): T { const normalizedValue = (String(value ?? "").trim() || options[0]) as T; if (!options.includes(normalizedValue)) { throw createRequestError(400, `字段 ${fieldName} 的取值不合法。`); } return normalizedValue; } function normalizeCount(value: unknown): number { const numericValue = Number.parseInt(String(value ?? "1"), 10); if (!Number.isFinite(numericValue) || numericValue < 1 || numericValue > 4) { throw createRequestError(400, "单次最多生成 4 张图片。"); } return numericValue; } function normalizeCompression( value: unknown, outputFormat: GenerateOutputFormat, ): number | undefined { if (outputFormat === "png") { return undefined; } const numericValue = Number.parseInt(String(value ?? "90"), 10); if ( !Number.isFinite(numericValue) || numericValue < 0 || numericValue > 100 ) { throw createRequestError(400, "压缩质量必须在 0 到 100 之间。"); } return numericValue; } function normalizeUser(value: unknown): string | undefined { const user = String(value ?? "").trim(); if (!user) { return undefined; } return user.slice(0, 64); }