338 lines
11 KiB
TypeScript
338 lines
11 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 {
|
||
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 模板上传或解析失败");
|
||
}
|
||
}
|