import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { getEditableAgentPsdTemplateLayers, getAgentPsdTemplateRuntimeIssue, isAgentPsdImageLayerSlot, isAgentPsdTemplateRuntimeReady, isAgentPsdTemplateReady, isAgentPsdTextLayerSlot, type AgentPsdTemplateRenderResponse, } from "@/lib/agents/psd-template-config"; import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement"; import { describeRemoteAssetLocation } from "@/lib/agents/psd-remote-asset"; import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair"; import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer"; import { normalizeHexColor } from "@/lib/agents/psd-text-layout"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; export const runtime = "nodejs"; const renderSchema = z.object({ templateId: z.string().trim().min(1), sourceImageUrl: z.string().trim().optional().nullable(), imageAssignments: z.record(z.string(), z.string()).default({}), imagePlacements: z .record( z.string(), z.object({ scale: z.number().positive().max(8).optional(), offsetX: z.number().optional(), offsetY: z.number().optional(), }) ) .default({}), textAssignments: z.record(z.string(), z.string()).default({}), textColorAssignments: z.record(z.string(), z.string()).default({}), }); function jsonError(status: number, error: string) { const payload: AgentPsdTemplateRenderResponse = { success: false, error, }; return NextResponse.json(payload, { status }); } export async function POST(request: NextRequest) { const session = await auth().catch(() => null); if (!session?.user?.id) { return jsonError(401, "未登录"); } let body: z.infer; try { body = renderSchema.parse(await request.json()); } catch { return jsonError(400, "渲染参数不合法"); } try { const templates = await readAgentPsdTemplates(); const template = templates.find( (entry) => entry.id === body.templateId && entry.status === "active" && isAgentPsdTemplateReady(entry) ); if (!template) { return jsonError(404, "PSD 模板不存在或未启用"); } const fonts = await readAgentPsdFonts(); const resolvedTemplate = repairAgentPsdTemplateFontBindings(template, fonts); if (!isAgentPsdTemplateRuntimeReady(resolvedTemplate)) { return jsonError( 409, getAgentPsdTemplateRuntimeIssue(resolvedTemplate) || "PSD 模板资源不完整,请在后台重新解析后再导出" ); } const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate); if (editableLayers.length === 0) { return jsonError(400, "当前模板未配置可编辑图层"); } const fontMap = new Map(fonts.map((font) => [font.id, font])); const missingFontLayer = editableLayers.find( (layer) => isAgentPsdTextLayerSlot(layer) && layer.fontId && !fontMap.has(layer.fontId) ); if (missingFontLayer) { return jsonError(400, `文字图层“${missingFontLayer.layerName}”引用的字体已不存在`); } const imageAssignments: Record = {}; const imagePlacements: Record> = {}; const textAssignments: Record = {}; const textColorAssignments: Record = {}; for (const layer of editableLayers) { if (isAgentPsdImageLayerSlot(layer)) { const explicitUrl = String(body.imageAssignments[layer.id] || "").trim(); if (explicitUrl) { imageAssignments[layer.id] = explicitUrl; imagePlacements[layer.id] = normalizeAgentPsdImagePlacement(body.imagePlacements[layer.id]); continue; } if (layer.isDefaultResultSource && body.sourceImageUrl?.trim()) { imageAssignments[layer.id] = body.sourceImageUrl.trim(); imagePlacements[layer.id] = normalizeAgentPsdImagePlacement(body.imagePlacements[layer.id]); } } if (isAgentPsdTextLayerSlot(layer)) { const nextText = String(body.textAssignments[layer.id] || layer.defaultText || "").trim(); if (nextText) { textAssignments[layer.id] = nextText; } textColorAssignments[layer.id] = normalizeHexColor( body.textColorAssignments[layer.id] || layer.textColor, layer.textColor ); } } const missingRequiredLayer = editableLayers.find( (layer) => layer.isRequired && (isAgentPsdImageLayerSlot(layer) ? !imageAssignments[layer.id] : !textAssignments[layer.id]) ); if (missingRequiredLayer) { return jsonError(400, `缺少必传素材:${missingRequiredLayer.layerName}`); } const uploaded = await renderAgentPsdTemplate({ template: resolvedTemplate, imageAssignments, imagePlacements, textAssignments, textColorAssignments, fonts, }); return NextResponse.json({ success: true, result: { url: uploaded.url, pathname: uploaded.pathname, contentType: uploaded.contentType, }, } satisfies AgentPsdTemplateRenderResponse); } catch (error) { console.error("[PSD Templates] render failed:", { templateId: body.templateId, sourceImage: body.sourceImageUrl ? describeRemoteAssetLocation(body.sourceImageUrl) : null, assignedImageCount: Object.keys(body.imageAssignments || {}).length, error, }); const message = error instanceof Error && String(error.message || "").trim() ? error.message : "PSD 模板渲染失败"; return jsonError(502, message); } }