366 lines
11 KiB
TypeScript
366 lines
11 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,
|
|
getEditableAgentPsdTemplateLayers,
|
|
isAgentPsdTextLayerSlot,
|
|
normalizeAgentPsdTemplateStatus,
|
|
parseAgentPsdTemplateLayers,
|
|
type AgentPsdTemplateMutationResponse,
|
|
} from "@/lib/agents/psd-template-config";
|
|
import {
|
|
parsePsdTemplateFileFromUrl,
|
|
preparePsdTemplateRenderAssets,
|
|
} from "@/lib/agents/psd-template-parser";
|
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
|
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),
|
|
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 });
|
|
}
|
|
|
|
async function repairTemplateLayerPreviewsIfNeeded(template: NonNullable<Awaited<ReturnType<typeof readAgentPsdTemplates>>[number]>) {
|
|
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) =>
|
|
isAgentPsdTextLayerSlot(layer) &&
|
|
(!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 (!isAgentPsdTextLayerSlot(layer)) 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 (!isAgentPsdTextLayerSlot(layer) || layer.fontId) {
|
|
return layer;
|
|
}
|
|
|
|
const matchedFont = findBestMatchingPsdFont({
|
|
detectedFontNames: [
|
|
...((layer.detectedFontNames || []).filter(Boolean)),
|
|
...(layer.detectedFontName ? [layer.detectedFontName] : []),
|
|
],
|
|
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(),
|
|
};
|
|
} catch (error) {
|
|
console.warn("[PSD Templates][Admin] failed to repair missing layer previews:", {
|
|
templateId: template.id,
|
|
error,
|
|
});
|
|
return template;
|
|
}
|
|
}
|
|
|
|
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 repairTemplateLayerPreviewsIfNeeded(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);
|
|
let nextLayers = parseAgentPsdTemplateLayers(body.layers);
|
|
if (nextLayers.length === 0) {
|
|
nextLayers = currentTemplate.layers;
|
|
}
|
|
|
|
if (currentTemplate.parseStatus !== "completed") {
|
|
if (nextStatus === "active") {
|
|
return jsonError(400, "请先解析 PSD 模板并配置可编辑图层后再启用模板");
|
|
}
|
|
|
|
const updatedTemplate = {
|
|
...currentTemplate,
|
|
name: body.name.trim(),
|
|
brandName: body.brandName.trim(),
|
|
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) {
|
|
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,
|
|
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(),
|
|
description: String(body.description || "").trim() || undefined,
|
|
status: nextStatus,
|
|
renderBaseUrl: uploadedRenderBase.url,
|
|
renderBasePath: uploadedRenderBase.pathname,
|
|
layers: nextLayers,
|
|
layerCount: Math.max(currentTemplate.layerCount, nextLayers.length),
|
|
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 模板删除失败");
|
|
}
|
|
}
|