From f87669c15a213143d45204733b2ef1ff8fe2b12a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 20:28:10 +0800 Subject: [PATCH] fix: stabilize agent psd template uploads --- .../psd-templates-settings-client.tsx | 30 ++++- app/api/admin/psd-templates/route.ts | 116 ++++++++++++++++-- 2 files changed, 133 insertions(+), 13 deletions(-) diff --git a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx index 4ca1668c0..1128ba03c 100644 --- a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx @@ -96,6 +96,7 @@ const SYSTEM_FONT_OPTIONS = [ const EMPTY_CATEGORY_SELECT_VALUE = "__none__"; const EMPTY_BRAND_SELECT_VALUE = "__unassigned__"; const UNASSIGNED_BRAND_LABEL = "未分类"; +const MAX_PSD_TEMPLATE_FILE_SIZE = 80 * 1024 * 1024; const PSD_TEMPLATE_MODE_OPTIONS: Array<{ value: AgentPsdTemplateMode; label: string; @@ -117,6 +118,13 @@ function getPsdTemplateModeLabel(mode?: AgentPsdTemplateMode | null) { return mode === "sku" ? "SKU图模式" : "主图模式"; } +function formatUploadFileSize(size: number) { + if (!Number.isFinite(size) || size <= 0) return "0MB"; + const mb = size / (1024 * 1024); + if (mb >= 1) return `${mb.toFixed(mb >= 10 ? 0 : 1)}MB`; + return `${Math.max(1, Math.round(size / 1024))}KB`; +} + function normalizeManagedBrandName(value?: string | null) { const normalized = String(value || "").trim(); if (!normalized || normalized === "未分配品牌") { @@ -577,6 +585,15 @@ export function PsdTemplatesSettingsClient({ function handleTemplateFileChange(event: ChangeEvent) { const nextFile = event.target.files?.[0] || null; + if (nextFile && nextFile.size > MAX_PSD_TEMPLATE_FILE_SIZE) { + toast.error( + `PSD 模板不能超过 80MB,当前文件约 ${formatUploadFileSize(nextFile.size)}` + ); + event.currentTarget.value = ""; + setUploadFile(null); + return; + } + setUploadFile(nextFile); if (nextFile && !uploadName.trim()) { @@ -702,6 +719,13 @@ export function PsdTemplatesSettingsClient({ return; } + if (uploadFile.size > MAX_PSD_TEMPLATE_FILE_SIZE) { + toast.error( + `PSD 模板不能超过 80MB,当前文件约 ${formatUploadFileSize(uploadFile.size)}` + ); + return; + } + if (!uploadBrandName.trim()) { toast.error("请先选择模板品牌"); return; @@ -757,8 +781,10 @@ export function PsdTemplatesSettingsClient({ reject(new Error(responsePayload.error || "PSD 模板上传失败")); }; - xhr.onerror = () => reject(new Error("网络错误,请稍后重试")); - xhr.ontimeout = () => reject(new Error("上传超时,请稍后重试")); + xhr.onerror = () => + reject(new Error("上传连接被中断,请检查网络或压缩 PSD 后重试")); + xhr.onabort = () => reject(new Error("上传已取消,请重新选择 PSD 后再试")); + xhr.ontimeout = () => reject(new Error("上传超时,请检查网络或压缩 PSD 后重试")); xhr.open("POST", "/api/admin/psd-templates"); xhr.timeout = 10 * 60 * 1000; diff --git a/app/api/admin/psd-templates/route.ts b/app/api/admin/psd-templates/route.ts index 259748d50..680553afb 100644 --- a/app/api/admin/psd-templates/route.ts +++ b/app/api/admin/psd-templates/route.ts @@ -2,7 +2,7 @@ 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 { createStorageUploadSession } from "@/lib/storage/service"; import { AGENT_PSD_TEMPLATE_ASSET_VERSION, AGENT_PSD_TEMPLATES_KEY, @@ -60,6 +60,89 @@ function toArrayBuffer(buffer: Buffer) { ) 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) { @@ -153,6 +236,11 @@ export async function POST(request: NextRequest) { try { const fileBuffer = await file.arrayBuffer(); const templateId = randomUUID(); + console.log("[PSD Templates][Admin] received PSD template upload:", { + templateId, + fileName: file.name, + size: file.size, + }); await cacheAgentPsdSourceLocally({ templateId, fileName: file.name, @@ -163,13 +251,11 @@ export async function POST(request: NextRequest) { error, }); }); - const [uploadedFile, uploadedPreview] = await Promise.all([ - uploadFile(file.name, fileBuffer), - uploadFile( - `agent-psd-preview-placeholder-${Date.now()}.png`, - toArrayBuffer(PLACEHOLDER_PREVIEW_PNG) - ), - ]); + 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 = { @@ -183,11 +269,11 @@ export async function POST(request: NextRequest) { status, parseStatus: "pending", parseProgress: 0, - parseStage: "等待手动解析", + parseStage: "PSD 已接收,远端源文件正在后台备份", parseError: undefined, fileName: file.name, - fileUrl: uploadedFile.url, - filePath: uploadedFile.pathname, + fileUrl: uploadedPreview.url, + filePath: uploadedPreview.pathname, previewUrl: uploadedPreview.url, previewPath: uploadedPreview.pathname, renderBaseUrl: undefined, @@ -211,6 +297,14 @@ export async function POST(request: NextRequest) { updatedBy: session.user.id, }); + schedulePsdSourceRemoteBackup({ + templateId, + updatedBy: session.user.id, + fileName: file.name, + fileBuffer, + uploadAsset, + }); + return NextResponse.json({ success: true, template: nextTemplate,