import "server-only"; import { upsertSystemSetting } from "@/lib/db/queries"; import { AGENT_PSD_TEMPLATES_KEY, AGENT_PSD_TEMPLATE_ASSET_VERSION, isAgentPsdImageLayerSlot, isAgentPsdTextLayerSlot, type AgentPsdTemplate, type AgentPsdTemplateLayerSlot, } from "@/lib/agents/psd-template-config"; import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache"; import { preparePsdTemplateRenderAssets } from "@/lib/agents/psd-template-parser"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; import { createStorageUploadSession } from "@/lib/storage/service"; type PsdTemplateAssetBackfillJobState = { running: boolean; rerun: boolean; }; declare global { // eslint-disable-next-line no-var var __agentPsdTemplateAssetBackfillJobs: | Map | undefined; } function getJobMap() { if (!globalThis.__agentPsdTemplateAssetBackfillJobs) { globalThis.__agentPsdTemplateAssetBackfillJobs = new Map(); } return globalThis.__agentPsdTemplateAssetBackfillJobs; } 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 后台资产补全失败"; } async function updateTemplateById(params: { templateId: string; updatedBy: string; updater: (template: AgentPsdTemplate) => AgentPsdTemplate; }) { const templates = await readAgentPsdTemplates(); const currentTemplate = templates.find((entry) => entry.id === params.templateId); if (!currentTemplate) { return null; } const nextTemplate = params.updater(currentTemplate); await upsertSystemSetting({ key: AGENT_PSD_TEMPLATES_KEY, value: templates.map((entry) => (entry.id === params.templateId ? nextTemplate : entry)), updatedBy: params.updatedBy, }); return nextTemplate; } function mergePreparedLayerAssets(params: { currentLayers: AgentPsdTemplateLayerSlot[]; preparedLayers: AgentPsdTemplateLayerSlot[]; }) { const preparedLayerMap = new Map( params.preparedLayers.map((layer) => [layer.id, layer] as const) ); return params.currentLayers.map((layer) => { const preparedLayer = preparedLayerMap.get(layer.id); if (!preparedLayer) { return layer; } if (isAgentPsdImageLayerSlot(layer) && isAgentPsdImageLayerSlot(preparedLayer)) { return { ...layer, previewUrl: preparedLayer.previewUrl || layer.previewUrl, previewPath: preparedLayer.previewPath || layer.previewPath, sourceUrl: preparedLayer.sourceUrl || layer.sourceUrl, sourcePath: preparedLayer.sourcePath || layer.sourcePath, contentBounds: preparedLayer.contentBounds || layer.contentBounds, maskUrl: preparedLayer.maskUrl, maskPath: preparedLayer.maskPath, } satisfies AgentPsdTemplateLayerSlot; } if (isAgentPsdTextLayerSlot(layer) && isAgentPsdTextLayerSlot(preparedLayer)) { let nextLayer: AgentPsdTemplateLayerSlot = { ...layer, previewUrl: preparedLayer.previewUrl || layer.previewUrl, previewPath: preparedLayer.previewPath || layer.previewPath, sourceUrl: preparedLayer.sourceUrl || layer.sourceUrl, sourcePath: preparedLayer.sourcePath || layer.sourcePath, }; if (layer.styleSource !== "custom") { nextLayer = { ...nextLayer, textColor: layer.textColor === "#111111" && preparedLayer.textColor ? preparedLayer.textColor : layer.textColor, textAlign: layer.textAlign === "left" && preparedLayer.textAlign !== "left" ? preparedLayer.textAlign : layer.textAlign, paddingX: layer.paddingX > 0 ? layer.paddingX : preparedLayer.paddingX, paddingY: layer.paddingY > 0 ? layer.paddingY : preparedLayer.paddingY, fontId: layer.fontId || preparedLayer.fontId, fontFamily: layer.fontFamily || preparedLayer.fontFamily, fontLabel: layer.fontLabel || preparedLayer.fontLabel, fontFileUrl: layer.fontFileUrl || preparedLayer.fontFileUrl, fontContentType: layer.fontContentType || preparedLayer.fontContentType, } satisfies AgentPsdTemplateLayerSlot; } return nextLayer; } return layer; }); } async function backfillTemplateAssetsOnce(params: { templateId: string; updatedBy: string; }) { const templates = await readAgentPsdTemplates(); const template = templates.find((entry) => entry.id === params.templateId); if (!template || template.parseStatus !== "processing") { return; } if (template.layers.length === 0 || template.width <= 1 || template.height <= 1) { throw new Error("PSD 模板尚未完成快速解析"); } await updateTemplateById({ templateId: template.id, updatedBy: params.updatedBy, updater: (currentTemplate) => ({ ...currentTemplate, parseStatus: "processing", parseProgress: Math.max(62, Number(currentTemplate.parseProgress || 0)), parseStage: "快速解析完成,正在后台补全图层资源", parseError: undefined, updatedAt: new Date().toISOString(), }), }); const uploadAsset = await createStorageUploadSession(); const preparedAssets = await preparePsdTemplateRenderAssets({ templateId: template.id, fileUrl: template.fileUrl, filePath: resolveAgentPsdLocalCachePath({ templateId: template.id, fileName: template.fileName, }), layers: template.layers, uploadAsset, }); await updateTemplateById({ templateId: template.id, updatedBy: params.updatedBy, updater: (currentTemplate) => ({ ...currentTemplate, parseStatus: "processing", parseProgress: Math.max(88, Number(currentTemplate.parseProgress || 0)), parseStage: "正在后台生成模板底图", parseError: undefined, updatedAt: new Date().toISOString(), }), }); const uploadedRenderBase = await uploadAsset( `agent-psd-base-${template.id}-${Date.now()}.png`, toArrayBuffer(preparedAssets.renderBaseBuffer) ); await updateTemplateById({ templateId: template.id, updatedBy: params.updatedBy, updater: (currentTemplate) => { const mergedLayers = mergePreparedLayerAssets({ currentLayers: currentTemplate.layers, preparedLayers: preparedAssets.layers, }); return { ...currentTemplate, renderBaseUrl: uploadedRenderBase.url, renderBasePath: uploadedRenderBase.pathname, layers: mergedLayers, layerCount: mergedLayers.length, assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION, parseStatus: "completed", parseProgress: 100, parseStage: "快速解析和可用资产补全已完成", parseError: undefined, updatedAt: new Date().toISOString(), }; }, }); } export function schedulePsdTemplateAssetBackfill(params: { templateId: string; updatedBy: string; }) { const jobs = getJobMap(); const existingJob = jobs.get(params.templateId); if (existingJob?.running) { existingJob.rerun = true; return; } const jobState: PsdTemplateAssetBackfillJobState = { running: true, rerun: false, }; jobs.set(params.templateId, jobState); queueMicrotask(() => { void (async () => { try { do { jobState.rerun = false; await backfillTemplateAssetsOnce(params); } while (jobState.rerun); } catch (error) { const message = getErrorMessage(error); await updateTemplateById({ templateId: params.templateId, updatedBy: params.updatedBy, updater: (currentTemplate) => ({ ...currentTemplate, parseStatus: "failed", parseProgress: Math.min(95, Math.max(45, Number(currentTemplate.parseProgress || 0))), parseStage: "快速解析已完成,但后台补全资产失败", parseError: message, updatedAt: new Date().toISOString(), }), }).catch(() => undefined); console.error("[PSD Template] background asset backfill failed:", { templateId: params.templateId, error, }); } finally { jobs.delete(params.templateId); } })(); }); }