312 lines
9.2 KiB
TypeScript
312 lines
9.2 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 { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
|
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
|
|
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 {
|
|
parsePsdTemplateFileFromUrl,
|
|
preparePsdTemplateRenderAssets,
|
|
} 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"]);
|
|
const PLACEHOLDER_PREVIEW_PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO2+p4kAAAAASUVORK5CYII=",
|
|
"base64"
|
|
);
|
|
|
|
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 模板";
|
|
}
|
|
|
|
function toArrayBuffer(buffer: Buffer) {
|
|
return buffer.buffer.slice(
|
|
buffer.byteOffset,
|
|
buffer.byteOffset + buffer.byteLength
|
|
) as ArrayBuffer;
|
|
}
|
|
|
|
async function repairTemplateLayerPreviewsIfNeeded(params: {
|
|
template: AgentPsdTemplate;
|
|
updatedBy: string;
|
|
}) {
|
|
const { template } = params;
|
|
if (template.parseStatus !== "completed") {
|
|
return template;
|
|
}
|
|
|
|
const needsRepair = template.layers.some((layer) => !String(layer.previewUrl || "").trim());
|
|
const fonts = await readAgentPsdFonts().catch(() => []);
|
|
const needsFontDetection = template.layers.some(
|
|
(layer) =>
|
|
layer.slotType === "text" &&
|
|
(!String(layer.detectedFontName || "").trim() ||
|
|
!Array.isArray(layer.detectedFontNames) ||
|
|
layer.detectedFontNames.length === 0)
|
|
);
|
|
|
|
let nextLayers = template.layers;
|
|
if (needsFontDetection) {
|
|
try {
|
|
const parsedTemplate = await parsePsdTemplateFileFromUrl(template.fileUrl);
|
|
const parsedTextLayerMap = new Map(
|
|
parsedTemplate.layers
|
|
.filter((layer) => layer.slotType === "text")
|
|
.map((layer) => [layer.layerId, layer])
|
|
);
|
|
nextLayers = nextLayers.map((layer) => {
|
|
if (layer.slotType !== "text") return layer;
|
|
const parsedLayer = parsedTextLayerMap.get(layer.layerId);
|
|
if (!parsedLayer) return layer;
|
|
return {
|
|
...layer,
|
|
detectedFontName: parsedLayer.detectedFontName || layer.detectedFontName,
|
|
detectedFontNames: parsedLayer.detectedFontNames || layer.detectedFontNames,
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.warn("[PSD Templates][Admin] failed to parse PSD font metadata:", {
|
|
templateId: template.id,
|
|
error,
|
|
});
|
|
}
|
|
}
|
|
|
|
nextLayers = nextLayers.map((layer) => {
|
|
if (layer.slotType !== "text") return layer;
|
|
if (layer.fontId) return layer;
|
|
|
|
const detectedFontNames = [
|
|
...((layer.detectedFontNames || []).filter(Boolean)),
|
|
...(layer.detectedFontName ? [layer.detectedFontName] : []),
|
|
];
|
|
const matchedFont = findBestMatchingPsdFont({
|
|
detectedFontNames,
|
|
fonts,
|
|
});
|
|
if (!matchedFont) return layer;
|
|
|
|
return {
|
|
...layer,
|
|
fontId: matchedFont.font.id,
|
|
fontFamily: matchedFont.font.family,
|
|
fontLabel: matchedFont.font.name,
|
|
fontFileUrl: matchedFont.font.fileUrl,
|
|
fontContentType: matchedFont.font.contentType,
|
|
};
|
|
});
|
|
const hasFontRepair = nextLayers.some(
|
|
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
|
|
);
|
|
|
|
if (!needsRepair && !hasFontRepair) {
|
|
return template;
|
|
}
|
|
|
|
try {
|
|
const preparedAssets = await preparePsdTemplateRenderAssets({
|
|
templateId: template.id,
|
|
fileUrl: template.fileUrl,
|
|
layers: nextLayers,
|
|
});
|
|
|
|
return {
|
|
...template,
|
|
layers: preparedAssets.layers,
|
|
updatedAt: new Date().toISOString(),
|
|
} satisfies AgentPsdTemplate;
|
|
} catch (error) {
|
|
console.warn("[PSD Templates][Admin] failed to repair missing layer previews:", {
|
|
templateId: template.id,
|
|
error,
|
|
});
|
|
return template;
|
|
}
|
|
}
|
|
|
|
export async function GET() {
|
|
const session = await requireAgentWorkflowAdmin().catch(() => null);
|
|
if (!session?.user?.id) {
|
|
return jsonListError(401, "未登录");
|
|
}
|
|
|
|
try {
|
|
const templates = await readAgentPsdTemplates();
|
|
const repairedTemplates = [] as AgentPsdTemplate[];
|
|
let hasRepairedTemplate = false;
|
|
|
|
for (const template of templates) {
|
|
const repairedTemplate = await repairTemplateLayerPreviewsIfNeeded({
|
|
template,
|
|
updatedBy: session.user.id,
|
|
});
|
|
repairedTemplates.push(repairedTemplate);
|
|
if (repairedTemplate !== template) {
|
|
hasRepairedTemplate = true;
|
|
}
|
|
}
|
|
|
|
if (hasRepairedTemplate) {
|
|
await upsertSystemSetting({
|
|
key: AGENT_PSD_TEMPLATES_KEY,
|
|
value: repairedTemplates,
|
|
updatedBy: session.user.id,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
templates: hasRepairedTemplate ? repairedTemplates : 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 brandName = String(formData.get("brandName") || "").trim();
|
|
const description = String(formData.get("description") || "").trim();
|
|
const status = normalizeAgentPsdTemplateStatus(formData.get("status"));
|
|
|
|
if (!name) {
|
|
return jsonMutationError(400, "缺少模板名称");
|
|
}
|
|
|
|
if (!brandName) {
|
|
return jsonMutationError(400, "请选择模板品牌");
|
|
}
|
|
|
|
if (name.length > 80) {
|
|
return jsonMutationError(400, "模板名称不能超过 80 个字符");
|
|
}
|
|
|
|
if (brandName.length > 64) {
|
|
return jsonMutationError(400, "模板品牌不能超过 64 个字符");
|
|
}
|
|
|
|
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 [uploadedFile, uploadedPreview] = await Promise.all([
|
|
uploadFile(file.name, fileBuffer),
|
|
uploadFile(
|
|
`agent-psd-preview-placeholder-${Date.now()}.png`,
|
|
toArrayBuffer(PLACEHOLDER_PREVIEW_PNG)
|
|
),
|
|
]);
|
|
|
|
const templateId = randomUUID();
|
|
const now = new Date().toISOString();
|
|
const nextTemplate: AgentPsdTemplate = {
|
|
id: templateId,
|
|
name,
|
|
brandName,
|
|
description: description || undefined,
|
|
status,
|
|
parseStatus: "pending",
|
|
parseProgress: 0,
|
|
parseStage: "等待手动解析",
|
|
parseError: undefined,
|
|
fileName: file.name,
|
|
fileUrl: uploadedFile.url,
|
|
filePath: uploadedFile.pathname,
|
|
previewUrl: uploadedPreview.url,
|
|
previewPath: uploadedPreview.pathname,
|
|
renderBaseUrl: undefined,
|
|
renderBasePath: undefined,
|
|
width: 1,
|
|
height: 1,
|
|
layerCount: 0,
|
|
layers: [] as AgentPsdTemplateLayerSlot[],
|
|
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 模板上传或解析失败");
|
|
}
|
|
}
|