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 d56af75c6..daf6b6f66 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 @@ -751,7 +751,7 @@ export function PsdTemplatesSettingsClient({ async function handleCreateTemplate(options?: { parseAfterUpload?: boolean }) { const parseAfterUpload = options?.parseAfterUpload === true; if (!uploadFile) { - toast.error("请先选择 PSD / PSB 文件"); + toast.error("请先选择 PSD 文件"); return; } @@ -2581,7 +2581,10 @@ export function PsdTemplatesSettingsClient({ 上传新模板 - 支持 PSD / PSB,建议图层命名清晰,便于后台配置图片位和文字位。 + + 支持 Photoshop PSD(.psd),建议图层命名清晰,便于后台配置图片位和文字位。PSB + 请先在 Photoshop 中另存为 PSD 后上传。 +
@@ -2742,11 +2745,11 @@ export function PsdTemplatesSettingsClient({

- {uploadFile ? `已选择:${uploadFile.name}` : "支持 80MB 以内 PSD / PSB 文件"} + {uploadFile ? `已选择:${uploadFile.name}` : "支持 80MB 以内 PSD 文件"}

diff --git a/app/api/admin/psd-templates/[id]/parse/route.ts b/app/api/admin/psd-templates/[id]/parse/route.ts index 796b8914b..2fd7bbcac 100644 --- a/app/api/admin/psd-templates/[id]/parse/route.ts +++ b/app/api/admin/psd-templates/[id]/parse/route.ts @@ -12,6 +12,10 @@ import { } from "@/lib/agents/psd-template-parser"; import { pickPrimaryPsdImageLayerId } from "@/lib/agents/psd-template-image-slot-selection"; import { schedulePsdTemplateAssetBackfill } from "@/lib/agents/psd-template-asset-backfill"; +import { + AGENT_PSD_TEMPLATE_UNSUPPORTED_PSB_MESSAGE, + assertSupportedAgentPsdTemplateFile, +} from "@/lib/agents/psd-template-format"; import { createAgentPsdTemplateParseHeartbeat, updateAgentPsdTemplateById, @@ -181,6 +185,11 @@ export async function POST( } const { id } = await context.params; + let latestProgress = { + progress: 0, + stage: "准备解析 PSD", + error: undefined as string | undefined, + }; try { const { templates, timedOutTemplates } = await failTimedOutAgentPsdTemplates({ @@ -204,10 +213,10 @@ export async function POST( return jsonError(409, "该模板正在解析中,请稍后刷新"); } - let latestProgress = { + latestProgress = { progress: 5, stage: "准备解析 PSD", - error: undefined as string | undefined, + error: undefined, }; const setParseProgress = async (progress: number, stage: string, error?: string) => { latestProgress = { @@ -279,6 +288,9 @@ export async function POST( }); } + await setParseProgress(22, "正在校验 PSD 文件格式"); + assertSupportedAgentPsdTemplateFile(fileBuffer); + await setParseProgress(24, "正在解析 PSD 图层结构与预览"); const parsedTemplate = await parsePsdTemplateFile(fileBuffer, { onProgress: async (event) => { @@ -353,10 +365,12 @@ export async function POST( const existingTemplate = (await readAgentPsdTemplates()).find((entry) => entry.id === id); if (existingTemplate) { - const fallbackStatus = - existingTemplate.parseStatus === "completed" && existingTemplate.layers.length > 0 - ? "completed" - : "failed"; + const hasUsablePreviousParse = + existingTemplate.layers.length > 0 && + existingTemplate.width > 1 && + existingTemplate.height > 1 && + Boolean(existingTemplate.previewUrl); + const fallbackStatus = hasUsablePreviousParse ? "completed" : "failed"; await updateAgentPsdTemplateById({ templateId: id, @@ -364,9 +378,14 @@ export async function POST( updater: (template) => ({ ...template, parseStatus: fallbackStatus, - parseProgress: fallbackStatus === "completed" ? 100 : 0, + parseProgress: + fallbackStatus === "completed" + ? 100 + : Math.max(0, Math.round(Number(latestProgress.progress || 0))), parseStage: - fallbackStatus === "completed" ? "沿用上次成功解析结果" : undefined, + fallbackStatus === "completed" + ? "沿用上次成功解析结果" + : latestProgress.stage || "PSD 解析失败", parseError: fallbackStatus === "completed" ? `重新解析失败:${message}` @@ -376,7 +395,19 @@ export async function POST( }).catch(() => undefined); } - console.error("[PSD Templates][Admin] parse failed:", error); - return jsonError(502, message); + console.error("[PSD Templates][Admin] parse failed:", { + templateId: id, + fileName: existingTemplate?.fileName, + progress: latestProgress.progress, + stage: latestProgress.stage, + error, + }); + return jsonError( + message === AGENT_PSD_TEMPLATE_UNSUPPORTED_PSB_MESSAGE || + message.startsWith("模板文件格式不正确") + ? 400 + : 502, + message + ); } } diff --git a/app/api/admin/psd-templates/route.ts b/app/api/admin/psd-templates/route.ts index 680553afb..07bb06497 100644 --- a/app/api/admin/psd-templates/route.ts +++ b/app/api/admin/psd-templates/route.ts @@ -15,6 +15,10 @@ import { type AgentPsdTemplateMutationResponse, } from "@/lib/agents/psd-template-config"; import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config"; +import { + AGENT_PSD_TEMPLATE_UNSUPPORTED_PSB_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"; @@ -22,7 +26,7 @@ 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 ALLOWED_TEMPLATE_EXTENSIONS = new Set(["psd"]); const PLACEHOLDER_PREVIEW_PNG = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO2+p4kAAAAASUVORK5CYII=", "base64" @@ -222,7 +226,12 @@ export async function POST(request: NextRequest) { const extension = getFileExtension(file.name); if (!ALLOWED_TEMPLATE_EXTENSIONS.has(extension)) { - return jsonMutationError(400, "模板文件仅支持 PSD / PSB"); + return jsonMutationError( + 400, + extension === "psb" + ? AGENT_PSD_TEMPLATE_UNSUPPORTED_PSB_MESSAGE + : "模板文件仅支持 Photoshop PSD(.psd)" + ); } if (file.size <= 0) { @@ -235,11 +244,15 @@ export async function POST(request: NextRequest) { 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, @@ -311,6 +324,14 @@ export async function POST(request: NextRequest) { } 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_UNSUPPORTED_PSB_MESSAGE || + error.message.startsWith("模板文件格式不正确") + ) { + return jsonMutationError(400, error.message); + } + } return jsonMutationError(502, "PSD 模板上传或解析失败"); } } diff --git a/lib/agents/psd-template-format.ts b/lib/agents/psd-template-format.ts new file mode 100644 index 000000000..7f3c26165 --- /dev/null +++ b/lib/agents/psd-template-format.ts @@ -0,0 +1,74 @@ +import "server-only"; + +export type AgentPsdTemplateFileFormat = "psd" | "psb" | "unknown"; + +export interface AgentPsdTemplateFileInspection { + signature: string; + version: number; + format: AgentPsdTemplateFileFormat; + channels: number; + height: number; + width: number; + depth: number; + colorMode: number; +} + +export const AGENT_PSD_TEMPLATE_UNSUPPORTED_PSB_MESSAGE = + "当前生产模板解析仅支持 Photoshop PSD(.psd)。检测到 PSB / Large Document 格式,请在 Photoshop 中另存为 PSD 后再上传解析。"; + +export function inspectAgentPsdTemplateFile( + value: ArrayBuffer | Buffer +): AgentPsdTemplateFileInspection { + const buffer = Buffer.isBuffer(value) + ? value + : Buffer.from(value); + + if (buffer.byteLength < 26) { + return { + signature: "", + version: 0, + format: "unknown", + channels: 0, + height: 0, + width: 0, + depth: 0, + colorMode: 0, + }; + } + + const signature = buffer.subarray(0, 4).toString("ascii"); + const version = buffer.readUInt16BE(4); + const format = + signature === "8BPS" && version === 1 + ? "psd" + : signature === "8BPS" && version === 2 + ? "psb" + : "unknown"; + + return { + signature, + version, + format, + channels: buffer.readUInt16BE(12), + height: buffer.readUInt32BE(14), + width: buffer.readUInt32BE(18), + depth: buffer.readUInt16BE(22), + colorMode: buffer.readUInt16BE(24), + }; +} + +export function assertSupportedAgentPsdTemplateFile( + value: ArrayBuffer | Buffer +) { + const inspection = inspectAgentPsdTemplateFile(value); + + if (inspection.format === "psd") { + return inspection; + } + + if (inspection.format === "psb") { + throw new Error(AGENT_PSD_TEMPLATE_UNSUPPORTED_PSB_MESSAGE); + } + + throw new Error("模板文件格式不正确,请上传 Photoshop PSD(.psd)文件。"); +}