import "server-only"; import { upsertSystemSetting } from "@/lib/db/queries"; import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement"; import { type AgentPsdTemplateRenderRequest } from "@/lib/agents/psd-render-task-contract"; import { AGENT_PSD_TEMPLATES_KEY, type AgentPsdTemplateRenderResult, getAgentPsdTemplateRuntimeIssue, getEditableAgentPsdTemplateLayers, isAgentPsdImageLayerSlot, isAgentPsdLayerSuperLocked, isAgentPsdTemplateReady, isAgentPsdTemplateRuntimeReady, isAgentPsdTemplateLayerUserEditable, isAgentPsdTextLayerSlot, } from "@/lib/agents/psd-template-config"; import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer"; import { repairAgentPsdTemplateAssetsIfNeeded, repairAgentPsdTemplateFontBindings, } from "@/lib/agents/psd-template-repair"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; import { normalizeHexColor } from "@/lib/agents/psd-text-layout"; type AgentPsdTextPositionOffset = { offsetX: number; offsetY: number; }; type AgentPsdLayerPositionOffset = AgentPsdTextPositionOffset; function clampTextPositionOffsetValue(value: number) { if (!Number.isFinite(value)) { return 0; } return Math.max(-5000, Math.min(5000, value)); } function normalizeTextPositionOffset( value?: Partial | null ): AgentPsdTextPositionOffset { return { offsetX: Number( clampTextPositionOffsetValue(Number(value?.offsetX || 0)).toFixed(2) ), offsetY: Number( clampTextPositionOffsetValue(Number(value?.offsetY || 0)).toFixed(2) ), }; } export async function executeAgentPsdTemplateRenderRequest( body: AgentPsdTemplateRenderRequest ): Promise { const templates = await readAgentPsdTemplates(); const template = templates.find( (entry) => entry.id === body.templateId && entry.status === "active" && isAgentPsdTemplateReady(entry) ); if (!template) { throw new Error("PSD 模板不存在或未启用"); } const fonts = await readAgentPsdFonts(); const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template); if (repairedTemplate !== template) { await upsertSystemSetting({ key: AGENT_PSD_TEMPLATES_KEY, value: templates.map((entry) => entry.id === template.id ? repairedTemplate : entry ), updatedBy: "system:psd-template-repair", }).catch((error) => { console.warn("[PSD Template] failed to persist repaired assets:", { templateId: template.id, error, }); }); } const resolvedTemplate = repairAgentPsdTemplateFontBindings( repairedTemplate, fonts ); if (!isAgentPsdTemplateRuntimeReady(resolvedTemplate)) { throw new Error( getAgentPsdTemplateRuntimeIssue(resolvedTemplate) || "PSD 模板资源不完整,请在后台重新解析后再导出" ); } const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate); if (editableLayers.length === 0) { throw new Error("当前模板未配置可编辑图层"); } const hiddenLayerIdSet = new Set( (body.hiddenLayerIds || []) .map((layerId) => String(layerId || "").trim()) .filter(Boolean) ); for (const layer of resolvedTemplate.layers) { if (isAgentPsdLayerSuperLocked(layer)) { hiddenLayerIdSet.delete(layer.id); } } const normalizedLayerOrderIds = Array.from( new Set( (body.layerOrderIds || []) .map((layerId) => String(layerId || "").trim()) .filter(Boolean) ) ); const fontMap = new Map(fonts.map((font) => [font.id, font])); const missingFontLayer = editableLayers.find( (layer) => !hiddenLayerIdSet.has(layer.id) && isAgentPsdTextLayerSlot(layer) && layer.fontId && !fontMap.has(layer.fontId) ); if (missingFontLayer) { throw new Error( `文字图层“${missingFontLayer.layerName}”引用的字体已不存在` ); } const imageAssignments: Record = {}; const imagePlacements: Record< string, ReturnType > = {}; const textAssignments: Record = {}; const textColorAssignments: Record = {}; const textPositionOffsets: Record = {}; const layerPositionOffsets: Record = {}; for (const layer of resolvedTemplate.layers) { if (hiddenLayerIdSet.has(layer.id)) { continue; } if (!isAgentPsdLayerSuperLocked(layer)) { layerPositionOffsets[layer.id] = normalizeTextPositionOffset( body.layerPositionOffsets[layer.id] ); } if (!isAgentPsdTemplateLayerUserEditable(layer)) { continue; } 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)) { if (Object.hasOwn(body.textAssignments, layer.id)) { textAssignments[layer.id] = String( body.textAssignments[layer.id] ?? "" ).trim(); } textColorAssignments[layer.id] = normalizeHexColor( body.textColorAssignments[layer.id] || layer.textColor, layer.textColor ); textPositionOffsets[layer.id] = normalizeTextPositionOffset( body.textPositionOffsets[layer.id] ); } } const missingRequiredLayer = editableLayers.find( (layer) => !hiddenLayerIdSet.has(layer.id) && layer.isRequired && (isAgentPsdImageLayerSlot(layer) ? !imageAssignments[layer.id] : !Object.hasOwn(textAssignments, layer.id) && !String(layer.defaultText || "").trim()) ); if (missingRequiredLayer) { throw new Error(`缺少必传素材:${missingRequiredLayer.layerName}`); } const uploaded = await renderAgentPsdTemplate({ template: resolvedTemplate, hiddenLayerIds: Array.from(hiddenLayerIdSet), layerOrderIds: normalizedLayerOrderIds, imageAssignments, imagePlacements, textAssignments, textColorAssignments, layerPositionOffsets, textPositionOffsets, fonts, }); return { url: uploaded.url, pathname: uploaded.pathname, contentType: uploaded.contentType, }; }