Your Name e74586c8a0
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: remove psd runtime source fallback
2026-04-10 20:59:43 +08:00

173 lines
5.8 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import {
getEditableAgentPsdTemplateLayers,
getAgentPsdTemplateRuntimeIssue,
isAgentPsdImageLayerSlot,
isAgentPsdTemplateRuntimeReady,
isAgentPsdTemplateReady,
isAgentPsdTextLayerSlot,
type AgentPsdTemplateRenderResponse,
} from "@/lib/agents/psd-template-config";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement";
import { describeRemoteAssetLocation } from "@/lib/agents/psd-remote-asset";
import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair";
import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
export const runtime = "nodejs";
const renderSchema = z.object({
templateId: z.string().trim().min(1),
sourceImageUrl: z.string().trim().optional().nullable(),
imageAssignments: z.record(z.string(), z.string()).default({}),
imagePlacements: z
.record(
z.string(),
z.object({
scale: z.number().positive().max(8).optional(),
offsetX: z.number().optional(),
offsetY: z.number().optional(),
})
)
.default({}),
textAssignments: z.record(z.string(), z.string()).default({}),
textColorAssignments: z.record(z.string(), z.string()).default({}),
});
function jsonError(status: number, error: string) {
const payload: AgentPsdTemplateRenderResponse = {
success: false,
error,
};
return NextResponse.json(payload, { status });
}
export async function POST(request: NextRequest) {
const session = await auth().catch(() => null);
if (!session?.user?.id) {
return jsonError(401, "未登录");
}
let body: z.infer<typeof renderSchema>;
try {
body = renderSchema.parse(await request.json());
} catch {
return jsonError(400, "渲染参数不合法");
}
try {
const templates = await readAgentPsdTemplates();
const template = templates.find(
(entry) =>
entry.id === body.templateId &&
entry.status === "active" &&
isAgentPsdTemplateReady(entry)
);
if (!template) {
return jsonError(404, "PSD 模板不存在或未启用");
}
const fonts = await readAgentPsdFonts();
const resolvedTemplate = repairAgentPsdTemplateFontBindings(template, fonts);
if (!isAgentPsdTemplateRuntimeReady(resolvedTemplate)) {
return jsonError(
409,
getAgentPsdTemplateRuntimeIssue(resolvedTemplate) ||
"PSD 模板资源不完整,请在后台重新解析后再导出"
);
}
const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate);
if (editableLayers.length === 0) {
return jsonError(400, "当前模板未配置可编辑图层");
}
const fontMap = new Map(fonts.map((font) => [font.id, font]));
const missingFontLayer = editableLayers.find(
(layer) => isAgentPsdTextLayerSlot(layer) && layer.fontId && !fontMap.has(layer.fontId)
);
if (missingFontLayer) {
return jsonError(400, `文字图层“${missingFontLayer.layerName}”引用的字体已不存在`);
}
const imageAssignments: Record<string, string> = {};
const imagePlacements: Record<string, ReturnType<typeof normalizeAgentPsdImagePlacement>> = {};
const textAssignments: Record<string, string> = {};
const textColorAssignments: Record<string, string> = {};
for (const layer of editableLayers) {
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)) {
const nextText = String(body.textAssignments[layer.id] || layer.defaultText || "").trim();
if (nextText) {
textAssignments[layer.id] = nextText;
}
textColorAssignments[layer.id] = normalizeHexColor(
body.textColorAssignments[layer.id] || layer.textColor,
layer.textColor
);
}
}
const missingRequiredLayer = editableLayers.find(
(layer) =>
layer.isRequired &&
(isAgentPsdImageLayerSlot(layer)
? !imageAssignments[layer.id]
: !textAssignments[layer.id])
);
if (missingRequiredLayer) {
return jsonError(400, `缺少必传素材:${missingRequiredLayer.layerName}`);
}
const uploaded = await renderAgentPsdTemplate({
template: resolvedTemplate,
imageAssignments,
imagePlacements,
textAssignments,
textColorAssignments,
fonts,
});
return NextResponse.json({
success: true,
result: {
url: uploaded.url,
pathname: uploaded.pathname,
contentType: uploaded.contentType,
},
} satisfies AgentPsdTemplateRenderResponse);
} catch (error) {
console.error("[PSD Templates] render failed:", {
templateId: body.templateId,
sourceImage: body.sourceImageUrl
? describeRemoteAssetLocation(body.sourceImageUrl)
: null,
assignedImageCount: Object.keys(body.imageAssignments || {}).length,
error,
});
const message =
error instanceof Error && String(error.message || "").trim()
? error.message
: "PSD 模板渲染失败";
return jsonError(502, message);
}
}