import { randomUUID } from "node:crypto"; import { NextRequest, NextResponse } from "next/server"; import { upsertSystemSetting } from "@/lib/db/queries"; import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin"; import { createStorageUploadSession } 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 { AGENT_PSD_TEMPLATE_INVALID_FORMAT_MESSAGE, assertSupportedAgentPsdTemplateFile, } from "@/lib/agents/psd-template-format"; import { cacheAgentPsdSourceLocally } from "@/lib/agents/psd-local-cache"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; import { failTimedOutAgentPsdTemplates } from "@/lib/agents/psd-template-timeout"; 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; } async function updateTemplateById(params: { templateId: string; updatedBy: string; updater: (template: AgentPsdTemplate) => AgentPsdTemplate; }) { const templates = await readAgentPsdTemplates(); const currentTemplate = templates.find((entry) => entry.id === params.templateId); if (!currentTemplate) { return null; } const nextTemplate = params.updater(currentTemplate); await upsertSystemSetting({ key: AGENT_PSD_TEMPLATES_KEY, value: templates.map((entry) => entry.id === params.templateId ? nextTemplate : entry ), updatedBy: params.updatedBy, }); return nextTemplate; } function schedulePsdSourceRemoteBackup(params: { templateId: string; updatedBy: string; fileName: string; fileBuffer: ArrayBuffer; uploadAsset: Awaited>; }) { queueMicrotask(() => { void (async () => { try { console.log("[PSD Templates][Admin] backing up PSD source in background:", { templateId: params.templateId, fileName: params.fileName, size: params.fileBuffer.byteLength, }); const uploadedFile = await params.uploadAsset(params.fileName, params.fileBuffer); await updateTemplateById({ templateId: params.templateId, updatedBy: params.updatedBy, updater: (template) => ({ ...template, fileUrl: uploadedFile.url, filePath: uploadedFile.pathname, parseStage: template.parseStatus === "pending" ? "PSD 已上传,等待手动解析" : template.parseStage, parseError: undefined, updatedAt: new Date().toISOString(), }), }); console.log("[PSD Templates][Admin] PSD source background backup finished:", { templateId: params.templateId, }); } catch (error) { console.error("[PSD Templates][Admin] PSD source background backup failed:", { templateId: params.templateId, error, }); await updateTemplateById({ templateId: params.templateId, updatedBy: params.updatedBy, updater: (template) => ({ ...template, parseStage: template.parseStatus === "pending" ? "本地 PSD 已保存,远端源文件备份失败,可先尝试解析或重新上传" : template.parseStage, parseError: template.parseError || "远端 PSD 源文件备份失败,本地缓存仍可用于当前解析", updatedAt: new Date().toISOString(), }), }).catch(() => undefined); } })(); }); } export async function GET() { const session = await requireAgentWorkflowAdmin().catch(() => null); if (!session?.user?.id) { return jsonListError(401, "未登录"); } try { const { templates, timedOutTemplates } = await failTimedOutAgentPsdTemplates({ updatedBy: session.user.id, }); if (timedOutTemplates.length > 0) { console.warn("[PSD Templates][Admin] timed out stale parsing templates:", { count: timedOutTemplates.length, templateIds: timedOutTemplates.map((template) => template.id), }); } 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, "模板文件仅支持 Photoshop 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 fileInspection = assertSupportedAgentPsdTemplateFile(fileBuffer); const templateId = randomUUID(); console.log("[PSD Templates][Admin] received PSD template upload:", { templateId, fileName: file.name, size: file.size, fileFormat: fileInspection.format, width: fileInspection.width, height: fileInspection.height, }); await cacheAgentPsdSourceLocally({ templateId, fileName: file.name, buffer: fileBuffer, }).catch((error) => { console.warn("[PSD Templates][Admin] failed to cache uploaded PSD locally:", { templateId, error, }); }); const uploadAsset = await createStorageUploadSession(); const uploadedPreview = await uploadAsset( `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: "PSD 已接收,远端源文件正在后台备份", parseError: undefined, fileName: file.name, fileUrl: uploadedPreview.url, filePath: uploadedPreview.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, }); schedulePsdSourceRemoteBackup({ templateId, updatedBy: session.user.id, fileName: file.name, fileBuffer, uploadAsset, }); return NextResponse.json({ success: true, template: nextTemplate, } satisfies AgentPsdTemplateMutationResponse); } catch (error) { console.error("[PSD Templates][Admin] create failed:", error); if (error instanceof Error && error.message.trim()) { if ( error.message === AGENT_PSD_TEMPLATE_INVALID_FORMAT_MESSAGE ) { return jsonMutationError(400, error.message); } } return jsonMutationError(502, "PSD 模板上传或解析失败"); } }