2026-05-29 17:22:10 +08:00

258 lines
7.8 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { and, desc, eq, inArray } from "drizzle-orm";
import { z } from "zod";
import { db } from "@/lib/db";
import {
siteSettings,
systemSettingHistory,
systemSettingSnapshot,
systemSettings,
user,
} from "@/lib/db/schema";
import { ChatSDKError } from "@/lib/errors";
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { clearScopedSiteSettingsCache } from "@/lib/site-settings/public-cache";
import {
AGENT_WORKFLOW_SNAPSHOT_SCOPE,
createSystemSettingSnapshotData,
getSystemSettingSnapshotScopeKeys,
readSystemSettingSnapshotData,
summarizeSystemSettingSnapshotData,
type SystemSettingSnapshotScope,
} from "@/lib/system-settings/snapshot";
const snapshotScopeSchema = z.enum([AGENT_WORKFLOW_SNAPSHOT_SCOPE]);
const listQuerySchema = z.object({
scope: snapshotScopeSchema,
limit: z.coerce.number().int().min(1).max(50).optional(),
});
const mutationSchema = z.discriminatedUnion("action", [
z.object({
action: z.literal("create"),
scope: snapshotScopeSchema,
label: z.string().trim().min(1).max(128).optional(),
}),
z.object({
action: z.literal("restore"),
snapshotId: z.string().uuid(),
reason: z.string().trim().min(1).max(128).optional(),
}),
]);
async function loadSnapshotSource(scope: SystemSettingSnapshotScope) {
const keys = getSystemSettingSnapshotScopeKeys(scope);
const rows = await db
.select({
key: systemSettings.key,
value: systemSettings.value,
})
.from(systemSettings)
.where(inArray(systemSettings.key, [...keys]));
const valueByKey = new Map(rows.map((row) => [row.key, row.value]));
const [currentSiteSettings] = await db
.select({
agentWorkflowDefaultModelId: siteSettings.agentWorkflowDefaultModelId,
})
.from(siteSettings)
.limit(1);
return {
snapshotData: createSystemSettingSnapshotData(scope, valueByKey, {
agentWorkflowDefaultModelId:
String(currentSiteSettings?.agentWorkflowDefaultModelId || "").trim() || null,
}),
};
}
async function createSnapshot(params: {
scope: SystemSettingSnapshotScope;
label?: string;
createdBy: string;
}) {
const { snapshotData } = await loadSnapshotSource(params.scope);
const [snapshot] = await db
.insert(systemSettingSnapshot)
.values({
scope: params.scope,
label: params.label || `manual-${params.scope}-snapshot`,
summary: summarizeSystemSettingSnapshotData(snapshotData),
data: snapshotData,
createdAt: new Date(),
createdBy: params.createdBy,
})
.returning({
id: systemSettingSnapshot.id,
scope: systemSettingSnapshot.scope,
label: systemSettingSnapshot.label,
createdAt: systemSettingSnapshot.createdAt,
});
return snapshot ?? null;
}
export async function GET(request: NextRequest) {
try {
await requireAgentWorkflowAdmin();
const parsed = listQuerySchema.parse(
Object.fromEntries(new URL(request.url).searchParams.entries())
);
const snapshots = await db
.select({
id: systemSettingSnapshot.id,
scope: systemSettingSnapshot.scope,
label: systemSettingSnapshot.label,
summary: systemSettingSnapshot.summary,
createdAt: systemSettingSnapshot.createdAt,
createdByName: user.name,
createdByEmail: user.email,
})
.from(systemSettingSnapshot)
.leftJoin(user, eq(user.id, systemSettingSnapshot.createdBy))
.where(eq(systemSettingSnapshot.scope, parsed.scope))
.orderBy(desc(systemSettingSnapshot.createdAt))
.limit(parsed.limit ?? 12);
return NextResponse.json({ snapshots });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}
export async function POST(request: NextRequest) {
try {
const session = await requireAgentWorkflowAdmin();
const body = mutationSchema.parse(await request.json());
if (body.action === "create") {
const snapshot = await createSnapshot({
scope: body.scope,
label: body.label,
createdBy: session.user.id,
});
return NextResponse.json({ snapshot });
}
const [targetSnapshot] = await db
.select({
id: systemSettingSnapshot.id,
scope: systemSettingSnapshot.scope,
data: systemSettingSnapshot.data,
})
.from(systemSettingSnapshot)
.where(eq(systemSettingSnapshot.id, body.snapshotId))
.limit(1);
if (!targetSnapshot) {
return NextResponse.json({ error: "快照不存在" }, { status: 404 });
}
const scope = snapshotScopeSchema.parse(targetSnapshot.scope);
const parsedSnapshot = readSystemSettingSnapshotData(scope, targetSnapshot.data);
const currentSource = await loadSnapshotSource(scope);
const [backupSnapshot] = await db.transaction(async (tx) => {
const [preRestoreSnapshot] = await tx
.insert(systemSettingSnapshot)
.values({
scope,
label: `before-restore-${body.snapshotId.slice(0, 8)}`,
summary: summarizeSystemSettingSnapshotData(currentSource.snapshotData),
data: currentSource.snapshotData,
createdAt: new Date(),
createdBy: session.user.id,
})
.returning({
id: systemSettingSnapshot.id,
});
for (const [key, nextValue] of Object.entries(parsedSnapshot.systemSettings)) {
const [existingSetting] = await tx
.select({ value: systemSettings.value })
.from(systemSettings)
.where(eq(systemSettings.key, key))
.limit(1);
await tx.insert(systemSettingHistory).values({
key,
previousValue: existingSetting?.value ?? null,
nextValue,
reason: body.reason || `restore-snapshot:${body.snapshotId}`,
changedAt: new Date(),
changedBy: session.user.id,
});
await tx
.insert(systemSettings)
.values({
key,
value: nextValue,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: nextValue,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if ("agentWorkflowDefaultModelId" in parsedSnapshot.siteSettings) {
const [existingSiteSettings] = await tx.select().from(siteSettings).limit(1);
const nextAgentWorkflowDefaultModelId =
String(parsedSnapshot.siteSettings.agentWorkflowDefaultModelId || "").trim() || null;
if (existingSiteSettings) {
await tx
.update(siteSettings)
.set({
agentWorkflowDefaultModelId: nextAgentWorkflowDefaultModelId,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.where(eq(siteSettings.id, existingSiteSettings.id));
} else {
await tx.insert(siteSettings).values({
siteName: "AI Chatbot",
agentWorkflowDefaultModelId: nextAgentWorkflowDefaultModelId,
updatedAt: new Date(),
updatedBy: session.user.id,
});
}
}
return [preRestoreSnapshot];
});
clearScopedSiteSettingsCache();
return NextResponse.json({
restoredSnapshotId: body.snapshotId,
backupSnapshotId: backupSnapshot?.id ?? null,
});
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: "请求参数不合法",
details: error.issues,
},
{ status: 400 }
);
}
return new ChatSDKError("offline:admin").toResponse();
}
}