258 lines
7.8 KiB
TypeScript
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();
|
|
}
|
|
}
|