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(); } }