Your Name db46934025
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: stabilize PSD template parsing workflow
2026-04-10 21:03:59 +08:00

287 lines
9.4 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { upsertSystemSetting } from "@/lib/db/queries";
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
import {
AGENT_PSD_TEMPLATES_KEY,
AGENT_PSD_TEMPLATE_ASSET_VERSION,
getEditableAgentPsdTemplateLayers,
isAgentPsdTextLayerSlot,
normalizeAgentPsdTemplateMode,
normalizeAgentPsdTemplateStatus,
parseAgentPsdTemplateLayers,
type AgentPsdTemplateMutationResponse,
} from "@/lib/agents/psd-template-config";
import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config";
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
import {
preparePsdTemplateRenderAssets,
} from "@/lib/agents/psd-template-parser";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
import { uploadFile } from "@/lib/storage/service";
import {
readAgentPsdTemplates,
tryDeleteAgentPsdTemplateAssets,
} from "@/lib/agents/psd-template-settings";
export const runtime = "nodejs";
const updateTemplateSchema = z.object({
name: z.string().trim().min(1).max(80),
brandName: z.string().trim().min(1).max(64),
mode: z.string().optional().nullable(),
secondaryCategory: z.string().trim().max(64).optional().nullable(),
categoryTags: z.array(z.string().trim().min(1).max(64)).max(30).optional().nullable(),
description: z.string().optional().nullable(),
status: z.string().optional(),
layers: z.array(z.any()).default([]),
});
function jsonError(status: number, error: string) {
const payload: AgentPsdTemplateMutationResponse = {
success: false,
error,
};
return NextResponse.json(payload, { status });
}
export async function GET(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session?.user?.id) {
return jsonError(401, "未登录");
}
try {
const { id } = await context.params;
const templates = await readAgentPsdTemplates();
const template = templates.find((entry) => entry.id === id);
if (!template) {
return jsonError(404, "未找到 PSD 模板");
}
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
if (repairedTemplate !== template) {
await upsertSystemSetting({
key: AGENT_PSD_TEMPLATES_KEY,
value: templates.map((entry) => (entry.id === id ? repairedTemplate : entry)),
updatedBy: session.user.id,
});
}
return NextResponse.json({
success: true,
template: repairedTemplate,
} satisfies AgentPsdTemplateMutationResponse);
} catch (error) {
console.error("[PSD Templates][Admin] get failed:", error);
return jsonError(503, "PSD 模板读取失败");
}
}
export async function PUT(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session?.user?.id) {
return jsonError(401, "未登录");
}
let body: z.infer<typeof updateTemplateSchema>;
try {
body = updateTemplateSchema.parse(await request.json());
} catch {
return jsonError(400, "模板参数不合法");
}
try {
const { id } = await context.params;
const templates = await readAgentPsdTemplates();
const currentTemplate = templates.find((entry) => entry.id === id);
if (!currentTemplate) {
return jsonError(404, "未找到 PSD 模板");
}
const nextStatus = normalizeAgentPsdTemplateStatus(body.status);
const nextMode = normalizeAgentPsdTemplateMode(body.mode);
const nextCategoryTags = normalizeAgentPsdTemplateCategoryTags(body.categoryTags);
let nextLayers = parseAgentPsdTemplateLayers(body.layers);
if (currentTemplate.parseStatus !== "completed") {
if (nextStatus === "active") {
return jsonError(400, "请先解析 PSD 模板并配置可编辑图层后再启用模板");
}
const updatedTemplate = {
...currentTemplate,
name: body.name.trim(),
brandName: body.brandName.trim(),
mode: nextMode,
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
categoryTags: nextCategoryTags.length > 0 ? nextCategoryTags : undefined,
description: String(body.description || "").trim() || undefined,
status: nextStatus,
updatedAt: new Date().toISOString(),
};
await upsertSystemSetting({
key: AGENT_PSD_TEMPLATES_KEY,
value: templates.map((template) =>
template.id === currentTemplate.id ? updatedTemplate : template
),
updatedBy: session.user.id,
});
return NextResponse.json({
success: true,
template: updatedTemplate,
} satisfies AgentPsdTemplateMutationResponse);
}
if (
nextStatus === "active" &&
getEditableAgentPsdTemplateLayers({
...currentTemplate,
layers: nextLayers,
}).length === 0
) {
return jsonError(400, "至少配置一个可编辑图层后才能启用模板");
}
const fonts = await readAgentPsdFonts();
const fontMap = new Map(fonts.map((font) => [font.id, font]));
for (const layer of nextLayers) {
if (!layer.isEditable || !isAgentPsdTextLayerSlot(layer)) {
continue;
}
if (!layer.fontId) {
const matchedFont = findBestMatchingPsdFont({
detectedFontNames: [
...((layer.detectedFontNames || []).filter(Boolean)),
...(layer.detectedFontName ? [layer.detectedFontName] : []),
],
fonts,
});
if (matchedFont?.confidence === "high") {
layer.fontId = matchedFont.font.id;
layer.fontFamily = matchedFont.font.family;
layer.fontLabel = matchedFont.font.name;
layer.fontFileUrl = matchedFont.font.fileUrl;
layer.fontContentType = matchedFont.font.contentType;
}
}
if (layer.fontId) {
const matchedFont = fontMap.get(layer.fontId);
if (!matchedFont) {
return jsonError(400, `文字图层“${layer.layerName}”选择的字体不存在`);
}
layer.fontFamily = matchedFont.family;
layer.fontLabel = matchedFont.name;
layer.fontFileUrl = matchedFont.fileUrl;
layer.fontContentType = matchedFont.contentType;
}
if (!String(layer.fontFamily || "").trim()) {
return jsonError(400, `文字图层“${layer.layerName}”必须选择字体`);
}
}
const preparedAssets = await preparePsdTemplateRenderAssets({
templateId: currentTemplate.id,
fileUrl: currentTemplate.fileUrl,
filePath: resolveAgentPsdLocalCachePath({
templateId: currentTemplate.id,
fileName: currentTemplate.fileName,
}),
layers: nextLayers,
});
nextLayers = preparedAssets.layers;
const uploadedRenderBase = await uploadFile(
`agent-psd-base-${currentTemplate.id}-${Date.now()}.png`,
preparedAssets.renderBaseBuffer.buffer.slice(
preparedAssets.renderBaseBuffer.byteOffset,
preparedAssets.renderBaseBuffer.byteOffset + preparedAssets.renderBaseBuffer.byteLength
) as ArrayBuffer
);
const updatedTemplate = {
...currentTemplate,
name: body.name.trim(),
brandName: body.brandName.trim(),
mode: nextMode,
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
categoryTags: nextCategoryTags.length > 0 ? nextCategoryTags : undefined,
description: String(body.description || "").trim() || undefined,
status: nextStatus,
renderBaseUrl: uploadedRenderBase.url,
renderBasePath: uploadedRenderBase.pathname,
layers: nextLayers,
layerCount: nextLayers.length,
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
updatedAt: new Date().toISOString(),
};
await upsertSystemSetting({
key: AGENT_PSD_TEMPLATES_KEY,
value: templates.map((template) =>
template.id === currentTemplate.id ? updatedTemplate : template
),
updatedBy: session.user.id,
});
return NextResponse.json({
success: true,
template: updatedTemplate,
} satisfies AgentPsdTemplateMutationResponse);
} catch (error) {
console.error("[PSD Templates][Admin] update failed:", error);
return jsonError(502, "PSD 模板保存失败");
}
}
export async function DELETE(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session?.user?.id) {
return jsonError(401, "未登录");
}
try {
const { id } = await context.params;
const templates = await readAgentPsdTemplates();
const template = templates.find((entry) => entry.id === id);
if (!template) {
return jsonError(404, "未找到 PSD 模板");
}
await upsertSystemSetting({
key: AGENT_PSD_TEMPLATES_KEY,
value: templates.filter((entry) => entry.id !== id),
updatedBy: session.user.id,
});
await tryDeleteAgentPsdTemplateAssets(template);
return NextResponse.json({
success: true,
template,
} satisfies AgentPsdTemplateMutationResponse);
} catch (error) {
console.error("[PSD Templates][Admin] delete failed:", error);
return jsonError(502, "PSD 模板删除失败");
}
}