import { NextResponse } from "next/server"; import { upsertSystemSetting } from "@/lib/db/queries"; import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin"; import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching"; import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; import { AGENT_PSD_TEMPLATES_KEY, type AgentPsdTemplate, type AgentPsdTemplateLayerSlot, type AgentPsdTemplateMutationResponse, } from "@/lib/agents/psd-template-config"; import { type ParsedPsdTemplateLayer, parsePsdTemplateFile, } from "@/lib/agents/psd-template-parser"; import { pickPrimaryPsdImageLayerId } from "@/lib/agents/psd-template-image-slot-selection"; import { schedulePsdTemplateAssetBackfill } from "@/lib/agents/psd-template-asset-backfill"; import { cacheAgentPsdSourceLocally, hasAgentPsdLocalCache, readAgentPsdSourceFromLocalCache, } from "@/lib/agents/psd-local-cache"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; import { failTimedOutAgentPsdTemplates } from "@/lib/agents/psd-template-timeout"; import { uploadFile } from "@/lib/storage/service"; export const runtime = "nodejs"; function jsonError(status: number, error: string) { const payload: AgentPsdTemplateMutationResponse = { success: false, error, }; return NextResponse.json(payload, { status }); } function toArrayBuffer(buffer: Buffer) { return buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; } function getErrorMessage(error: unknown) { if (error instanceof Error && error.message.trim()) { return error.message.trim(); } return "PSD 解析失败"; } function pickPrimaryImageLayerId(params: { parsedLayers: Awaited>["layers"]; canvasWidth: number; canvasHeight: number; }) { const { parsedLayers, canvasWidth, canvasHeight } = params; return pickPrimaryPsdImageLayerId({ imageLayers: parsedLayers.filter((layer) => layer.slotType === "image"), canvasWidth, canvasHeight, }); } function shouldAutoEditableTextLayer( layer: ParsedPsdTemplateLayer ) { const text = String(layer.defaultText || "").replace(/\s+/g, ""); if (!text) { return false; } return ( layer.width >= 80 || layer.height >= 30 || text.length >= 4 || /[A-Za-z0-9]{3,}/.test(text) ); } function buildInitialTemplateLayers(params: { parsedLayers: Awaited>["layers"]; fonts: Awaited>; canvasWidth: number; canvasHeight: number; }) { const { parsedLayers, fonts, canvasWidth, canvasHeight } = params; const primaryImageLayerId = pickPrimaryImageLayerId({ parsedLayers, canvasWidth, canvasHeight, }); return parsedLayers.map((layer): AgentPsdTemplateLayerSlot => { if (layer.slotType === "text") { const matchedFont = findBestMatchingPsdFont({ detectedFontNames: layer.detectedFontNames || [], fonts, }); const autoBoundFont = matchedFont?.confidence === "high" ? matchedFont.font : undefined; const isEditable = shouldAutoEditableTextLayer(layer); return { ...layer, slotType: "text", styleSource: "auto", isEditable, isRequired: false, placeholder: layer.defaultText || layer.layerName, detectedFontName: layer.detectedFontName, detectedFontNames: layer.detectedFontNames, fontId: autoBoundFont?.id, fontFamily: autoBoundFont?.family || layer.detectedFontNames?.[0] || layer.detectedFontName, fontLabel: autoBoundFont?.name || layer.detectedFontNames?.[0] || layer.detectedFontName, fontFileUrl: autoBoundFont?.fileUrl, fontContentType: autoBoundFont?.contentType, fontSize: Math.max( 10, Math.round( Number( layer.detectedFontSize || Math.max(16, Math.min(layer.height, Math.round(layer.height * 0.78))) ) ) ), lineHeight: Number.isFinite(Number(layer.detectedLineHeight)) && Number(layer.detectedLineHeight) > 0 ? Number(layer.detectedLineHeight) : 1.2, letterSpacing: Number.isFinite(Number(layer.detectedLetterSpacing)) ? Number(layer.detectedLetterSpacing) : 0, fontWeight: 500, textColor: layer.detectedTextColor || "#111111", textAlign: "left", paddingX: 0, paddingY: 0, maxLines: 4, }; } const isPrimaryImageLayer = layer.id === primaryImageLayerId; return { ...layer, slotType: "image", isEditable: isPrimaryImageLayer, isRequired: false, isDefaultResultSource: isPrimaryImageLayer, allowUserUploadOverride: isPrimaryImageLayer, replaceMode: "cover", }; }); } async function updateTemplate( templateId: string, updatedBy: string, updater: (template: AgentPsdTemplate) => AgentPsdTemplate ) { const templates = await readAgentPsdTemplates(); const currentTemplate = templates.find((entry) => entry.id === templateId); if (!currentTemplate) { return null; } const nextTemplate = updater(currentTemplate); await upsertSystemSetting({ key: AGENT_PSD_TEMPLATES_KEY, value: templates.map((entry) => (entry.id === templateId ? nextTemplate : entry)), updatedBy, }); return nextTemplate; } async function updateParseProgress(params: { templateId: string; updatedBy: string; progress: number; stage: string; error?: string; }) { const { templateId, updatedBy, progress, stage, error } = params; return updateTemplate(templateId, updatedBy, (template) => ({ ...template, parseStatus: "processing", parseProgress: progress, parseStage: stage, parseError: error, updatedAt: new Date().toISOString(), })); } export async function POST( _request: Request, context: { params: Promise<{ id: string }> } ) { const session = await requireAgentWorkflowAdmin().catch(() => null); if (!session?.user?.id) { return jsonError(401, "未登录"); } const { id } = await context.params; try { const { templates, timedOutTemplates } = await failTimedOutAgentPsdTemplates({ updatedBy: session.user.id, }); if (timedOutTemplates.length > 0) { console.warn("[PSD Templates][Admin] cleared timed out parsing templates before parse:", { requestedTemplateId: id, count: timedOutTemplates.length, templateIds: timedOutTemplates.map((template) => template.id), }); } const currentTemplate = templates.find((entry) => entry.id === id); if (!currentTemplate) { return jsonError(404, "未找到 PSD 模板"); } if (currentTemplate.parseStatus === "processing") { return jsonError(409, "该模板正在解析中,请稍后刷新"); } await updateParseProgress({ templateId: id, updatedBy: session.user.id, progress: 5, stage: "准备解析 PSD", error: undefined, }); let fileBuffer: ArrayBuffer; if ( await hasAgentPsdLocalCache({ templateId: id, fileName: currentTemplate.fileName, }) ) { await updateParseProgress({ templateId: id, updatedBy: session.user.id, progress: 18, stage: "正在读取服务器本地 PSD 缓存", error: undefined, }); const localBuffer = await readAgentPsdSourceFromLocalCache({ templateId: id, fileName: currentTemplate.fileName, }); fileBuffer = localBuffer.buffer.slice( localBuffer.byteOffset, localBuffer.byteOffset + localBuffer.byteLength ) as ArrayBuffer; } else { const response = await fetch(currentTemplate.fileUrl, { cache: "no-store", signal: AbortSignal.timeout(60_000), }); if (!response.ok) { throw new Error(`PSD 文件下载失败 (${response.status})`); } await updateParseProgress({ templateId: id, updatedBy: session.user.id, progress: 18, stage: "正在下载并写入服务器本地 PSD 缓存", error: undefined, }); fileBuffer = await response.arrayBuffer(); await cacheAgentPsdSourceLocally({ templateId: id, fileName: currentTemplate.fileName, buffer: fileBuffer, }).catch((error) => { console.warn("[PSD Templates][Admin] failed to cache PSD source locally:", { templateId: id, error, }); }); } const parsedTemplate = await parsePsdTemplateFile(fileBuffer); if (parsedTemplate.layers.length === 0) { throw new Error("PSD 中未识别到可配置图层"); } await updateParseProgress({ templateId: id, updatedBy: session.user.id, progress: 42, stage: "正在生成模板预览", error: undefined, }); const uploadedPreview = await uploadFile( `agent-psd-preview-${id}-${Date.now()}.png`, toArrayBuffer(parsedTemplate.previewBuffer) ); await updateParseProgress({ templateId: id, updatedBy: session.user.id, progress: 58, stage: "正在匹配字体与初始化图层", error: undefined, }); const fonts = await readAgentPsdFonts().catch(() => []); const initialLayers = buildInitialTemplateLayers({ parsedLayers: parsedTemplate.layers, fonts, canvasWidth: parsedTemplate.width, canvasHeight: parsedTemplate.height, }); const updatedTemplate = await updateTemplate(id, session.user.id, (template) => ({ ...template, previewUrl: uploadedPreview.url, previewPath: uploadedPreview.pathname, width: parsedTemplate.width, height: parsedTemplate.height, layerCount: parsedTemplate.layers.length, layers: initialLayers, parseStatus: "processing", parseProgress: 48, parseStage: "快速解析完成,正在后台补全图层资源和模板底图", parseError: undefined, updatedAt: new Date().toISOString(), })); if (!updatedTemplate) { return jsonError(404, "未找到 PSD 模板"); } schedulePsdTemplateAssetBackfill({ templateId: id, updatedBy: session.user.id, }); return NextResponse.json({ success: true, template: updatedTemplate, } satisfies AgentPsdTemplateMutationResponse); } catch (error) { const message = getErrorMessage(error); const existingTemplate = (await readAgentPsdTemplates()).find((entry) => entry.id === id); if (existingTemplate) { const fallbackStatus = existingTemplate.parseStatus === "completed" && existingTemplate.layers.length > 0 ? "completed" : "failed"; await updateTemplate(id, session.user.id, (template) => ({ ...template, parseStatus: fallbackStatus, parseProgress: fallbackStatus === "completed" ? 100 : 0, parseStage: fallbackStatus === "completed" ? "沿用上次成功解析结果" : undefined, parseError: fallbackStatus === "completed" ? `重新解析失败:${message}` : message, updatedAt: new Date().toISOString(), })).catch(() => undefined); } console.error("[PSD Templates][Admin] parse failed:", error); return jsonError(502, message); } }