Your Name f87669c15a
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: stabilize agent psd template uploads
2026-05-19 20:28:10 +08:00

317 lines
9.9 KiB
TypeScript

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 { 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<ReturnType<typeof createStorageUploadSession>>;
}) {
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, "模板文件仅支持 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();
console.log("[PSD Templates][Admin] received PSD template upload:", {
templateId,
fileName: file.name,
size: file.size,
});
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);
return jsonMutationError(502, "PSD 模板上传或解析失败");
}
}