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>[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; 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 模板删除失败"); } }