feat: add agent workflow snapshot center
This commit is contained in:
parent
e160d7f217
commit
f31db01b8e
@ -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">
|
||||
|
||||
257
app/api/admin/system-settings/snapshots/route.ts
Normal file
257
app/api/admin/system-settings/snapshots/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user