177 lines
6.1 KiB
TypeScript
177 lines
6.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import { upsertSystemSetting } from "@/lib/db/queries";
|
|
import {
|
|
AGENT_PSD_TEMPLATES_KEY,
|
|
getEditableAgentPsdTemplateLayers,
|
|
isAgentPsdImageLayerSlot,
|
|
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 { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
|
import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
|
|
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
|
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 templateIndex = templates.findIndex(
|
|
(entry) =>
|
|
entry.id === body.templateId &&
|
|
entry.status === "active" &&
|
|
isAgentPsdTemplateReady(entry)
|
|
);
|
|
const template = templateIndex >= 0 ? templates[templateIndex] : undefined;
|
|
|
|
if (!template) {
|
|
return jsonError(404, "PSD 模板不存在或未启用");
|
|
}
|
|
|
|
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
|
|
if (repairedTemplate !== template) {
|
|
await upsertSystemSetting({
|
|
key: AGENT_PSD_TEMPLATES_KEY,
|
|
value: templates.map((entry, index) =>
|
|
index === templateIndex ? repairedTemplate : entry
|
|
),
|
|
updatedBy: session.user.id,
|
|
}).catch((error) => {
|
|
console.warn("[PSD Templates] failed to persist repaired template before render:", {
|
|
templateId: template.id,
|
|
error,
|
|
});
|
|
});
|
|
}
|
|
|
|
const resolvedTemplate = await resolveAgentPsdTemplateTextStyles(repairedTemplate).catch(
|
|
() => repairedTemplate
|
|
);
|
|
const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate);
|
|
if (editableLayers.length === 0) {
|
|
return jsonError(400, "当前模板未配置可编辑图层");
|
|
}
|
|
|
|
const fonts = await readAgentPsdFonts();
|
|
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:", error);
|
|
const message =
|
|
error instanceof Error && String(error.message || "").trim()
|
|
? error.message
|
|
: "PSD 模板渲染失败";
|
|
return jsonError(502, message);
|
|
}
|
|
}
|