2026-06-04 13:26:14 +08:00

338 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_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";
export const runtime = "nodejs";
const MAX_TEMPLATE_FILE_SIZE = 80 * 1024 * 1024;
const ALLOWED_TEMPLATE_EXTENSIONS = new Set(["psd"]);
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,
extension === "psb"
? AGENT_PSD_TEMPLATE_UNSUPPORTED_PSB_MESSAGE
: "模板文件仅支持 Photoshop PSD(.psd)"
);
}
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_UNSUPPORTED_PSB_MESSAGE ||
error.message.startsWith("模板文件格式不正确")
) {
return jsonMutationError(400, error.message);
}
}
return jsonMutationError(502, "PSD 模板上传或解析失败");
}
}