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 模板上传或解析失败"); } }