feat: add agent workflow snapshot center

This commit is contained in:
Your Name 2026-05-29 17:22:10 +08:00 committed by root
parent e160d7f217
commit f31db01b8e
6 changed files with 627 additions and 13 deletions

View File

@ -32,6 +32,8 @@ import {
MonitorPlay,
Package,
Plus,
RefreshCw,
RotateCcw,
Save,
Search,
Settings2,
@ -187,6 +189,8 @@ const WORKFLOW_SETTINGS_STALE_DRAFT_STORAGE_KEY = "agent-workflow-settings-stale
const WORKFLOW_SETTINGS_DRAFT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
const AGENT_WORKFLOW_SETTINGS_ENDPOINT =
"/api/site-settings?scope=agent-workflow-admin";
const AGENT_WORKFLOW_SNAPSHOTS_ENDPOINT =
"/api/admin/system-settings/snapshots?scope=agent-workflow&limit=12";
const AGENT_WORKFLOW_SAVE_TIMEOUT_MS = 30_000;
const DELETE_WORKFLOW_STEP_PASSWORD = "123456";
const BUSINESS_MODE_DEFAULT_MODEL_EMPTY_VALUE = "__agent_workflow_mode_default_model_empty__";
@ -1824,6 +1828,30 @@ interface AgentWorkflowDestructiveChangePayload {
approvalToken: string;
}
interface AgentWorkflowSnapshotItem {
id: string;
scope: string;
label: string | null;
summary?: {
keyCount?: number;
nonNullKeyCount?: number;
byteLength?: number;
siteFieldCount?: number;
} | null;
createdAt: string;
createdByName?: string | null;
createdByEmail?: string | null;
}
function formatSnapshotByteLength(value?: number | null) {
const byteLength = Number(value || 0);
if (!Number.isFinite(byteLength) || byteLength <= 0) {
return "0 KB";
}
return `${(byteLength / 1024).toFixed(byteLength >= 1024 * 1024 ? 1 : 0)} KB`;
}
function formatAgentWorkflowDestructiveChangeConfirmText(
payload: AgentWorkflowDestructiveChangePayload
) {
@ -1961,6 +1989,15 @@ export function AgentWorkflowSettingsClient({
fetcher,
{ revalidateOnFocus: false }
);
const {
data: snapshotsData,
isLoading: isLoadingSnapshots,
mutate: mutateSnapshots,
} = useSWR<{ snapshots: AgentWorkflowSnapshotItem[] }>(
AGENT_WORKFLOW_SNAPSHOTS_ENDPOINT,
fetcher,
{ revalidateOnFocus: false }
);
const [agentWorkflowDefaultModelId, setAgentWorkflowDefaultModelId] = useState<string | null>(null);
const [presetLibraryConfig, setPresetLibraryConfig] = useState<AgentPresetLibraryConfig>(
defaultAgentPresetLibraryConfig
@ -2016,6 +2053,9 @@ export function AgentWorkflowSettingsClient({
const [autoDraftUpdatedAt, setAutoDraftUpdatedAt] = useState<number | null>(null);
const [autoDraftStatus, setAutoDraftStatus] = useState<"idle" | "restored" | "saved">("idle");
const [recoverableDraftUpdatedAt, setRecoverableDraftUpdatedAt] = useState<number | null>(null);
const [snapshotDialogOpen, setSnapshotDialogOpen] = useState(false);
const [isCreatingSnapshot, setIsCreatingSnapshot] = useState(false);
const [restoringSnapshotId, setRestoringSnapshotId] = useState<string | null>(null);
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
const brandImportInputRef = useRef<HTMLInputElement | null>(null);
const hasAppliedInitialSettingsRef = useRef(false);
@ -4074,6 +4114,81 @@ export function AgentWorkflowSettingsClient({
}
};
const handleCreateSnapshot = async () => {
try {
setIsCreatingSnapshot(true);
const response = await fetch("/api/admin/system-settings/snapshots", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "create",
scope: "agent-workflow",
}),
});
const payload = (await response.json().catch(() => null)) as
| { error?: string; snapshot?: AgentWorkflowSnapshotItem | null }
| null;
if (!response.ok) {
throw new Error(payload?.error || "创建快照失败");
}
await mutateSnapshots();
toast.success("已创建当前工作流快照");
} catch (error) {
toast.error(error instanceof Error ? error.message : "创建快照失败");
} finally {
setIsCreatingSnapshot(false);
}
};
const handleRestoreSnapshot = async (snapshot: AgentWorkflowSnapshotItem) => {
const shouldContinue = window.confirm(
[
"将把风格化工作流恢复到这个快照版本。",
"恢复前系统会自动再做一份当前快照,便于继续回退。",
"",
`快照标签:${snapshot.label || "未命名快照"}`,
`创建时间:${new Date(snapshot.createdAt).toLocaleString("zh-CN")}`,
].join("\n")
);
if (!shouldContinue) {
return;
}
try {
setRestoringSnapshotId(snapshot.id);
const response = await fetch("/api/admin/system-settings/snapshots", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "restore",
snapshotId: snapshot.id,
reason: `restore-snapshot:${snapshot.id}`,
}),
});
const payload = (await response.json().catch(() => null)) as
| { error?: string; backupSnapshotId?: string | null }
| null;
if (!response.ok) {
throw new Error(payload?.error || "恢复快照失败");
}
await Promise.all([mutate(), mutateSnapshots()]);
toast.success(
payload?.backupSnapshotId
? `已恢复快照,并自动创建恢复前备份 ${payload.backupSnapshotId.slice(0, 8)}`
: "已恢复到所选快照"
);
} catch (error) {
toast.error(error instanceof Error ? error.message : "恢复快照失败");
} finally {
setRestoringSnapshotId(null);
}
};
const handleDownloadBrandTemplate = async () => {
try {
await downloadAgentWorkflowBrandImportTemplate(categoryLabels);
@ -5866,6 +5981,15 @@ export function AgentWorkflowSettingsClient({
>
{recoverableDraftUpdatedAt ? "恢复旧草稿" : "查找旧草稿"}
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setSnapshotDialogOpen(true)}
>
<RotateCcw className="mr-2 h-4 w-4" />
快照中心
</Button>
<Button asChild variant="outline" size="sm">
<Link href="/settings/agent-workflow/psd-templates">
<Layers className="mr-2 h-4 w-4" />
@ -5896,6 +6020,126 @@ export function AgentWorkflowSettingsClient({
</div>
</header>
<Dialog open={snapshotDialogOpen} onOpenChange={setSnapshotDialogOpen}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>风格化工作流快照中心</DialogTitle>
<DialogDescription>
查看最近快照、手动创建安全点,并一键恢复到指定版本。恢复前系统会自动再备份一次当前状态。
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 rounded-2xl border bg-muted/20 p-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<div className="text-sm font-medium">当前共有 {snapshotsData?.snapshots?.length || 0} 条最近快照</div>
<div className="text-xs text-muted-foreground">
自动快照会在高风险保存前生成,手动快照适合在批量编辑前先留一个恢复点。
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => mutateSnapshots()}
disabled={isLoadingSnapshots}
>
<RefreshCw
className={cn("mr-2 h-4 w-4", isLoadingSnapshots && "animate-spin")}
/>
刷新
</Button>
<Button
type="button"
size="sm"
onClick={handleCreateSnapshot}
disabled={isCreatingSnapshot}
>
{isCreatingSnapshot ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
创建快照
</Button>
</div>
</div>
<div className="max-h-[60vh] space-y-3 overflow-y-auto pr-1">
{isLoadingSnapshots ? (
<div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
正在加载快照列表...
</div>
) : (snapshotsData?.snapshots?.length || 0) === 0 ? (
<div className="flex h-40 items-center justify-center rounded-2xl border border-dashed text-sm text-muted-foreground">
还没有可用快照,先创建一个安全点吧。
</div>
) : (
(snapshotsData?.snapshots || []).map((snapshot, index) => (
<div
key={snapshot.id}
className="rounded-2xl border bg-background p-4 shadow-sm"
>
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<Badge variant={index === 0 ? "default" : "secondary"}>
{index === 0 ? "最新" : `#${index + 1}`}
</Badge>
<span className="font-medium">
{snapshot.label || "未命名快照"}
</span>
<span className="font-mono text-xs text-muted-foreground">
{snapshot.id.slice(0, 8)}
</span>
</div>
<div className="text-sm text-muted-foreground">
{new Date(snapshot.createdAt).toLocaleString("zh-CN")}
{" · "}
{snapshot.createdByName ||
snapshot.createdByEmail ||
"系统自动创建"}
</div>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<span className="rounded-full border px-2 py-1">
配置键 {snapshot.summary?.keyCount ?? "-"}
</span>
<span className="rounded-full border px-2 py-1">
站点字段 {snapshot.summary?.siteFieldCount ?? 0}
</span>
<span className="rounded-full border px-2 py-1">
非空项 {snapshot.summary?.nonNullKeyCount ?? "-"}
</span>
<span className="rounded-full border px-2 py-1">
大小 {formatSnapshotByteLength(snapshot.summary?.byteLength)}
</span>
</div>
</div>
<Button
type="button"
variant="outline"
onClick={() => handleRestoreSnapshot(snapshot)}
disabled={restoringSnapshotId === snapshot.id}
>
{restoringSnapshotId === snapshot.id ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RotateCcw className="mr-2 h-4 w-4" />
)}
恢复到此版本
</Button>
</div>
</div>
))
)}
</div>
</DialogContent>
</Dialog>
{/* Stats Bar */}
<div className="border-b bg-muted/30">
<div className="container mx-auto px-6 py-3">

View File

@ -0,0 +1,257 @@
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();
}
}

View File

@ -2224,9 +2224,19 @@ export async function PUT(request: NextRequest) {
return null;
}
const [currentSiteSettings] = await db
.select({
agentWorkflowDefaultModelId: siteSettings.agentWorkflowDefaultModelId,
})
.from(siteSettings)
.limit(1);
const snapshotData = createSystemSettingSnapshotData(
AGENT_WORKFLOW_SNAPSHOT_SCOPE,
currentWorkflowValues
currentWorkflowValues,
{
agentWorkflowDefaultModelId:
String(currentSiteSettings?.agentWorkflowDefaultModelId || "").trim() || null,
}
);
const [snapshot] = await db
.insert(systemSettingSnapshot)

View File

@ -38,6 +38,10 @@ export const SYSTEM_SETTING_SNAPSHOT_SCOPES = {
],
} as const;
export const SYSTEM_SETTING_SNAPSHOT_SITE_FIELDS = {
[AGENT_WORKFLOW_SNAPSHOT_SCOPE]: ["agentWorkflowDefaultModelId"],
} as const;
export type SystemSettingSnapshotScope = keyof typeof SYSTEM_SETTING_SNAPSHOT_SCOPES;
export function getSystemSettingSnapshotScopeKeys(
@ -46,21 +50,83 @@ export function getSystemSettingSnapshotScopeKeys(
return SYSTEM_SETTING_SNAPSHOT_SCOPES[scope];
}
export function getSystemSettingSnapshotScopeSiteFields(
scope: SystemSettingSnapshotScope
) {
return SYSTEM_SETTING_SNAPSHOT_SITE_FIELDS[scope];
}
export function createSystemSettingSnapshotData(
scope: SystemSettingSnapshotScope,
valueByKey: ReadonlyMap<string, unknown>
valueByKey: ReadonlyMap<string, unknown>,
siteSettingsValues: Record<string, unknown> = {}
) {
const keys = getSystemSettingSnapshotScopeKeys(scope);
return Object.fromEntries(keys.map((key) => [key, valueByKey.get(key) ?? null]));
const siteFields = getSystemSettingSnapshotScopeSiteFields(scope);
return {
version: 2,
scope,
systemSettings: Object.fromEntries(keys.map((key) => [key, valueByKey.get(key) ?? null])),
siteSettings: Object.fromEntries(
siteFields.map((field) => [field, siteSettingsValues[field] ?? null])
),
};
}
export function readSystemSettingSnapshotData(
scope: SystemSettingSnapshotScope,
rawData: unknown
) {
const normalized =
rawData && typeof rawData === "object" && !Array.isArray(rawData)
? (rawData as Record<string, unknown>)
: {};
if (
normalized.systemSettings &&
typeof normalized.systemSettings === "object" &&
!Array.isArray(normalized.systemSettings)
) {
return {
systemSettings: normalized.systemSettings as Record<string, unknown>,
siteSettings:
normalized.siteSettings &&
typeof normalized.siteSettings === "object" &&
!Array.isArray(normalized.siteSettings)
? (normalized.siteSettings as Record<string, unknown>)
: {},
};
}
const keys = new Set<string>(getSystemSettingSnapshotScopeKeys(scope));
return {
systemSettings: Object.fromEntries(
Object.entries(normalized).filter(([key]) => keys.has(key))
),
siteSettings: {},
};
}
export function summarizeSystemSettingSnapshotData(
data: Record<string, unknown>
) {
const entries = Object.entries(data);
const systemEntries =
data.systemSettings &&
typeof data.systemSettings === "object" &&
!Array.isArray(data.systemSettings)
? Object.entries(data.systemSettings as Record<string, unknown>)
: Object.entries(data);
const siteEntries =
data.siteSettings &&
typeof data.siteSettings === "object" &&
!Array.isArray(data.siteSettings)
? Object.entries(data.siteSettings as Record<string, unknown>)
: [];
const entries = [...systemEntries, ...siteEntries];
return {
keyCount: entries.length,
nonNullKeyCount: entries.filter(([, value]) => value !== null && value !== undefined).length,
byteLength: Buffer.byteLength(JSON.stringify(data), "utf8"),
siteFieldCount: siteEntries.length,
};
}

View File

@ -22,7 +22,7 @@ async function main() {
const snapshotModule = await import("../../lib/system-settings/snapshot");
const { db } = dbModule;
const { systemSettingSnapshot, systemSettings } = schemaModule;
const { siteSettings, systemSettingSnapshot, systemSettings } = schemaModule;
const {
createSystemSettingSnapshotData,
getSystemSettingSnapshotScopeKeys,
@ -40,9 +40,19 @@ async function main() {
.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);
const snapshotData = createSystemSettingSnapshotData(
scopeArg as keyof typeof snapshotModule.SYSTEM_SETTING_SNAPSHOT_SCOPES,
valueByKey
valueByKey,
{
agentWorkflowDefaultModelId:
String(currentSiteSettings?.agentWorkflowDefaultModelId || "").trim() || null,
}
);
const [snapshot] = await db

View File

@ -21,9 +21,12 @@ async function main() {
const dbModule = await import("../../lib/db");
const schemaModule = await import("../../lib/db/schema");
const snapshotModule = await import("../../lib/system-settings/snapshot");
const { db } = dbModule;
const { systemSettingHistory, systemSettingSnapshot, systemSettings } = schemaModule;
const { siteSettings, systemSettingHistory, systemSettingSnapshot, systemSettings } =
schemaModule;
const { readSystemSettingSnapshotData } = snapshotModule;
const [snapshot] = await db
.select({
@ -39,11 +42,11 @@ async function main() {
throw new Error(`Snapshot not found: ${snapshotId}`);
}
const data =
snapshot.data && typeof snapshot.data === "object" && !Array.isArray(snapshot.data)
? (snapshot.data as Record<string, unknown>)
: {};
const keys = Object.keys(data);
const parsedSnapshot = readSystemSettingSnapshotData(
snapshot.scope as keyof typeof snapshotModule.SYSTEM_SETTING_SNAPSHOT_SCOPES,
snapshot.data
);
const keys = Object.keys(parsedSnapshot.systemSettings);
await db.transaction(async (tx) => {
for (const key of keys) {
@ -53,7 +56,7 @@ async function main() {
.where(eq(systemSettings.key, key))
.limit(1);
const nextValue = data[key] ?? null;
const nextValue = parsedSnapshot.systemSettings[key] ?? null;
await tx.insert(systemSettingHistory).values({
key,
@ -81,6 +84,30 @@ async function main() {
},
});
}
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: null,
})
.where(eq(siteSettings.id, existingSiteSettings.id));
} else {
await tx.insert(siteSettings).values({
siteName: "AI Chatbot",
agentWorkflowDefaultModelId: nextAgentWorkflowDefaultModelId,
updatedAt: new Date(),
updatedBy: null,
});
}
}
});
console.log(