278 lines
8.5 KiB
TypeScript
278 lines
8.5 KiB
TypeScript
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<string, PsdTemplateAssetBackfillJobState>
|
|
| 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);
|
|
}
|
|
})();
|
|
});
|
|
}
|