import { NextResponse } from "next/server"; import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin"; import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching"; import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; import { 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 { AGENT_PSD_TEMPLATE_INCOMPATIBLE_PSB_MESSAGE, AGENT_PSD_TEMPLATE_INVALID_FORMAT_MESSAGE, assertSupportedAgentPsdTemplateFile, type AgentPsdTemplateFileInspection, } from "@/lib/agents/psd-template-format"; import { createAgentPsdTemplateParseHeartbeat, updateAgentPsdTemplateById, updateAgentPsdTemplateParseProgress, } from "@/lib/agents/psd-template-progress"; 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"; export const maxDuration = 1800; 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 getPsdTemplateParseErrorMessage(params: { error: unknown; fileInspection?: AgentPsdTemplateFileInspection; }) { const message = getErrorMessage(params.error); if ( params.fileInspection?.format === "psb" && /invalid signature/i.test(message) ) { return AGENT_PSD_TEMPLATE_INCOMPATIBLE_PSB_MESSAGE; } return message; } 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 updateParseProgress(params: { templateId: string; updatedBy: string; progress: number; stage: string; error?: string; restart?: boolean; }) { return updateAgentPsdTemplateParseProgress(params); } 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; let fileInspection: AgentPsdTemplateFileInspection | undefined; let latestProgress = { progress: 0, stage: "准备解析 PSD", error: undefined as string | undefined, }; 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, "该模板正在解析中,请稍后刷新"); } latestProgress = { progress: 5, stage: "准备解析 PSD", error: undefined, }; const setParseProgress = async (progress: number, stage: string, error?: string) => { latestProgress = { progress, stage, error, }; return updateParseProgress({ templateId: id, updatedBy: session.user.id, progress, stage, error, }); }; await updateParseProgress({ templateId: id, updatedBy: session.user.id, ...latestProgress, error: undefined, restart: true, }); const heartbeat = createAgentPsdTemplateParseHeartbeat({ templateId: id, updatedBy: session.user.id, getSnapshot: () => latestProgress, }); try { let fileBuffer: ArrayBuffer; if ( await hasAgentPsdLocalCache({ templateId: id, fileName: currentTemplate.fileName, }) ) { await setParseProgress(18, "正在读取服务器本地 PSD 缓存"); 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 setParseProgress(18, "正在下载并写入服务器本地 PSD 缓存"); 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, }); }); } await setParseProgress(22, "正在校验 PSD 文件格式"); fileInspection = assertSupportedAgentPsdTemplateFile(fileBuffer); await setParseProgress(24, "正在解析 PSD 图层结构与预览"); const parsedTemplate = await parsePsdTemplateFile(fileBuffer, { onProgress: async (event) => { await setParseProgress(event.progress, event.stage); }, }); if (parsedTemplate.layers.length === 0) { throw new Error("PSD 中未识别到可配置图层"); } await setParseProgress(42, "正在上传模板预览"); const uploadedPreview = await uploadFile( `agent-psd-preview-${id}-${Date.now()}.png`, toArrayBuffer(parsedTemplate.previewBuffer) ); await setParseProgress(58, "正在匹配字体与初始化图层"); const fonts = await readAgentPsdFonts().catch(() => []); const initialLayers = buildInitialTemplateLayers({ parsedLayers: parsedTemplate.layers, fonts, canvasWidth: parsedTemplate.width, canvasHeight: parsedTemplate.height, }); heartbeat.stop(); const updatedTemplate = await updateAgentPsdTemplateById({ templateId: id, updatedBy: session.user.id, updater: (template) => { const now = new Date().toISOString(); return { ...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, parseHeartbeatAt: now, updatedAt: now, }; }, }); if (!updatedTemplate) { return jsonError(404, "未找到 PSD 模板"); } schedulePsdTemplateAssetBackfill({ templateId: id, updatedBy: session.user.id, }); return NextResponse.json({ success: true, template: updatedTemplate, } satisfies AgentPsdTemplateMutationResponse); } catch (error) { heartbeat.stop(); throw error; } } catch (error) { const message = getPsdTemplateParseErrorMessage({ error, fileInspection, }); const existingTemplate = (await readAgentPsdTemplates()).find((entry) => entry.id === id); if (existingTemplate) { const hasUsablePreviousParse = existingTemplate.layers.length > 0 && existingTemplate.width > 1 && existingTemplate.height > 1 && Boolean(existingTemplate.previewUrl); const fallbackStatus = hasUsablePreviousParse ? "completed" : "failed"; await updateAgentPsdTemplateById({ templateId: id, updatedBy: session.user.id, updater: (template) => ({ ...template, parseStatus: fallbackStatus, parseProgress: fallbackStatus === "completed" ? 100 : Math.max(0, Math.round(Number(latestProgress.progress || 0))), parseStage: fallbackStatus === "completed" ? "沿用上次成功解析结果" : latestProgress.stage || "PSD 解析失败", parseError: fallbackStatus === "completed" ? `重新解析失败:${message}` : message, updatedAt: new Date().toISOString(), }), }).catch(() => undefined); } console.error("[PSD Templates][Admin] parse failed:", { templateId: id, fileName: existingTemplate?.fileName, progress: latestProgress.progress, stage: latestProgress.stage, fileFormat: fileInspection?.format, error, }); return jsonError( message === AGENT_PSD_TEMPLATE_INCOMPATIBLE_PSB_MESSAGE || message === AGENT_PSD_TEMPLATE_INVALID_FORMAT_MESSAGE ? 400 : 502, message ); } }