Your Name 17dcfcd55d
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add PSD template font rendering workflow
2026-03-27 11:01:13 +08:00

199 lines
6.0 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { upsertSystemSetting } from "@/lib/db/queries";
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { uploadFile } from "@/lib/storage/service";
import {
AGENT_PSD_TEMPLATES_KEY,
normalizeAgentPsdTemplateStatus,
upsertAgentPsdTemplate,
type AgentPsdTemplate,
type AgentPsdTemplateLayerSlot,
type AgentPsdTemplateListResponse,
type AgentPsdTemplateMutationResponse,
} from "@/lib/agents/psd-template-config";
import { parsePsdTemplateFile } from "@/lib/agents/psd-template-parser";
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
export const runtime = "nodejs";
const MAX_TEMPLATE_FILE_SIZE = 80 * 1024 * 1024;
const ALLOWED_TEMPLATE_EXTENSIONS = new Set(["psd", "psb"]);
function jsonListError(status: number, error: string) {
const payload: AgentPsdTemplateListResponse = {
success: false,
templates: [],
error,
};
return NextResponse.json(payload, { status });
}
function jsonMutationError(status: number, error: string) {
const payload: AgentPsdTemplateMutationResponse = {
success: false,
error,
};
return NextResponse.json(payload, { status });
}
function getFileExtension(fileName: string) {
return fileName.toLowerCase().split(".").pop() || "";
}
function getDefaultTemplateName(fileName: string) {
return fileName.replace(/\.[^.]+$/, "").trim() || "未命名 PSD 模板";
}
export async function GET() {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session?.user?.id) {
return jsonListError(401, "未登录");
}
try {
const templates = await readAgentPsdTemplates();
return NextResponse.json({
success: true,
templates,
} satisfies AgentPsdTemplateListResponse);
} catch (error) {
console.error("[PSD Templates][Admin] list failed:", error);
return jsonListError(503, "PSD 模板读取失败");
}
}
export async function POST(request: NextRequest) {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session?.user?.id) {
return jsonMutationError(401, "未登录");
}
let formData: FormData;
try {
formData = await request.formData();
} catch {
return jsonMutationError(400, "上传表单解析失败");
}
const file = formData.get("file");
const name =
String(formData.get("name") || "").trim() ||
(file instanceof File ? getDefaultTemplateName(file.name) : "");
const description = String(formData.get("description") || "").trim();
const status = normalizeAgentPsdTemplateStatus(formData.get("status"));
if (!name) {
return jsonMutationError(400, "缺少模板名称");
}
if (name.length > 80) {
return jsonMutationError(400, "模板名称不能超过 80 个字符");
}
if (!(file instanceof File)) {
return jsonMutationError(400, "缺少 PSD 模板文件");
}
const extension = getFileExtension(file.name);
if (!ALLOWED_TEMPLATE_EXTENSIONS.has(extension)) {
return jsonMutationError(400, "模板文件仅支持 PSD / PSB");
}
if (file.size <= 0) {
return jsonMutationError(400, "模板文件不能为空");
}
if (file.size > MAX_TEMPLATE_FILE_SIZE) {
return jsonMutationError(400, "模板文件不能超过 80MB");
}
try {
const fileBuffer = await file.arrayBuffer();
const parsedTemplate = await parsePsdTemplateFile(fileBuffer);
if (parsedTemplate.layers.length === 0) {
return jsonMutationError(400, "PSD 中未识别到可配置图层");
}
const [uploadedFile, uploadedPreview] = await Promise.all([
uploadFile(file.name, fileBuffer),
uploadFile(
`agent-psd-preview-${Date.now()}.png`,
parsedTemplate.previewBuffer.buffer.slice(
parsedTemplate.previewBuffer.byteOffset,
parsedTemplate.previewBuffer.byteOffset + parsedTemplate.previewBuffer.byteLength
) as ArrayBuffer
),
]);
const now = new Date().toISOString();
const nextTemplate: AgentPsdTemplate = {
id: randomUUID(),
name,
description: description || undefined,
status,
fileName: file.name,
fileUrl: uploadedFile.url,
filePath: uploadedFile.pathname,
previewUrl: uploadedPreview.url,
previewPath: uploadedPreview.pathname,
renderBaseUrl: uploadedPreview.url,
renderBasePath: uploadedPreview.pathname,
width: parsedTemplate.width,
height: parsedTemplate.height,
layerCount: parsedTemplate.layers.length,
layers: parsedTemplate.layers.map((layer): AgentPsdTemplateLayerSlot => {
if (layer.slotType === "text") {
return {
...layer,
slotType: "text",
isEditable: false,
isRequired: false,
placeholder: layer.defaultText || layer.layerName,
fontSize: Math.max(16, Math.min(layer.height, Math.round(layer.height * 0.78))),
lineHeight: 1.2,
letterSpacing: 0,
fontWeight: 500,
textColor: "#111111",
textAlign: "left",
paddingX: 0,
paddingY: 0,
maxLines: 4,
};
}
return {
...layer,
slotType: "image",
isEditable: false,
isRequired: false,
isDefaultResultSource: false,
allowUserUploadOverride: true,
replaceMode: "cover",
};
}),
createdAt: now,
updatedAt: now,
uploadedById: session.user.id,
};
const currentTemplates = await readAgentPsdTemplates();
const templates = upsertAgentPsdTemplate(currentTemplates, nextTemplate);
await upsertSystemSetting({
key: AGENT_PSD_TEMPLATES_KEY,
value: templates,
updatedBy: session.user.id,
});
return NextResponse.json({
success: true,
template: nextTemplate,
} satisfies AgentPsdTemplateMutationResponse);
} catch (error) {
console.error("[PSD Templates][Admin] create failed:", error);
return jsonMutationError(502, "PSD 模板上传或解析失败");
}
}