Files
NianAIGC/lib/server/image-template-input.ts
2026-07-03 11:25:25 +08:00

105 lines
4.0 KiB
TypeScript

import type { ImageTemplate } from "@/lib/types";
type ImageTemplateBody = {
name?: unknown;
description?: unknown;
prompt?: unknown;
previewImageUrl?: unknown;
settings?: unknown;
sortOrder?: unknown;
};
type ImageTemplatePatch = Partial<Omit<ImageTemplate, "id" | "ownerId" | "createdAt" | "updatedAt">>;
export function normalizeImageTemplateCreate(body: ImageTemplateBody): Omit<ImageTemplate, "id" | "ownerId" | "createdAt" | "updatedAt"> {
return {
name: requiredText(body.name, "模板名称", 80),
description: optionalText(body.description, 240),
prompt: requiredText(body.prompt, "预设提示词", 4000),
previewImageUrl: optionalPreviewUrl(body.previewImageUrl),
settings: normalizeTemplateSettings(body.settings),
sortOrder: optionalNumber(body.sortOrder) ?? 0
};
}
export function normalizeImageTemplateUpdate(body: ImageTemplateBody): ImageTemplatePatch {
const patch: ImageTemplatePatch = {};
if ("name" in body) patch.name = requiredText(body.name, "模板名称", 80);
if ("description" in body) patch.description = optionalText(body.description, 240);
if ("prompt" in body) patch.prompt = requiredText(body.prompt, "预设提示词", 4000);
if ("previewImageUrl" in body) patch.previewImageUrl = optionalPreviewUrl(body.previewImageUrl);
if ("settings" in body) patch.settings = normalizeTemplateSettings(body.settings);
if ("sortOrder" in body) patch.sortOrder = optionalNumber(body.sortOrder) ?? 0;
return patch;
}
function requiredText(value: unknown, label: string, maxLength: number): string {
const text = optionalText(value, maxLength);
if (!text) throw new Error(`${label}不能为空`);
return text;
}
function optionalText(value: unknown, maxLength: number): string | undefined {
if (value === undefined || value === null) return undefined;
const text = String(value).trim();
if (!text) return undefined;
return text.slice(0, maxLength);
}
function optionalPreviewUrl(value: unknown): string | undefined {
const url = optionalText(value, 1000);
if (!url) return undefined;
if (url.startsWith("/") || /^https?:\/\//i.test(url)) return url;
throw new Error("效果预览图地址必须是 http(s) 或站内路径");
}
function normalizeTemplateSettings(value: unknown): ImageTemplate["settings"] {
if (!isRecord(value)) return {};
const width = optionalPositiveInteger(value.width);
const height = optionalPositiveInteger(value.height);
const engine = normalizeImageEngine(value.engine);
const scale = optionalScale(value.scale);
const quality = normalizeQuality(value.quality);
return {
...(engine ? { engine } : {}),
...(width && height ? { width, height } : {}),
...(typeof value.forceSingle === "boolean" ? { forceSingle: value.forceSingle } : {}),
...(scale !== undefined ? { scale } : {}),
...(quality ? { quality } : {})
};
}
function optionalNumber(value: unknown): number | undefined {
if (value === undefined || value === null || value === "") return undefined;
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.trunc(parsed) : undefined;
}
function optionalPositiveInteger(value: unknown): number | undefined {
const parsed = optionalNumber(value);
if (!parsed || parsed < 1) return undefined;
return Math.min(parsed, 8192);
}
function optionalScale(value: unknown): number | undefined {
const parsed = optionalNumber(value);
if (parsed === undefined) return undefined;
return Math.max(1, Math.min(100, parsed));
}
function normalizeImageEngine(value: unknown): ImageTemplate["settings"]["engine"] | undefined {
const text = optionalText(value, 20);
if (text === "jimeng" || text === "evolink") return text;
return undefined;
}
function normalizeQuality(value: unknown): ImageTemplate["settings"]["quality"] | undefined {
const text = optionalText(value, 20);
if (text === "low" || text === "medium" || text === "high") return text;
return undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}