Files
image-prompt-studio/lib/request-schema.ts
2026-04-25 11:26:33 +08:00

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);
}