ai-chatbot/lib/agents/psd-template-repair.ts
Your Name fb6b2170bd
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: harden psd template rendering
2026-04-01 22:36:45 +08:00

160 lines
4.9 KiB
TypeScript

import "server-only";
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import {
AGENT_PSD_TEMPLATE_ASSET_VERSION,
isAgentPsdImageLayerSlot,
isAgentPsdTextLayerSlot,
type AgentPsdTemplate,
} from "@/lib/agents/psd-template-config";
import {
parsePsdTemplateFileFromUrl,
preparePsdTemplateRenderAssets,
} from "@/lib/agents/psd-template-parser";
import { uploadFile } from "@/lib/storage/service";
function hasMissingLayerAssets(template: AgentPsdTemplate) {
return template.layers.some(
(layer) =>
!String(layer.previewUrl || "").trim() ||
!String(layer.sourceUrl || "").trim() ||
(isAgentPsdImageLayerSlot(layer) &&
layer.isEditable &&
!String(layer.maskUrl || "").trim())
);
}
function needsTextMetadataRepair(template: AgentPsdTemplate) {
return template.layers.some(
(layer) =>
isAgentPsdTextLayerSlot(layer) &&
(!String(layer.detectedFontName || "").trim() ||
!Array.isArray(layer.detectedFontNames) ||
layer.detectedFontNames.length === 0)
);
}
function hasDedicatedRenderBase(template: AgentPsdTemplate) {
const renderBaseUrl = String(template.renderBaseUrl || "").trim();
const previewUrl = String(template.previewUrl || "").trim();
return Boolean(String(template.renderBasePath || "").trim()) || (
Boolean(renderBaseUrl) &&
renderBaseUrl !== previewUrl
);
}
function toArrayBuffer(buffer: Buffer) {
return buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
) as ArrayBuffer;
}
export async function repairAgentPsdTemplateAssetsIfNeeded(
template: AgentPsdTemplate
) {
if (template.parseStatus !== "completed") {
return template;
}
const needsAssetVersionRefresh =
Number(template.assetVersion || 0) !== AGENT_PSD_TEMPLATE_ASSET_VERSION;
const needsRepair = needsAssetVersionRefresh || hasMissingLayerAssets(template);
const needsRenderBaseRepair = !hasDedicatedRenderBase(template);
const fonts = await readAgentPsdFonts().catch(() => []);
const needsFontDetection = needsTextMetadataRepair(template);
let nextLayers = template.layers;
if (needsFontDetection) {
try {
const parsedTemplate = await parsePsdTemplateFileFromUrl(template.fileUrl);
const parsedTextLayerMap = new Map(
parsedTemplate.layers
.filter((layer) => layer.slotType === "text")
.map((layer) => [layer.layerId, layer])
);
nextLayers = nextLayers.map((layer) => {
if (!isAgentPsdTextLayerSlot(layer)) return layer;
const parsedLayer = parsedTextLayerMap.get(layer.layerId);
if (!parsedLayer) return layer;
return {
...layer,
detectedFontName: parsedLayer.detectedFontName || layer.detectedFontName,
detectedFontNames: parsedLayer.detectedFontNames || layer.detectedFontNames,
};
});
} catch (error) {
console.warn("[PSD Template] failed to repair parsed text metadata:", {
templateId: template.id,
error,
});
}
}
nextLayers = nextLayers.map((layer) => {
if (!isAgentPsdTextLayerSlot(layer) || layer.fontId) {
return layer;
}
const matchedFont = findBestMatchingPsdFont({
detectedFontNames: [
...((layer.detectedFontNames || []).filter(Boolean)),
...(layer.detectedFontName ? [layer.detectedFontName] : []),
],
fonts,
});
if (!matchedFont) {
return layer;
}
return {
...layer,
fontId: matchedFont.font.id,
fontFamily: matchedFont.font.family,
fontLabel: matchedFont.font.name,
fontFileUrl: matchedFont.font.fileUrl,
fontContentType: matchedFont.font.contentType,
};
});
const hasFontRepair = nextLayers.some(
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
);
if (!needsRepair && !hasFontRepair && !needsRenderBaseRepair) {
return template;
}
try {
const preparedAssets = await preparePsdTemplateRenderAssets({
templateId: template.id,
fileUrl: template.fileUrl,
layers: nextLayers,
forceRefreshLayerAssets: needsAssetVersionRefresh,
});
const uploadedRenderBase =
needsRenderBaseRepair
? await uploadFile(
`agent-psd-base-${template.id}-${Date.now()}.png`,
toArrayBuffer(preparedAssets.renderBaseBuffer)
)
: null;
return {
...template,
renderBaseUrl: uploadedRenderBase?.url || template.renderBaseUrl,
renderBasePath: uploadedRenderBase?.pathname || template.renderBasePath,
layers: preparedAssets.layers,
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
updatedAt: new Date().toISOString(),
} satisfies AgentPsdTemplate;
} catch (error) {
console.warn("[PSD Template] failed to repair template assets:", {
templateId: template.id,
error,
});
return template;
}
}