import "server-only"; import { AGENT_PSD_TEMPLATE_ASSET_VERSION, isAgentPsdImageLayerSlot, isAgentPsdTextLayerSlot, type AgentPsdTemplateLayerSlot, } from "@/lib/agents/psd-template-config"; import { createAgentPsdTemplateParseHeartbeat, updateAgentPsdTemplateById, updateAgentPsdTemplateParseProgress, } from "@/lib/agents/psd-template-progress"; import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache"; import { preparePsdTemplateRenderAssets, uploadPreparedPsdTemplateRenderSegments, } 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 后台资产补全失败"; } 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 模板尚未完成快速解析"); } let latestProgress = { progress: Math.max(62, Number(template.parseProgress || 0)), stage: "快速解析完成,正在后台补全图层资源", error: undefined as string | undefined, }; const setBackfillProgress = async ( progress: number, stage: string, error?: string ) => { latestProgress = { progress, stage, error, }; await updateAgentPsdTemplateParseProgress({ templateId: template.id, updatedBy: params.updatedBy, progress, stage, error, }); }; const heartbeat = createAgentPsdTemplateParseHeartbeat({ templateId: template.id, updatedBy: params.updatedBy, getSnapshot: () => latestProgress, }); await setBackfillProgress(latestProgress.progress, latestProgress.stage); try { 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, onProgress: async (event) => { await setBackfillProgress(event.progress, event.stage); }, }); await setBackfillProgress(88, "正在后台生成模板底图和快速导出分层"); const uploadedRenderBase = await uploadAsset( `agent-psd-base-${template.id}-${Date.now()}.png`, toArrayBuffer(preparedAssets.renderBaseBuffer) ); await setBackfillProgress(96, "正在上传快速导出分层"); const uploadedRenderSegments = await uploadPreparedPsdTemplateRenderSegments({ templateId: template.id, segments: preparedAssets.renderSegments, uploadAsset, }); heartbeat.stop(); await updateAgentPsdTemplateById({ templateId: template.id, updatedBy: params.updatedBy, updater: (currentTemplate) => { const now = new Date().toISOString(); const mergedLayers = mergePreparedLayerAssets({ currentLayers: currentTemplate.layers, preparedLayers: preparedAssets.layers, }); return { ...currentTemplate, renderBaseUrl: uploadedRenderBase.url, renderBasePath: uploadedRenderBase.pathname, renderSegments: uploadedRenderSegments, layers: mergedLayers, layerCount: mergedLayers.length, assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION, parseStatus: "completed", parseProgress: 100, parseStage: "快速解析和可用资产补全已完成", parseError: undefined, parseHeartbeatAt: now, updatedAt: now, }; }, }); } catch (error) { heartbeat.stop(); throw error; } } 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 updateAgentPsdTemplateById({ 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); } })(); }); }