156 lines
3.7 KiB
TypeScript
156 lines
3.7 KiB
TypeScript
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<string, unknown> {
|
|
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<T extends string>(
|
|
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);
|
|
}
|