import { randomUUID } from "node:crypto"; import { NextRequest, NextResponse } from "next/server"; import { upsertSystemSetting } from "@/lib/db/queries"; import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin"; import { uploadFile } from "@/lib/storage/service"; import { AGENT_PSD_TEMPLATE_ASSET_VERSION, AGENT_PSD_TEMPLATES_KEY, normalizeAgentPsdTemplateMode, normalizeAgentPsdTemplateStatus, upsertAgentPsdTemplate, type AgentPsdTemplate, type AgentPsdTemplateLayerSlot, type AgentPsdTemplateListResponse, type AgentPsdTemplateMutationResponse, } from "@/lib/agents/psd-template-config"; import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config"; import { cacheAgentPsdSourceLocally } from "@/lib/agents/psd-local-cache"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; export const runtime = "nodejs"; const MAX_TEMPLATE_FILE_SIZE = 80 * 1024 * 1024; const ALLOWED_TEMPLATE_EXTENSIONS = new Set(["psd", "psb"]); const PLACEHOLDER_PREVIEW_PNG = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO2+p4kAAAAASUVORK5CYII=", "base64" ); function jsonListError(status: number, error: string) { const payload: AgentPsdTemplateListResponse = { success: false, templates: [], error, }; return NextResponse.json(payload, { status }); } function jsonMutationError(status: number, error: string) { const payload: AgentPsdTemplateMutationResponse = { success: false, error, }; return NextResponse.json(payload, { status }); } function getFileExtension(fileName: string) { return fileName.toLowerCase().split(".").pop() || ""; } function getDefaultTemplateName(fileName: string) { return fileName.replace(/\.[^.]+$/, "").trim() || "未命名 PSD 模板"; } function toArrayBuffer(buffer: Buffer) { return buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; } export async function GET() { const session = await requireAgentWorkflowAdmin().catch(() => null); if (!session?.user?.id) { return jsonListError(401, "未登录"); } try { const templates = await readAgentPsdTemplates(); return NextResponse.json({ success: true, templates, } satisfies AgentPsdTemplateListResponse); } catch (error) { console.error("[PSD Templates][Admin] list failed:", error); return jsonListError(503, "PSD 模板读取失败"); } } export async function POST(request: NextRequest) { const session = await requireAgentWorkflowAdmin().catch(() => null); if (!session?.user?.id) { return jsonMutationError(401, "未登录"); } let formData: FormData; try { formData = await request.formData(); } catch { return jsonMutationError(400, "上传表单解析失败"); } const file = formData.get("file"); const name = String(formData.get("name") || "").trim() || (file instanceof File ? getDefaultTemplateName(file.name) : ""); const brandName = String(formData.get("brandName") || "").trim(); const mode = normalizeAgentPsdTemplateMode(formData.get("mode")); const secondaryCategory = String(formData.get("secondaryCategory") || "").trim(); const categoryTags = normalizeAgentPsdTemplateCategoryTags( formData.getAll("categoryTags").map((item) => String(item || "")) ); const description = String(formData.get("description") || "").trim(); const status = normalizeAgentPsdTemplateStatus(formData.get("status")); if (!name) { return jsonMutationError(400, "缺少模板名称"); } if (!brandName) { return jsonMutationError(400, "请选择模板品牌"); } if (name.length > 80) { return jsonMutationError(400, "模板名称不能超过 80 个字符"); } if (brandName.length > 64) { return jsonMutationError(400, "模板品牌不能超过 64 个字符"); } if (secondaryCategory.length > 64) { return jsonMutationError(400, "模板二级分类不能超过 64 个字符"); } if (!(file instanceof File)) { return jsonMutationError(400, "缺少 PSD 模板文件"); } const extension = getFileExtension(file.name); if (!ALLOWED_TEMPLATE_EXTENSIONS.has(extension)) { return jsonMutationError(400, "模板文件仅支持 PSD / PSB"); } if (file.size <= 0) { return jsonMutationError(400, "模板文件不能为空"); } if (file.size > MAX_TEMPLATE_FILE_SIZE) { return jsonMutationError(400, "模板文件不能超过 80MB"); } try { const fileBuffer = await file.arrayBuffer(); const templateId = randomUUID(); await cacheAgentPsdSourceLocally({ templateId, fileName: file.name, buffer: fileBuffer, }).catch((error) => { console.warn("[PSD Templates][Admin] failed to cache uploaded PSD locally:", { templateId, error, }); }); const [uploadedFile, uploadedPreview] = await Promise.all([ uploadFile(file.name, fileBuffer), uploadFile( `agent-psd-preview-placeholder-${Date.now()}.png`, toArrayBuffer(PLACEHOLDER_PREVIEW_PNG) ), ]); const now = new Date().toISOString(); const nextTemplate: AgentPsdTemplate = { id: templateId, name, brandName, mode, secondaryCategory: secondaryCategory || undefined, categoryTags: categoryTags.length > 0 ? categoryTags : undefined, description: description || undefined, status, parseStatus: "pending", parseProgress: 0, parseStage: "等待手动解析", parseError: undefined, fileName: file.name, fileUrl: uploadedFile.url, filePath: uploadedFile.pathname, previewUrl: uploadedPreview.url, previewPath: uploadedPreview.pathname, renderBaseUrl: undefined, renderBasePath: undefined, width: 1, height: 1, layerCount: 0, layers: [] as AgentPsdTemplateLayerSlot[], assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION, createdAt: now, updatedAt: now, uploadedById: session.user.id, }; const currentTemplates = await readAgentPsdTemplates(); const templates = upsertAgentPsdTemplate(currentTemplates, nextTemplate); await upsertSystemSetting({ key: AGENT_PSD_TEMPLATES_KEY, value: templates, updatedBy: session.user.id, }); return NextResponse.json({ success: true, template: nextTemplate, } satisfies AgentPsdTemplateMutationResponse); } catch (error) { console.error("[PSD Templates][Admin] create failed:", error); return jsonMutationError(502, "PSD 模板上传或解析失败"); } }