223 lines
6.7 KiB
TypeScript
223 lines
6.7 KiB
TypeScript
import "server-only";
|
|
|
|
import { upsertSystemSetting } from "@/lib/db/queries";
|
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
|
import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement";
|
|
import { type AgentPsdTemplateRenderRequest } from "@/lib/agents/psd-render-task-contract";
|
|
import {
|
|
AGENT_PSD_TEMPLATES_KEY,
|
|
type AgentPsdTemplateRenderResult,
|
|
getAgentPsdTemplateRuntimeIssue,
|
|
getEditableAgentPsdTemplateLayers,
|
|
isAgentPsdImageLayerSlot,
|
|
isAgentPsdLayerSuperLocked,
|
|
isAgentPsdTemplateReady,
|
|
isAgentPsdTemplateRuntimeReady,
|
|
isAgentPsdTemplateLayerUserEditable,
|
|
isAgentPsdTextLayerSlot,
|
|
} from "@/lib/agents/psd-template-config";
|
|
import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
|
|
import {
|
|
repairAgentPsdTemplateAssetsIfNeeded,
|
|
repairAgentPsdTemplateFontBindings,
|
|
} from "@/lib/agents/psd-template-repair";
|
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
|
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
|
|
|
type AgentPsdTextPositionOffset = {
|
|
offsetX: number;
|
|
offsetY: number;
|
|
};
|
|
type AgentPsdLayerPositionOffset = AgentPsdTextPositionOffset;
|
|
|
|
function clampTextPositionOffsetValue(value: number) {
|
|
if (!Number.isFinite(value)) {
|
|
return 0;
|
|
}
|
|
return Math.max(-5000, Math.min(5000, value));
|
|
}
|
|
|
|
function normalizeTextPositionOffset(
|
|
value?: Partial<AgentPsdTextPositionOffset> | null
|
|
): AgentPsdTextPositionOffset {
|
|
return {
|
|
offsetX: Number(
|
|
clampTextPositionOffsetValue(Number(value?.offsetX || 0)).toFixed(2)
|
|
),
|
|
offsetY: Number(
|
|
clampTextPositionOffsetValue(Number(value?.offsetY || 0)).toFixed(2)
|
|
),
|
|
};
|
|
}
|
|
|
|
export async function executeAgentPsdTemplateRenderRequest(
|
|
body: AgentPsdTemplateRenderRequest
|
|
): Promise<AgentPsdTemplateRenderResult> {
|
|
const templates = await readAgentPsdTemplates();
|
|
const template = templates.find(
|
|
(entry) =>
|
|
entry.id === body.templateId &&
|
|
entry.status === "active" &&
|
|
isAgentPsdTemplateReady(entry)
|
|
);
|
|
|
|
if (!template) {
|
|
throw new Error("PSD 模板不存在或未启用");
|
|
}
|
|
|
|
const fonts = await readAgentPsdFonts();
|
|
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
|
|
if (repairedTemplate !== template) {
|
|
await upsertSystemSetting({
|
|
key: AGENT_PSD_TEMPLATES_KEY,
|
|
value: templates.map((entry) =>
|
|
entry.id === template.id ? repairedTemplate : entry
|
|
),
|
|
updatedBy: "system:psd-template-repair",
|
|
}).catch((error) => {
|
|
console.warn("[PSD Template] failed to persist repaired assets:", {
|
|
templateId: template.id,
|
|
error,
|
|
});
|
|
});
|
|
}
|
|
|
|
const resolvedTemplate = repairAgentPsdTemplateFontBindings(
|
|
repairedTemplate,
|
|
fonts
|
|
);
|
|
if (!isAgentPsdTemplateRuntimeReady(resolvedTemplate)) {
|
|
throw new Error(
|
|
getAgentPsdTemplateRuntimeIssue(resolvedTemplate) ||
|
|
"PSD 模板资源不完整,请在后台重新解析后再导出"
|
|
);
|
|
}
|
|
|
|
const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate);
|
|
if (editableLayers.length === 0) {
|
|
throw new Error("当前模板未配置可编辑图层");
|
|
}
|
|
const hiddenLayerIdSet = new Set(
|
|
(body.hiddenLayerIds || [])
|
|
.map((layerId) => String(layerId || "").trim())
|
|
.filter(Boolean)
|
|
);
|
|
for (const layer of resolvedTemplate.layers) {
|
|
if (isAgentPsdLayerSuperLocked(layer)) {
|
|
hiddenLayerIdSet.delete(layer.id);
|
|
}
|
|
}
|
|
const normalizedLayerOrderIds = Array.from(
|
|
new Set(
|
|
(body.layerOrderIds || [])
|
|
.map((layerId) => String(layerId || "").trim())
|
|
.filter(Boolean)
|
|
)
|
|
);
|
|
|
|
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
|
const missingFontLayer = editableLayers.find(
|
|
(layer) =>
|
|
!hiddenLayerIdSet.has(layer.id) &&
|
|
isAgentPsdTextLayerSlot(layer) &&
|
|
layer.fontId &&
|
|
!fontMap.has(layer.fontId)
|
|
);
|
|
if (missingFontLayer) {
|
|
throw new Error(
|
|
`文字图层“${missingFontLayer.layerName}”引用的字体已不存在`
|
|
);
|
|
}
|
|
|
|
const imageAssignments: Record<string, string> = {};
|
|
const imagePlacements: Record<
|
|
string,
|
|
ReturnType<typeof normalizeAgentPsdImagePlacement>
|
|
> = {};
|
|
const textAssignments: Record<string, string> = {};
|
|
const textColorAssignments: Record<string, string> = {};
|
|
const textPositionOffsets: Record<string, AgentPsdTextPositionOffset> = {};
|
|
const layerPositionOffsets: Record<string, AgentPsdLayerPositionOffset> = {};
|
|
|
|
for (const layer of resolvedTemplate.layers) {
|
|
if (hiddenLayerIdSet.has(layer.id)) {
|
|
continue;
|
|
}
|
|
|
|
if (!isAgentPsdLayerSuperLocked(layer)) {
|
|
layerPositionOffsets[layer.id] = normalizeTextPositionOffset(
|
|
body.layerPositionOffsets[layer.id]
|
|
);
|
|
}
|
|
|
|
if (!isAgentPsdTemplateLayerUserEditable(layer)) {
|
|
continue;
|
|
}
|
|
|
|
if (isAgentPsdImageLayerSlot(layer)) {
|
|
const explicitUrl = String(body.imageAssignments[layer.id] || "").trim();
|
|
if (explicitUrl) {
|
|
imageAssignments[layer.id] = explicitUrl;
|
|
imagePlacements[layer.id] = normalizeAgentPsdImagePlacement(
|
|
body.imagePlacements[layer.id]
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (layer.isDefaultResultSource && body.sourceImageUrl?.trim()) {
|
|
imageAssignments[layer.id] = body.sourceImageUrl.trim();
|
|
imagePlacements[layer.id] = normalizeAgentPsdImagePlacement(
|
|
body.imagePlacements[layer.id]
|
|
);
|
|
}
|
|
}
|
|
|
|
if (isAgentPsdTextLayerSlot(layer)) {
|
|
if (Object.hasOwn(body.textAssignments, layer.id)) {
|
|
textAssignments[layer.id] = String(
|
|
body.textAssignments[layer.id] ?? ""
|
|
).trim();
|
|
}
|
|
textColorAssignments[layer.id] = normalizeHexColor(
|
|
body.textColorAssignments[layer.id] || layer.textColor,
|
|
layer.textColor
|
|
);
|
|
textPositionOffsets[layer.id] = normalizeTextPositionOffset(
|
|
body.textPositionOffsets[layer.id]
|
|
);
|
|
}
|
|
}
|
|
|
|
const missingRequiredLayer = editableLayers.find(
|
|
(layer) =>
|
|
!hiddenLayerIdSet.has(layer.id) &&
|
|
layer.isRequired &&
|
|
(isAgentPsdImageLayerSlot(layer)
|
|
? !imageAssignments[layer.id]
|
|
: !Object.hasOwn(textAssignments, layer.id) &&
|
|
!String(layer.defaultText || "").trim())
|
|
);
|
|
if (missingRequiredLayer) {
|
|
throw new Error(`缺少必传素材:${missingRequiredLayer.layerName}`);
|
|
}
|
|
|
|
const uploaded = await renderAgentPsdTemplate({
|
|
template: resolvedTemplate,
|
|
hiddenLayerIds: Array.from(hiddenLayerIdSet),
|
|
layerOrderIds: normalizedLayerOrderIds,
|
|
imageAssignments,
|
|
imagePlacements,
|
|
textAssignments,
|
|
textColorAssignments,
|
|
layerPositionOffsets,
|
|
textPositionOffsets,
|
|
fonts,
|
|
});
|
|
|
|
return {
|
|
url: uploaded.url,
|
|
pathname: uploaded.pathname,
|
|
contentType: uploaded.contentType,
|
|
};
|
|
}
|