Compare commits
3 Commits
180ee864d9
...
fec4b9d5aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fec4b9d5aa | ||
|
|
9adb55c6a7 | ||
|
|
ba46f97f3d |
@ -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__";
|
||||
@ -1811,6 +1815,76 @@ interface AgentWorkflowSettingsPayload {
|
||||
agentPsdTemplateSecondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
|
||||
}
|
||||
|
||||
interface AgentWorkflowDestructiveChangePayload {
|
||||
totalBeforeOptionCount: number;
|
||||
totalAfterOptionCount: number;
|
||||
removedOptionCount: number;
|
||||
removedEnabledOptionCount: number;
|
||||
promptClearedCount: number;
|
||||
previewClearedCount: number;
|
||||
removedOptionSamples: string[];
|
||||
promptClearedSamples: string[];
|
||||
previewClearedSamples: string[];
|
||||
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
|
||||
) {
|
||||
const lines = [
|
||||
"这次保存会对风格化工作流做大规模删改,系统已拦截。",
|
||||
"",
|
||||
`删除节点:${payload.removedOptionCount}(其中启用中 ${payload.removedEnabledOptionCount})`,
|
||||
`清空提示词:${payload.promptClearedCount}`,
|
||||
`清空预览图:${payload.previewClearedCount}`,
|
||||
"",
|
||||
];
|
||||
|
||||
const sampleGroups: Array<[string, string[]]> = [
|
||||
["将删除的分支示例", payload.removedOptionSamples],
|
||||
["将清空提示词的示例", payload.promptClearedSamples],
|
||||
["将清空预览图的示例", payload.previewClearedSamples],
|
||||
];
|
||||
|
||||
for (const [label, samples] of sampleGroups) {
|
||||
if (samples.length === 0) {
|
||||
continue;
|
||||
}
|
||||
lines.push(`${label}:`);
|
||||
for (const sample of samples.slice(0, 5)) {
|
||||
lines.push(`- ${sample}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("如果这是你确认要执行的结构性删除,请点击“确定”再次保存。");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
interface AdminAiModelOption {
|
||||
id: string;
|
||||
modelId?: string | null;
|
||||
@ -1915,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
|
||||
@ -1970,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);
|
||||
@ -3899,39 +3985,77 @@ export function AgentWorkflowSettingsClient({
|
||||
changedPayload.agentWorkflowSettingsBaseSignatures[field] = baseSignatures[field];
|
||||
}
|
||||
|
||||
const response = await fetchAgentWorkflowSettingsWithTimeout(
|
||||
"/api/site-settings",
|
||||
{
|
||||
const submitChangedPayload = async (payload: Record<string, unknown>) =>
|
||||
fetchAgentWorkflowSettingsWithTimeout("/api/site-settings", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(changedPayload),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
let response = await submitChangedPayload(changedPayload as Record<string, unknown>);
|
||||
let responsePayload = (await response.json().catch(() => null)) as
|
||||
| {
|
||||
error?: string;
|
||||
details?: Array<{ message?: string; path?: Array<string | number> }>;
|
||||
requiresDestructiveChangeApproval?: boolean;
|
||||
destructiveChange?: AgentWorkflowDestructiveChangePayload;
|
||||
}
|
||||
| null;
|
||||
|
||||
if (
|
||||
!response.ok &&
|
||||
responsePayload?.requiresDestructiveChangeApproval &&
|
||||
responsePayload?.destructiveChange?.approvalToken
|
||||
) {
|
||||
const shouldContinue = window.confirm(
|
||||
formatAgentWorkflowDestructiveChangeConfirmText(
|
||||
responsePayload.destructiveChange
|
||||
)
|
||||
);
|
||||
|
||||
if (!shouldContinue) {
|
||||
toast.info("已取消本次高风险保存");
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
response = await submitChangedPayload({
|
||||
...changedPayload,
|
||||
agentWorkflowDestructiveChangeApproval: {
|
||||
token: responsePayload.destructiveChange.approvalToken,
|
||||
},
|
||||
});
|
||||
responsePayload = (await response.json().catch(() => null)) as
|
||||
| {
|
||||
error?: string;
|
||||
details?: Array<{ message?: string; path?: Array<string | number> }>;
|
||||
}
|
||||
| null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let message = "保存失败";
|
||||
|
||||
try {
|
||||
const payload = await response.json();
|
||||
if (Array.isArray(payload?.details) && payload.details.length > 0) {
|
||||
const firstIssue = payload.details[0] as {
|
||||
message?: string;
|
||||
path?: Array<string | number>;
|
||||
};
|
||||
const pathText =
|
||||
Array.isArray(firstIssue.path) && firstIssue.path.length > 0
|
||||
? firstIssue.path
|
||||
.filter((segment) => segment !== undefined && segment !== null && segment !== "")
|
||||
.join(" / ")
|
||||
: "";
|
||||
const reasonText = String(firstIssue.message || "").trim();
|
||||
if (Array.isArray(responsePayload?.details) && responsePayload.details.length > 0) {
|
||||
const firstIssue = responsePayload.details[0] as {
|
||||
message?: string;
|
||||
path?: Array<string | number>;
|
||||
};
|
||||
const pathText =
|
||||
Array.isArray(firstIssue.path) && firstIssue.path.length > 0
|
||||
? firstIssue.path
|
||||
.filter((segment) => segment !== undefined && segment !== null && segment !== "")
|
||||
.join(" / ")
|
||||
: "";
|
||||
const reasonText = String(firstIssue.message || "").trim();
|
||||
|
||||
message = [payload?.error, pathText, reasonText].filter(Boolean).join(":");
|
||||
} else if (typeof payload?.error === "string" && payload.error.trim()) {
|
||||
message = payload.error.trim();
|
||||
}
|
||||
} catch {
|
||||
// Ignore JSON parsing issues and keep the generic message.
|
||||
message = [responsePayload?.error, pathText, reasonText]
|
||||
.filter(Boolean)
|
||||
.join(":");
|
||||
} else if (
|
||||
typeof responsePayload?.error === "string" &&
|
||||
responsePayload.error.trim()
|
||||
) {
|
||||
message = responsePayload.error.trim();
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
@ -3990,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);
|
||||
@ -5782,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" />
|
||||
@ -5812,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();
|
||||
}
|
||||
}
|
||||
@ -328,7 +328,6 @@ async function createEnhanceProJob(params: {
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
image_url: params.sourceImageUrl,
|
||||
batchSize: params.requestBody.batchSize,
|
||||
seed: params.requestBody.seed,
|
||||
prompt: String(params.requestBody.prompt || "").trim(),
|
||||
output_format: params.requestBody.output_format || params.config.defaultOutputFormat,
|
||||
@ -1065,9 +1064,8 @@ export async function POST(request: NextRequest) {
|
||||
config.maxOutputEdge,
|
||||
);
|
||||
|
||||
const batchSize = config.allowBatchSize
|
||||
? clampInteger(body.batchSize, 1, config.maxBatchSize, 1)
|
||||
: 1;
|
||||
// Krea removed batch generation from the enhance endpoint.
|
||||
const batchSize = 1;
|
||||
const upscalingActivated = body.upscaling_activated === true;
|
||||
const imageScalingFactor = clampNumber(
|
||||
body.image_scaling_factor,
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createHash } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { aiProvider, siteSettings, systemSettingHistory, systemSettings } from "@/lib/db/schema";
|
||||
import {
|
||||
aiProvider,
|
||||
siteSettings,
|
||||
systemSettingHistory,
|
||||
systemSettingSnapshot,
|
||||
systemSettings,
|
||||
} from "@/lib/db/schema";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { isAgentWorkflowAdminType, isSiteAdminType } from "@/lib/middleware/admin";
|
||||
@ -35,8 +42,10 @@ import {
|
||||
AGENT_WORKFLOW_UI_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH,
|
||||
defaultAgentWorkflowUiConfig,
|
||||
isAgentWorkflowDestructiveChangeSignificant,
|
||||
normalizeAgentWorkflowStepOrdering,
|
||||
normalizeAgentWorkflowUiConfig,
|
||||
summarizeAgentWorkflowDestructiveChange,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import {
|
||||
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
|
||||
@ -105,6 +114,11 @@ import {
|
||||
PUBLIC_SITE_SETTINGS_CACHE_TTL_MS,
|
||||
setScopedSiteSettingsCache,
|
||||
} from "@/lib/site-settings/public-cache";
|
||||
import {
|
||||
AGENT_WORKFLOW_SNAPSHOT_SCOPE,
|
||||
createSystemSettingSnapshotData,
|
||||
summarizeSystemSettingSnapshotData,
|
||||
} from "@/lib/system-settings/snapshot";
|
||||
import {
|
||||
getHomeChatAnimationHtmlBytes,
|
||||
HOME_CHAT_ANIMATION_CONFIG_KEY,
|
||||
@ -1612,6 +1626,11 @@ const updateSchema = z.object({
|
||||
agentWorkflowSettingsBaseSignatures: z
|
||||
.record(z.string().trim().min(1).max(128))
|
||||
.optional(),
|
||||
agentWorkflowDestructiveChangeApproval: z
|
||||
.object({
|
||||
token: z.string().trim().min(16).max(128),
|
||||
})
|
||||
.optional(),
|
||||
agentWorkflowUiConfig: z
|
||||
.object({
|
||||
inputOptions: z
|
||||
@ -1965,6 +1984,7 @@ export async function PUT(request: NextRequest) {
|
||||
"agentPresetLibraryConfig",
|
||||
"agentWorkflowSettingsBaseSignature",
|
||||
"agentWorkflowSettingsBaseSignatures",
|
||||
"agentWorkflowDestructiveChangeApproval",
|
||||
"agentWorkflowUiConfig",
|
||||
"agentWorkflowMaterialApiConfig",
|
||||
"agentWorkflowGoodsInfoApiConfig",
|
||||
@ -1992,6 +2012,7 @@ export async function PUT(request: NextRequest) {
|
||||
agentSecondaryTypeConfig,
|
||||
agentWorkflowSettingsBaseSignature,
|
||||
agentWorkflowSettingsBaseSignatures,
|
||||
agentWorkflowDestructiveChangeApproval,
|
||||
agentWorkflowUiConfig,
|
||||
agentWorkflowMaterialApiConfig,
|
||||
agentWorkflowGoodsInfoApiConfig,
|
||||
@ -2033,6 +2054,29 @@ export async function PUT(request: NextRequest) {
|
||||
.map(([field]) => field);
|
||||
|
||||
const isUpdatingAgentWorkflowSettings = updatedAgentWorkflowFields.length > 0;
|
||||
let currentWorkflowValues = new Map<string, unknown>();
|
||||
let currentWorkflowSectionSignatures:
|
||||
| ReturnType<typeof createAgentWorkflowSettingsSectionSignatures>
|
||||
| null = null;
|
||||
let workflowDestructiveChangeSummary:
|
||||
| ReturnType<typeof summarizeAgentWorkflowDestructiveChange>
|
||||
| null = null;
|
||||
let agentWorkflowSnapshotCreated = false;
|
||||
|
||||
const createAgentWorkflowDestructiveChangeApprovalToken = (params: {
|
||||
currentSignature: string;
|
||||
nextConfig: unknown;
|
||||
summary: ReturnType<typeof summarizeAgentWorkflowDestructiveChange>;
|
||||
}) =>
|
||||
createHash("sha256")
|
||||
.update(
|
||||
JSON.stringify({
|
||||
currentSignature: params.currentSignature,
|
||||
nextConfig: params.nextConfig,
|
||||
summary: params.summary,
|
||||
})
|
||||
)
|
||||
.digest("hex");
|
||||
|
||||
if (isUpdatingAgentWorkflowSettings) {
|
||||
if (!agentWorkflowSettingsBaseSignatures && !agentWorkflowSettingsBaseSignature) {
|
||||
@ -2052,10 +2096,10 @@ export async function PUT(request: NextRequest) {
|
||||
})
|
||||
.from(systemSettings)
|
||||
.where(inArray(systemSettings.key, [...AGENT_WORKFLOW_SETTINGS_CONFLICT_KEYS]));
|
||||
const currentWorkflowValues = new Map<string, unknown>(
|
||||
currentWorkflowValues = new Map<string, unknown>(
|
||||
currentWorkflowRows.map((row) => [row.key, row.value])
|
||||
);
|
||||
const currentSectionSignatures =
|
||||
currentWorkflowSectionSignatures =
|
||||
createAgentWorkflowSettingsSectionSignatures(currentWorkflowValues);
|
||||
const currentWorkflowSignature =
|
||||
agentWorkflowSettingsBaseSignature && !agentWorkflowSettingsBaseSignatures
|
||||
@ -2064,7 +2108,8 @@ export async function PUT(request: NextRequest) {
|
||||
const hasWorkflowConflict = agentWorkflowSettingsBaseSignatures
|
||||
? updatedAgentWorkflowFields.some(
|
||||
(field) =>
|
||||
currentSectionSignatures[field] !== agentWorkflowSettingsBaseSignatures[field]
|
||||
currentWorkflowSectionSignatures![field] !==
|
||||
agentWorkflowSettingsBaseSignatures[field]
|
||||
)
|
||||
: currentWorkflowSignature !== agentWorkflowSettingsBaseSignature;
|
||||
|
||||
@ -2077,6 +2122,44 @@ export async function PUT(request: NextRequest) {
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
if (agentWorkflowUiConfig !== undefined) {
|
||||
const currentWorkflowUiConfig = normalizeAgentWorkflowUiConfig(
|
||||
currentWorkflowValues.get(AGENT_WORKFLOW_UI_CONFIG_KEY)
|
||||
);
|
||||
const nextWorkflowUiConfig = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig);
|
||||
workflowDestructiveChangeSummary = summarizeAgentWorkflowDestructiveChange(
|
||||
currentWorkflowUiConfig,
|
||||
nextWorkflowUiConfig
|
||||
);
|
||||
|
||||
if (isAgentWorkflowDestructiveChangeSignificant(workflowDestructiveChangeSummary)) {
|
||||
const approvalToken = createAgentWorkflowDestructiveChangeApprovalToken({
|
||||
currentSignature:
|
||||
currentWorkflowSectionSignatures.agentWorkflowUiConfig ||
|
||||
createAgentWorkflowSettingsSignature(
|
||||
new Map([[AGENT_WORKFLOW_UI_CONFIG_KEY, currentWorkflowUiConfig]])
|
||||
),
|
||||
nextConfig: nextWorkflowUiConfig,
|
||||
summary: workflowDestructiveChangeSummary,
|
||||
});
|
||||
|
||||
if (agentWorkflowDestructiveChangeApproval?.token !== approvalToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"本次保存会删除大量风格化工作流节点或清空关键内容,已被系统拦截。请确认后再次提交。",
|
||||
requiresDestructiveChangeApproval: true,
|
||||
destructiveChange: {
|
||||
...workflowDestructiveChangeSummary,
|
||||
approvalToken,
|
||||
},
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const persistSystemSettingWithHistory = async (
|
||||
@ -2136,6 +2219,47 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
};
|
||||
|
||||
const createAgentWorkflowScopeSnapshot = async () => {
|
||||
if (!isUpdatingAgentWorkflowSettings || agentWorkflowSnapshotCreated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [currentSiteSettings] = await db
|
||||
.select({
|
||||
agentWorkflowDefaultModelId: siteSettings.agentWorkflowDefaultModelId,
|
||||
})
|
||||
.from(siteSettings)
|
||||
.limit(1);
|
||||
const snapshotData = createSystemSettingSnapshotData(
|
||||
AGENT_WORKFLOW_SNAPSHOT_SCOPE,
|
||||
currentWorkflowValues,
|
||||
{
|
||||
agentWorkflowDefaultModelId:
|
||||
String(currentSiteSettings?.agentWorkflowDefaultModelId || "").trim() || null,
|
||||
}
|
||||
);
|
||||
const [snapshot] = await db
|
||||
.insert(systemSettingSnapshot)
|
||||
.values({
|
||||
scope: AGENT_WORKFLOW_SNAPSHOT_SCOPE,
|
||||
label: workflowDestructiveChangeSummary
|
||||
? "before-destructive-agent-workflow-save"
|
||||
: "before-agent-workflow-save",
|
||||
summary: {
|
||||
...summarizeSystemSettingSnapshotData(snapshotData),
|
||||
updatedFields: updatedAgentWorkflowFields,
|
||||
destructiveChange: workflowDestructiveChangeSummary,
|
||||
},
|
||||
data: snapshotData,
|
||||
createdAt: new Date(),
|
||||
createdBy: session.user.id,
|
||||
})
|
||||
.returning({ id: systemSettingSnapshot.id });
|
||||
|
||||
agentWorkflowSnapshotCreated = true;
|
||||
return snapshot?.id ?? null;
|
||||
};
|
||||
|
||||
const persistHomeAnimationConfig = async () => {
|
||||
if (homeAnimationConfig === undefined) {
|
||||
return;
|
||||
@ -2175,6 +2299,8 @@ export async function PUT(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await createAgentWorkflowScopeSnapshot();
|
||||
|
||||
// 查找现有设置
|
||||
const [existing] = await db.select().from(siteSettings).limit(1);
|
||||
|
||||
@ -798,6 +798,139 @@ export function countAgentWorkflowOptionTree(
|
||||
return flattenAgentWorkflowOptionTree(options).length;
|
||||
}
|
||||
|
||||
export interface AgentWorkflowDestructiveChangeSummary {
|
||||
totalBeforeOptionCount: number;
|
||||
totalAfterOptionCount: number;
|
||||
removedOptionCount: number;
|
||||
removedEnabledOptionCount: number;
|
||||
promptClearedCount: number;
|
||||
previewClearedCount: number;
|
||||
removedOptionSamples: string[];
|
||||
promptClearedSamples: string[];
|
||||
previewClearedSamples: string[];
|
||||
}
|
||||
|
||||
const AGENT_WORKFLOW_DESTRUCTIVE_CHANGE_SAMPLE_LIMIT = 12;
|
||||
|
||||
function collectAgentWorkflowAuditEntries(
|
||||
steps: AgentWorkflowStepUiConfig[],
|
||||
stepKeyPath: string[] = [],
|
||||
stepLabelPath: string[] = [],
|
||||
output: Array<{
|
||||
identityPath: string;
|
||||
displayPath: string;
|
||||
enabled: boolean;
|
||||
hasPrompt: boolean;
|
||||
hasPreview: boolean;
|
||||
}> = []
|
||||
) {
|
||||
for (const step of [...(steps || [])].sort((left, right) => left.sortOrder - right.sortOrder)) {
|
||||
const nextStepKeyPath = [...stepKeyPath, `s:${String(step.key || "").trim()}`];
|
||||
const nextStepLabelPath = [
|
||||
...stepLabelPath,
|
||||
String(step.title || step.key || "未命名模块").trim() || "未命名模块",
|
||||
];
|
||||
|
||||
const visitOptions = (
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
parentKeyPath: string[],
|
||||
parentLabelPath: string[]
|
||||
) => {
|
||||
for (const option of [...(options || [])].sort((left, right) => left.sortOrder - right.sortOrder)) {
|
||||
const optionKey = String(option.key || "").trim() || "option";
|
||||
const optionLabel =
|
||||
String(option.buttonName || option.key || "未命名卡片").trim() || "未命名卡片";
|
||||
const nextKeyPath = [...parentKeyPath, `o:${optionKey}`];
|
||||
const nextLabelPath = [...parentLabelPath, optionLabel];
|
||||
output.push({
|
||||
identityPath: nextKeyPath.join(">"),
|
||||
displayPath: nextLabelPath.join(" > "),
|
||||
enabled: option.enabled !== false,
|
||||
hasPrompt: Boolean(String(option.promptFragment || "").trim()),
|
||||
hasPreview: hasAgentWorkflowOptionOwnPreview(option),
|
||||
});
|
||||
visitOptions(option.children || [], nextKeyPath, nextLabelPath);
|
||||
}
|
||||
};
|
||||
|
||||
visitOptions(step.options || [], nextStepKeyPath, nextStepLabelPath);
|
||||
collectAgentWorkflowAuditEntries(
|
||||
step.children || [],
|
||||
nextStepKeyPath,
|
||||
nextStepLabelPath,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export function summarizeAgentWorkflowDestructiveChange(
|
||||
beforeConfig: AgentWorkflowUiConfig,
|
||||
afterConfig: AgentWorkflowUiConfig
|
||||
): AgentWorkflowDestructiveChangeSummary {
|
||||
const beforeEntries = collectAgentWorkflowAuditEntries(beforeConfig.steps || []);
|
||||
const afterEntries = collectAgentWorkflowAuditEntries(afterConfig.steps || []);
|
||||
const afterByIdentityPath = new Map(
|
||||
afterEntries.map((entry) => [entry.identityPath, entry])
|
||||
);
|
||||
|
||||
const removedEntries = beforeEntries.filter(
|
||||
(entry) => !afterByIdentityPath.has(entry.identityPath)
|
||||
);
|
||||
const promptClearedEntries = beforeEntries.filter((entry) => {
|
||||
const nextEntry = afterByIdentityPath.get(entry.identityPath);
|
||||
return Boolean(nextEntry && entry.hasPrompt && !nextEntry.hasPrompt);
|
||||
});
|
||||
const previewClearedEntries = beforeEntries.filter((entry) => {
|
||||
const nextEntry = afterByIdentityPath.get(entry.identityPath);
|
||||
return Boolean(nextEntry && entry.hasPreview && !nextEntry.hasPreview);
|
||||
});
|
||||
|
||||
return {
|
||||
totalBeforeOptionCount: beforeEntries.length,
|
||||
totalAfterOptionCount: afterEntries.length,
|
||||
removedOptionCount: removedEntries.length,
|
||||
removedEnabledOptionCount: removedEntries.filter((entry) => entry.enabled).length,
|
||||
promptClearedCount: promptClearedEntries.length,
|
||||
previewClearedCount: previewClearedEntries.length,
|
||||
removedOptionSamples: removedEntries
|
||||
.slice(0, AGENT_WORKFLOW_DESTRUCTIVE_CHANGE_SAMPLE_LIMIT)
|
||||
.map((entry) => entry.displayPath),
|
||||
promptClearedSamples: promptClearedEntries
|
||||
.slice(0, AGENT_WORKFLOW_DESTRUCTIVE_CHANGE_SAMPLE_LIMIT)
|
||||
.map((entry) => entry.displayPath),
|
||||
previewClearedSamples: previewClearedEntries
|
||||
.slice(0, AGENT_WORKFLOW_DESTRUCTIVE_CHANGE_SAMPLE_LIMIT)
|
||||
.map((entry) => entry.displayPath),
|
||||
};
|
||||
}
|
||||
|
||||
export function isAgentWorkflowDestructiveChangeSignificant(
|
||||
summary: AgentWorkflowDestructiveChangeSummary
|
||||
) {
|
||||
if (summary.removedEnabledOptionCount >= 10) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (summary.removedOptionCount >= 20) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
summary.totalBeforeOptionCount > 0 &&
|
||||
summary.removedOptionCount / summary.totalBeforeOptionCount >= 0.03
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (summary.promptClearedCount >= 10) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return summary.previewClearedCount >= 5;
|
||||
}
|
||||
|
||||
export function hasAgentWorkflowOptionOwnPreview(
|
||||
option: AgentWorkflowOptionUiConfig
|
||||
): boolean {
|
||||
|
||||
@ -27,23 +27,33 @@ const REFERENCE_INPUT_FIELDS = new Set([
|
||||
"imageUrl",
|
||||
"imageUrls",
|
||||
"image_url",
|
||||
"image_urls",
|
||||
"styleImages",
|
||||
"style_images",
|
||||
"imageStyleRefs",
|
||||
"image_style_references",
|
||||
"characterReferenceImages",
|
||||
"character_reference_images",
|
||||
"backgroundImage",
|
||||
"referenceImages",
|
||||
"reference_images",
|
||||
"startImage",
|
||||
"endImage",
|
||||
"init_image_url",
|
||||
"references",
|
||||
"subjects",
|
||||
"styleReferences",
|
||||
"style_references",
|
||||
"elementReferences",
|
||||
"element_references",
|
||||
"videoReference",
|
||||
"video_reference",
|
||||
"startVideo",
|
||||
"init_video",
|
||||
"referenceVideos",
|
||||
"reference_videos",
|
||||
"referenceAudios",
|
||||
"reference_audios",
|
||||
]);
|
||||
|
||||
function hasReferenceInput(fields: string[]) {
|
||||
@ -68,12 +78,15 @@ function createDefaultParams(
|
||||
|
||||
function createImageConfig(fields: string[]) {
|
||||
return {
|
||||
maxImages: fields.includes("batchSize") ? 4 : 1,
|
||||
// Krea removed batch generation; one request now produces one output.
|
||||
maxImages: 1,
|
||||
supportsSize: fields.includes("width") && fields.includes("height"),
|
||||
supports1K: true,
|
||||
supports2K: true,
|
||||
supports4K: fields.includes("resolution") || fields.includes("width"),
|
||||
supportsPromptExtend: fields.includes("skipPromptExpansion"),
|
||||
supportsPromptExtend:
|
||||
fields.includes("skipPromptExpansion") ||
|
||||
fields.includes("skip_prompt_expansion"),
|
||||
supportsNegativePrompt: fields.includes("negative_prompt"),
|
||||
supportsWatermark: false,
|
||||
supportsSeed: fields.includes("seed"),
|
||||
@ -233,62 +246,65 @@ function videoPreset(
|
||||
}
|
||||
|
||||
export const KREA_MODEL_PRESETS: KreaModelPreset[] = [
|
||||
imagePreset(10, "/generate/image/bfl/flux-1-dev", "Flux", ["strength", "seed", "steps", "styleImages", "imageStyleRefs", "prompt", "width", "height", "guidance_scale_flux", "imageUrl", "relaxedModeAccess", "styles"], ["prompt"]),
|
||||
imagePreset(20, "/generate/image/bfl/flux-1-kontext-dev", "Flux Kontext", ["strength", "seed", "steps", "styleImages", "imageStyleRefs", "prompt", "width", "height", "guidance_scale_flux", "imageUrl", "relaxedModeAccess", "styles"], ["prompt"]),
|
||||
imagePreset(30, "/generate/image/google/nano-banana-pro", "Nano Banana Pro", ["batchSize", "prompt", "width", "height", "imageUrls", "aspectRatio", "styleImages", "resolution"], ["prompt"]),
|
||||
imagePreset(40, "/generate/image/google/nano-banana-2", "Nano Banana 2", ["batchSize", "prompt", "width", "height", "imageUrls", "aspectRatio", "styleImages", "resolution"], ["prompt"]),
|
||||
imagePreset(50, "/generate/image/google/nano-banana", "Nano Banana", ["batchSize", "prompt", "width", "height", "imageUrls", "styleImages", "aspectRatio"], ["prompt"]),
|
||||
imagePreset(60, "/generate/image/bfl/flux-1.1-pro", "Flux 1.1 Pro", ["batchSize", "prompt", "width", "height", "seed"], ["prompt", "width", "height"]),
|
||||
imagePreset(70, "/generate/image/bfl/flux-1.1-pro-ultra", "Flux 1.1 Pro Ultra", ["batchSize", "prompt", "width", "height", "seed", "raw"], ["prompt"]),
|
||||
imagePreset(80, "/generate/image/ideogram/ideogram-2-turbo", "Ideogram 2.0A Turbo", ["batchSize", "prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(90, "/generate/image/ideogram/ideogram-3", "Ideogram 3.0", ["batchSize", "prompt", "width", "height", "seed", "styleImages", "characterReferenceImages"], ["prompt"]),
|
||||
imagePreset(100, "/generate/image/google/imagen-3", "Imagen 3", ["batchSize", "prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(110, "/generate/image/google/imagen-4", "Imagen 4", ["batchSize", "prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(120, "/generate/image/google/imagen-4-fast", "Imagen 4 Fast", ["batchSize", "prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(130, "/generate/image/google/imagen-4-ultra", "Imagen 4 Ultra", ["batchSize", "prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(140, "/generate/image/runway/gen-4-image", "Runway Gen-4", ["batchSize", "prompt", "width", "height", "seed", "referenceImages"], ["prompt", "referenceImages"]),
|
||||
imagePreset(150, "/generate/image/openai/gpt-image", "ChatGPT Image", ["batchSize", "prompt", "width", "height", "quality", "backgroundImage", "imageUrls", "styleImages", "styles"], ["prompt"]),
|
||||
imagePreset(160, "/generate/image/bytedance/seedream-3", "Seedream 3", ["batchSize", "prompt", "width", "height", "seed", "model"], ["prompt", "model"]),
|
||||
imagePreset(170, "/generate/image/bytedance/seedream-4", "Seedream 4", ["batchSize", "prompt", "seed", "width", "height", "styleImages"], ["prompt", "width", "height"]),
|
||||
imagePreset(180, "/generate/image/bytedance/seedream-5-lite", "Seedream 5 Lite", ["batchSize", "prompt", "seed", "width", "height", "styleImages"], ["prompt", "width", "height"]),
|
||||
imagePreset(190, "/generate/image/qwen/2512", "Qwen 2512", ["prompt", "negative_prompt", "width", "height", "num_inference_steps", "cfg_scale", "seed", "batchSize", "skipPromptExpansion", "styles"], ["prompt"]),
|
||||
imagePreset(200, "/generate/image/z-image/z-image", "Z Image", ["prompt", "width", "height", "batchSize", "seed", "imageUrl", "denoising_strength", "styleImages", "skipPromptExpansion", "styles"], ["prompt", "width", "height"]),
|
||||
imagePreset(210, "/generate/image/bytedance/seededit", "SeedEdit", ["batchSize", "prompt", "seed", "imageUrl"], ["prompt", "imageUrl"]),
|
||||
enhancePreset(220, "/generate/enhance/topaz/generative-enhance", "Topaz Generative", ["batchSize", "width", "height", "seed", "prompt", "output_format", "subject_detection", "face_enhancement", "face_enhancement_creativity", "face_enhancement_strength", "crop_to_fill", "image_url", "model", "upscaling_activated", "image_scaling_factor", "creativity", "texture", "sharpen", "denoise", "detail"], ["width", "height", "image_url"]),
|
||||
enhancePreset(230, "/generate/enhance/topaz/standard-enhance", "Topaz", ["batchSize", "width", "height", "seed", "prompt", "output_format", "subject_detection", "face_enhancement", "face_enhancement_creativity", "face_enhancement_strength", "crop_to_fill", "image_url", "model", "upscaling_activated", "image_scaling_factor", "sharpen", "denoise", "fix_compression", "strength"], ["width", "height", "image_url", "model"]),
|
||||
enhancePreset(240, "/generate/enhance/topaz/bloom-enhance", "Topaz Bloom", ["batchSize", "width", "height", "seed", "prompt", "output_format", "crop_to_fill", "image_url", "model", "creativity", "face_preservation", "color_preservation", "upscaling_activated", "image_scaling_factor"], ["width", "height", "image_url", "model"]),
|
||||
videoPreset(250, "/generate/video/kling/kling-1", "Kling 1.0", ["startImage", "prompt", "aspectRatio", "duration", "cameraControl", "endImage", "mode"], ["prompt"]),
|
||||
videoPreset(260, "/generate/video/kling/kling-1.5", "Kling 1.5", ["startImage", "prompt", "aspectRatio", "duration", "cameraControl", "endImage"], ["prompt"]),
|
||||
videoPreset(270, "/generate/video/kling/kling-1.6", "Kling 1.6", ["startImage", "prompt", "aspectRatio", "duration", "cameraControl", "endImage", "mode"], ["prompt"]),
|
||||
videoPreset(280, "/generate/video/kling/kling-2", "Kling 2.0", ["startImage", "prompt", "aspectRatio", "endImage", "mode"], ["prompt"]),
|
||||
videoPreset(290, "/generate/video/kling/kling-2.1", "Kling 2.1", ["startImage", "prompt", "aspectRatio", "duration", "endImage", "mode"], ["prompt"]),
|
||||
videoPreset(300, "/generate/video/kling/kling-2.5", "Kling 2.5", ["startImage", "prompt", "aspectRatio", "duration", "endImage"], ["prompt"]),
|
||||
videoPreset(310, "/generate/video/kling/kling-2.6", "Kling 2.6", ["startImage", "prompt", "aspectRatio", "duration", "endImage", "generateAudio"], ["prompt"]),
|
||||
videoPreset(320, "/generate/video/kling/kling-o1", "Kling o1", ["startImage", "prompt", "aspectRatio", "duration", "endImage", "styleReferences", "elementReferences", "videoReference"], ["prompt"]),
|
||||
videoPreset(330, "/generate/video/minimax/hailuo", "Hailuo", ["startImage", "prompt", "expandPrompt", "cameraControl", "model", "subjects"], ["prompt"]),
|
||||
videoPreset(340, "/generate/video/minimax/hailuo-02", "Hailuo 02", ["startImage", "prompt", "expandPrompt", "endImage", "resolution", "duration"], ["prompt"]),
|
||||
videoPreset(350, "/generate/video/minimax/hailuo-2.3", "Hailuo 2.3", ["startImage", "prompt", "expandPrompt", "endImage", "resolution", "duration"], ["prompt"]),
|
||||
videoPreset(360, "/generate/video/minimax/hailuo-2.3-fast", "Hailuo 2.3 Fast", ["startImage", "prompt", "expandPrompt", "endImage", "resolution", "duration"], ["prompt"]),
|
||||
videoPreset(370, "/generate/video/alibaba/wan-2.1", "Wan 2.1", ["startImage", "prompt", "width", "height", "endImage", "aspectRatio", "frame_num", "styles"], ["prompt", "width", "height"]),
|
||||
videoPreset(380, "/generate/video/alibaba/wan-2.2", "Wan 2.2", ["startImage", "guide_scale", "prompt", "width", "height", "aspectRatio", "frame_num", "styles"], ["prompt", "width", "height"]),
|
||||
videoPreset(390, "/generate/video/alibaba/wan-2.5", "Wan 2.5", ["startImage", "prompt", "aspectRatio", "resolution", "enablePromptExpansion", "duration", "seed", "audio_url", "generateAudio"], ["prompt"]),
|
||||
videoPreset(400, "/generate/video/google/veo-2", "Veo 2", ["startImage", "prompt", "aspectRatio", "duration", "image_url"], ["prompt"]),
|
||||
videoPreset(410, "/generate/video/google/veo-3", "Veo 3", ["startImage", "prompt", "aspectRatio", "duration", "generateAudio", "resolution"], ["prompt"]),
|
||||
videoPreset(420, "/generate/video/google/veo-3-fast", "Veo 3 Fast", ["startImage", "prompt", "aspectRatio", "duration", "generateAudio", "resolution"], ["prompt"]),
|
||||
videoPreset(430, "/generate/video/google/veo-3.1", "Veo 3.1", ["startImage", "prompt", "aspectRatio", "duration", "generateAudio", "resolution", "endImage", "referenceImages"], ["prompt"]),
|
||||
videoPreset(440, "/generate/video/google/veo-3.1-fast", "Veo 3.1 Fast", ["startImage", "prompt", "aspectRatio", "duration", "generateAudio", "resolution", "endImage", "referenceImages"], ["prompt"]),
|
||||
videoPreset(450, "/generate/video/google/veo-3.1-lite", "Veo 3.1 Lite", ["startImage", "prompt", "aspectRatio", "duration", "resolution", "endImage"], ["prompt"]),
|
||||
videoPreset(460, "/generate/video/bytedance/seedance-1.0-lite", "Seedance Lite", ["startImage", "prompt", "aspectRatio", "endImage", "referenceImages", "duration", "resolution"], ["prompt"]),
|
||||
videoPreset(470, "/generate/video/bytedance/seedance-1.0-pro", "Seedance Pro", ["startImage", "prompt", "aspectRatio", "endImage", "referenceImages", "duration", "resolution"], ["prompt"]),
|
||||
videoPreset(480, "/generate/video/bytedance/seedance-1.0-pro-fast", "Seedance Pro Fast", ["startImage", "prompt", "aspectRatio", "duration", "resolution"], ["prompt"]),
|
||||
videoPreset(490, "/generate/video/bytedance/seedance-2", "Seedance 2.0", ["startImage", "prompt", "aspectRatio", "endImage", "referenceImages", "referenceVideos", "referenceAudios", "duration", "generateAudio", "resolution", "seed"], ["prompt"]),
|
||||
videoPreset(500, "/generate/video/bytedance/seedance-2-fast", "Seedance 2.0 Fast", ["startImage", "prompt", "aspectRatio", "endImage", "referenceImages", "referenceVideos", "referenceAudios", "duration", "generateAudio", "resolution", "seed"], ["prompt"]),
|
||||
videoPreset(510, "/generate/video/openai/sora-2", "Sora 2", ["startImage", "prompt", "duration", "aspectRatio"], ["prompt"]),
|
||||
videoPreset(520, "/generate/video/luma/ray-2", "Ray 2", ["startImage", "prompt", "aspectRatio", "loop", "width", "height"], ["prompt", "width", "height"]),
|
||||
videoPreset(530, "/generate/video/runway/gen-3", "Runway Gen-3", ["startImage", "prompt", "aspectRatio", "duration", "seed"], ["prompt"]),
|
||||
videoPreset(540, "/generate/video/runway/gen-4-video", "Runway Gen-4", ["startImage", "prompt", "aspectRatio", "duration", "seed"], ["prompt"]),
|
||||
videoPreset(550, "/generate/video/runway/gen-4.5", "Runway Gen-4.5", ["startImage", "prompt", "aspectRatio", "duration", "seed"], ["prompt"]),
|
||||
videoPreset(560, "/generate/video/runway/aleph", "Runway Aleph", ["startImage", "startVideo", "startVideoTrim", "endVideoTrim", "prompt", "init_video", "init_image_url", "references", "ratio", "seed", "contentModeration"], ["prompt", "init_video"]),
|
||||
imagePreset(10, "/generate/image/krea/krea-2/medium", "Krea 2 Medium", ["prompt", "seed", "styles", "image_style_references", "aspect_ratio", "resolution", "creativity", "moodboards"], ["prompt", "aspect_ratio", "resolution"]),
|
||||
imagePreset(20, "/generate/image/krea/krea-2/large", "Krea 2 Large", ["prompt", "seed", "styles", "image_style_references", "aspect_ratio", "resolution", "creativity", "moodboards"], ["prompt", "aspect_ratio", "resolution"]),
|
||||
imagePreset(30, "/generate/image/bfl/flux-1-dev", "Flux", ["prompt", "seed", "steps", "width", "height", "image_url", "styles", "strength", "guidance_scale", "style_images", "image_style_references"], ["prompt"]),
|
||||
imagePreset(40, "/generate/image/bfl/flux-1-kontext-dev", "Flux Kontext", ["prompt", "seed", "steps", "width", "height", "image_url", "strength", "guidance_scale", "style_images"], ["prompt"]),
|
||||
imagePreset(50, "/generate/image/google/nano-banana-pro", "Nano Banana Pro", ["prompt", "width", "height", "image_urls", "aspect_ratio", "resolution", "style_images"], ["prompt"]),
|
||||
imagePreset(60, "/generate/image/google/nano-banana-2", "Nano Banana 2", ["prompt", "width", "height", "image_urls", "aspect_ratio", "resolution", "style_images"], ["prompt"]),
|
||||
imagePreset(70, "/generate/image/google/nano-banana", "Nano Banana", ["prompt", "width", "height", "image_urls", "aspect_ratio", "style_images"], ["prompt"]),
|
||||
imagePreset(80, "/generate/image/bfl/flux-1.1-pro", "Flux 1.1 Pro", ["prompt", "width", "height", "seed"], ["prompt", "width", "height"]),
|
||||
imagePreset(90, "/generate/image/bfl/flux-1.1-pro-ultra", "Flux 1.1 Pro Ultra", ["prompt", "width", "height", "seed", "raw"], ["prompt"]),
|
||||
imagePreset(100, "/generate/image/ideogram/ideogram-2-turbo", "Ideogram 2.0A Turbo", ["prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(110, "/generate/image/ideogram/ideogram-3", "Ideogram 3.0", ["prompt", "width", "height", "seed", "character_reference_images", "style_images"], ["prompt"]),
|
||||
imagePreset(120, "/generate/image/google/imagen-3", "Imagen 3", ["prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(130, "/generate/image/google/imagen-4", "Imagen 4", ["prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(140, "/generate/image/google/imagen-4-fast", "Imagen 4 Fast", ["prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(150, "/generate/image/google/imagen-4-ultra", "Imagen 4 Ultra", ["prompt", "width", "height", "seed"], ["prompt"]),
|
||||
imagePreset(160, "/generate/image/runway/gen-4-image", "Runway Gen-4", ["prompt", "width", "height", "seed", "reference_images"], ["prompt", "reference_images"]),
|
||||
imagePreset(170, "/generate/image/openai/gpt-image", "ChatGPT Image", ["prompt", "width", "height", "quality", "image_urls", "styles", "style_images"], ["prompt"]),
|
||||
imagePreset(180, "/generate/image/openai/gpt-image-2", "ChatGPT Image 2", ["prompt", "quality", "image_urls", "width", "height", "aspect_ratio", "resolution", "style_images"], ["prompt"]),
|
||||
imagePreset(190, "/generate/image/luma/uni-1", "Luma Uni 1", ["prompt", "width", "height", "mode", "style", "output_format", "web_search", "style_images"], ["prompt"]),
|
||||
imagePreset(200, "/generate/image/bytedance/seedream-4", "Seedream 4", ["prompt", "width", "height", "seed", "style_images"], ["prompt", "width", "height"]),
|
||||
imagePreset(210, "/generate/image/bytedance/seedream-5-lite", "Seedream 5 Lite", ["prompt", "width", "height", "seed", "style_images"], ["prompt", "width", "height"]),
|
||||
imagePreset(220, "/generate/image/qwen/2512", "Qwen 2512", ["prompt", "negative_prompt", "width", "height", "num_inference_steps", "cfg_scale", "seed", "skip_prompt_expansion", "styles"], ["prompt"]),
|
||||
imagePreset(230, "/generate/image/z-image/z-image", "Z Image", ["prompt", "seed", "denoising_strength", "styles", "skip_prompt_expansion", "aspect_ratio", "resolution", "image_url", "style_images"], ["prompt", "aspect_ratio", "resolution"]),
|
||||
imagePreset(240, "/generate/image/bytedance/seededit", "SeedEdit", ["prompt", "seed", "image_url"], ["prompt", "image_url"]),
|
||||
enhancePreset(250, "/generate/enhance/topaz/generative-enhance", "Topaz Generative", ["prompt", "width", "height", "seed", "image_url", "model", "output_format", "subject_detection", "face_enhancement", "face_enhancement_creativity", "face_enhancement_strength", "crop_to_fill", "upscaling_activated", "image_scaling_factor", "creativity", "texture", "sharpen", "denoise", "detail"], ["width", "height", "image_url"]),
|
||||
enhancePreset(260, "/generate/enhance/topaz/standard-enhance", "Topaz", ["prompt", "width", "height", "seed", "image_url", "model", "output_format", "subject_detection", "face_enhancement", "face_enhancement_creativity", "face_enhancement_strength", "crop_to_fill", "upscaling_activated", "image_scaling_factor", "sharpen", "denoise", "fix_compression", "strength"], ["width", "height", "image_url", "model"]),
|
||||
enhancePreset(270, "/generate/enhance/topaz/bloom-enhance", "Topaz Bloom", ["prompt", "width", "height", "seed", "image_url", "output_format", "crop_to_fill", "creativity", "face_preservation", "color_preservation", "upscaling_activated", "image_scaling_factor"], ["width", "height", "image_url"]),
|
||||
videoPreset(280, "/generate/video/kling/kling-1", "Kling 1.0", ["startImage", "prompt", "aspectRatio", "duration", "cameraControl", "endImage", "mode"], ["prompt"]),
|
||||
videoPreset(290, "/generate/video/kling/kling-1.5", "Kling 1.5", ["startImage", "prompt", "aspectRatio", "duration", "cameraControl", "endImage"], ["prompt"]),
|
||||
videoPreset(300, "/generate/video/kling/kling-1.6", "Kling 1.6", ["startImage", "prompt", "aspectRatio", "duration", "cameraControl", "endImage", "mode"], ["prompt"]),
|
||||
videoPreset(310, "/generate/video/kling/kling-2", "Kling 2.0", ["startImage", "prompt", "aspectRatio", "endImage", "mode"], ["prompt"]),
|
||||
videoPreset(320, "/generate/video/kling/kling-2.1", "Kling 2.1", ["startImage", "prompt", "aspectRatio", "duration", "endImage", "mode"], ["prompt"]),
|
||||
videoPreset(330, "/generate/video/kling/kling-2.5", "Kling 2.5", ["startImage", "prompt", "aspectRatio", "duration", "endImage"], ["prompt"]),
|
||||
videoPreset(340, "/generate/video/kling/kling-2.6", "Kling 2.6", ["startImage", "prompt", "aspectRatio", "duration", "endImage", "generateAudio"], ["prompt"]),
|
||||
videoPreset(350, "/generate/video/kling/kling-o1", "Kling o1", ["startImage", "prompt", "aspectRatio", "duration", "endImage", "styleReferences", "elementReferences", "videoReference"], ["prompt"]),
|
||||
videoPreset(360, "/generate/video/minimax/hailuo", "Hailuo", ["startImage", "prompt", "expandPrompt", "cameraControl", "model", "subjects"], ["prompt"]),
|
||||
videoPreset(370, "/generate/video/minimax/hailuo-02", "Hailuo 02", ["startImage", "prompt", "expandPrompt", "endImage", "resolution", "duration"], ["prompt"]),
|
||||
videoPreset(380, "/generate/video/minimax/hailuo-2.3", "Hailuo 2.3", ["startImage", "prompt", "expandPrompt", "endImage", "resolution", "duration"], ["prompt"]),
|
||||
videoPreset(390, "/generate/video/minimax/hailuo-2.3-fast", "Hailuo 2.3 Fast", ["startImage", "prompt", "expandPrompt", "endImage", "resolution", "duration"], ["prompt"]),
|
||||
videoPreset(400, "/generate/video/alibaba/wan-2.1", "Wan 2.1", ["startImage", "prompt", "width", "height", "endImage", "aspectRatio", "frame_num", "styles"], ["prompt", "width", "height"]),
|
||||
videoPreset(410, "/generate/video/alibaba/wan-2.2", "Wan 2.2", ["startImage", "guide_scale", "prompt", "width", "height", "aspectRatio", "frame_num", "styles"], ["prompt", "width", "height"]),
|
||||
videoPreset(420, "/generate/video/alibaba/wan-2.5", "Wan 2.5", ["startImage", "prompt", "aspectRatio", "resolution", "enablePromptExpansion", "duration", "seed", "audio_url", "generateAudio"], ["prompt"]),
|
||||
videoPreset(430, "/generate/video/google/veo-2", "Veo 2", ["startImage", "prompt", "aspectRatio", "duration", "image_url"], ["prompt"]),
|
||||
videoPreset(440, "/generate/video/google/veo-3", "Veo 3", ["startImage", "prompt", "aspectRatio", "duration", "generateAudio", "resolution"], ["prompt"]),
|
||||
videoPreset(450, "/generate/video/google/veo-3-fast", "Veo 3 Fast", ["startImage", "prompt", "aspectRatio", "duration", "generateAudio", "resolution"], ["prompt"]),
|
||||
videoPreset(460, "/generate/video/google/veo-3.1", "Veo 3.1", ["startImage", "prompt", "aspectRatio", "duration", "generateAudio", "resolution", "endImage", "referenceImages"], ["prompt"]),
|
||||
videoPreset(470, "/generate/video/google/veo-3.1-fast", "Veo 3.1 Fast", ["startImage", "prompt", "aspectRatio", "duration", "generateAudio", "resolution", "endImage", "referenceImages"], ["prompt"]),
|
||||
videoPreset(480, "/generate/video/google/veo-3.1-lite", "Veo 3.1 Lite", ["startImage", "prompt", "aspectRatio", "duration", "resolution", "endImage"], ["prompt"]),
|
||||
videoPreset(490, "/generate/video/bytedance/seedance-1.0-lite", "Seedance Lite", ["startImage", "prompt", "aspectRatio", "endImage", "referenceImages", "duration", "resolution"], ["prompt"]),
|
||||
videoPreset(500, "/generate/video/bytedance/seedance-1.0-pro", "Seedance Pro", ["startImage", "prompt", "aspectRatio", "endImage", "referenceImages", "duration", "resolution"], ["prompt"]),
|
||||
videoPreset(510, "/generate/video/bytedance/seedance-1.0-pro-fast", "Seedance Pro Fast", ["startImage", "prompt", "aspectRatio", "duration", "resolution"], ["prompt"]),
|
||||
videoPreset(520, "/generate/video/bytedance/seedance-2", "Seedance 2.0", ["startImage", "prompt", "aspectRatio", "endImage", "referenceImages", "referenceVideos", "referenceAudios", "duration", "generateAudio", "resolution", "seed"], ["prompt"]),
|
||||
videoPreset(530, "/generate/video/bytedance/seedance-2-fast", "Seedance 2.0 Fast", ["startImage", "prompt", "aspectRatio", "endImage", "referenceImages", "referenceVideos", "referenceAudios", "duration", "generateAudio", "resolution", "seed"], ["prompt"]),
|
||||
videoPreset(540, "/generate/video/openai/sora-2", "Sora 2", ["startImage", "prompt", "duration", "aspectRatio"], ["prompt"]),
|
||||
videoPreset(550, "/generate/video/luma/ray-2", "Ray 2", ["startImage", "prompt", "aspectRatio", "loop", "width", "height"], ["prompt", "width", "height"]),
|
||||
videoPreset(560, "/generate/video/runway/gen-3", "Runway Gen-3", ["startImage", "prompt", "aspectRatio", "duration", "seed"], ["prompt"]),
|
||||
videoPreset(570, "/generate/video/runway/gen-4-video", "Runway Gen-4", ["startImage", "prompt", "aspectRatio", "duration", "seed"], ["prompt"]),
|
||||
videoPreset(580, "/generate/video/runway/gen-4.5", "Runway Gen-4.5", ["startImage", "prompt", "aspectRatio", "duration", "seed"], ["prompt"]),
|
||||
videoPreset(590, "/generate/video/runway/aleph", "Runway Aleph", ["startImage", "startVideo", "startVideoTrim", "endVideoTrim", "prompt", "init_video", "init_image_url", "references", "ratio", "seed", "contentModeration"], ["prompt", "init_video"]),
|
||||
];
|
||||
|
||||
export function getKreaModelPresetById(modelId: string) {
|
||||
|
||||
@ -318,11 +318,6 @@ function normalizeSeed(seed?: number) {
|
||||
return clamp(Math.round(Number(seed)), 0, 4294967295);
|
||||
}
|
||||
|
||||
function normalizeBatchSize(value?: number) {
|
||||
if (!Number.isFinite(Number(value))) return 1;
|
||||
return clamp(Math.round(Number(value)), 1, 4);
|
||||
}
|
||||
|
||||
function normalizeVideoDuration(duration: unknown, endpoint: string) {
|
||||
const value = Number.isFinite(Number(duration)) ? Math.round(Number(duration)) : 5;
|
||||
if (endpoint.includes("/bytedance/seedance-2")) {
|
||||
@ -387,9 +382,11 @@ function buildTaggedReferences(imageUrls: string[], tagPrefix = "ref", max = 3)
|
||||
|
||||
const KREA_SOURCE_IMAGE_FIELDS = new Set([
|
||||
"imageUrls",
|
||||
"image_urls",
|
||||
"imageUrl",
|
||||
"image_url",
|
||||
"referenceImages",
|
||||
"reference_images",
|
||||
"init_image_url",
|
||||
"references",
|
||||
]);
|
||||
@ -401,6 +398,19 @@ function hasKreaSourceImageField(fields: Iterable<string>) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasAnyKreaField(fields: Set<string>, aliases: string[]) {
|
||||
return aliases.some((field) => fields.has(field));
|
||||
}
|
||||
|
||||
function getPreferredKreaField(fields: Set<string>, aliases: string[]) {
|
||||
for (const field of aliases) {
|
||||
if (fields.has(field)) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildReferencePreservingPrompt(params: {
|
||||
endpoint: string;
|
||||
kind: KreaModelKind;
|
||||
@ -449,19 +459,26 @@ function summarizeKreaReferenceFields(body: Record<string, any>) {
|
||||
|
||||
for (const field of [
|
||||
"imageUrls",
|
||||
"image_urls",
|
||||
"imageUrl",
|
||||
"image_url",
|
||||
"styleImages",
|
||||
"style_images",
|
||||
"imageStyleRefs",
|
||||
"image_style_references",
|
||||
"characterReferenceImages",
|
||||
"character_reference_images",
|
||||
"referenceImages",
|
||||
"reference_images",
|
||||
"startImage",
|
||||
"endImage",
|
||||
"init_image_url",
|
||||
"references",
|
||||
"subjects",
|
||||
"styleReferences",
|
||||
"style_references",
|
||||
"elementReferences",
|
||||
"element_references",
|
||||
]) {
|
||||
const value = body[field];
|
||||
if (Array.isArray(value)) {
|
||||
@ -841,6 +858,7 @@ function buildKreaBody(params: {
|
||||
const required = new Set(requiredFields);
|
||||
const body: Record<string, any> = {};
|
||||
const isVideo = kind === "video";
|
||||
const isImageLike = !isVideo;
|
||||
const videoMode =
|
||||
isVideo
|
||||
? videoParams?.mode ||
|
||||
@ -864,7 +882,6 @@ function buildKreaBody(params: {
|
||||
const videoDimensions = resolveVideoDimensions(ratio, videoResolution);
|
||||
|
||||
if (fields.has("prompt")) body.prompt = prompt;
|
||||
if (fields.has("batchSize")) body.batchSize = normalizeBatchSize(imageParams?.imageCount);
|
||||
|
||||
if (
|
||||
fields.has("width") &&
|
||||
@ -878,13 +895,20 @@ function buildKreaBody(params: {
|
||||
) {
|
||||
body.height = isVideo ? videoDimensions.height : imageDimensions.height;
|
||||
}
|
||||
if (fields.has("aspectRatio") && shouldInferImageGeometryFromReference && inferredReferenceAspectRatio) {
|
||||
body.aspectRatio = inferredReferenceAspectRatio;
|
||||
const aspectRatioField = getPreferredKreaField(
|
||||
fields,
|
||||
isImageLike ? ["aspect_ratio", "aspectRatio"] : ["aspectRatio", "aspect_ratio"]
|
||||
);
|
||||
if (aspectRatioField && shouldInferImageGeometryFromReference && inferredReferenceAspectRatio) {
|
||||
body[aspectRatioField] = inferredReferenceAspectRatio;
|
||||
} else if (
|
||||
fields.has("aspectRatio") &&
|
||||
(isVideo || !shouldInferImageGeometryFromReference || required.has("aspectRatio"))
|
||||
aspectRatioField &&
|
||||
(isVideo ||
|
||||
!shouldInferImageGeometryFromReference ||
|
||||
required.has("aspectRatio") ||
|
||||
required.has("aspect_ratio"))
|
||||
) {
|
||||
body.aspectRatio = ratio === "auto" ? "1:1" : ratio;
|
||||
body[aspectRatioField] = ratio === "auto" ? "1:1" : ratio;
|
||||
}
|
||||
if (fields.has("ratio")) body.ratio = ratio === "auto" ? "16:9" : ratio;
|
||||
if (fields.has("resolution")) {
|
||||
@ -896,8 +920,12 @@ function buildKreaBody(params: {
|
||||
if (fields.has("negative_prompt") && imageParams?.negativePrompt) {
|
||||
body.negative_prompt = imageParams.negativePrompt;
|
||||
}
|
||||
if (fields.has("skipPromptExpansion") && typeof imageParams?.promptExtend === "boolean") {
|
||||
body.skipPromptExpansion = !imageParams.promptExtend;
|
||||
const skipPromptExpansionField = getPreferredKreaField(fields, [
|
||||
"skip_prompt_expansion",
|
||||
"skipPromptExpansion",
|
||||
]);
|
||||
if (skipPromptExpansionField && typeof imageParams?.promptExtend === "boolean") {
|
||||
body[skipPromptExpansionField] = !imageParams.promptExtend;
|
||||
}
|
||||
if (fields.has("enablePromptExpansion")) {
|
||||
body.enablePromptExpansion = true;
|
||||
@ -908,8 +936,12 @@ function buildKreaBody(params: {
|
||||
if (fields.has("duration")) {
|
||||
body.duration = normalizeVideoDuration(videoParams?.duration, endpoint);
|
||||
}
|
||||
if (fields.has("generateAudio") && typeof videoParams?.generateAudio === "boolean") {
|
||||
body.generateAudio = videoParams.generateAudio;
|
||||
const generateAudioField = getPreferredKreaField(fields, [
|
||||
"generate_audio",
|
||||
"generateAudio",
|
||||
]);
|
||||
if (generateAudioField && typeof videoParams?.generateAudio === "boolean") {
|
||||
body[generateAudioField] = videoParams.generateAudio;
|
||||
}
|
||||
if (fields.has("loop")) {
|
||||
body.loop = false;
|
||||
@ -924,31 +956,65 @@ function buildKreaBody(params: {
|
||||
if (fields.has("output_format")) body.output_format = "jpg";
|
||||
if (fields.has("backgroundImage")) body.backgroundImage = "auto";
|
||||
|
||||
if (fields.has("imageUrls") && imageUrls.length > 0) {
|
||||
body.imageUrls = imageUrls.slice(0, 8);
|
||||
}
|
||||
if (fields.has("imageUrl") && imageUrls[0]) {
|
||||
body.imageUrl = imageUrls[0];
|
||||
}
|
||||
if (fields.has("image_url") && imageUrls[0]) {
|
||||
body.image_url = imageUrls[0];
|
||||
}
|
||||
if (fields.has("styleImages") && imageUrls.length > 0 && !body.imageUrls && !body.imageUrl) {
|
||||
body.styleImages = buildStyleImages(imageUrls.slice(0, 4));
|
||||
}
|
||||
if (fields.has("imageStyleRefs") && imageUrls.length > 1) {
|
||||
body.imageStyleRefs = buildStyleImages(imageUrls.slice(1, 5));
|
||||
}
|
||||
if (fields.has("characterReferenceImages") && imageUrls.length > 0) {
|
||||
body.characterReferenceImages = imageUrls.slice(0, 4);
|
||||
const imageUrlsField = getPreferredKreaField(
|
||||
fields,
|
||||
isImageLike ? ["image_urls", "imageUrls"] : ["imageUrls", "image_urls"]
|
||||
);
|
||||
if (imageUrlsField && imageUrls.length > 0) {
|
||||
body[imageUrlsField] = imageUrls.slice(0, 8);
|
||||
}
|
||||
|
||||
const imageUrlField = getPreferredKreaField(
|
||||
fields,
|
||||
isImageLike ? ["image_url", "imageUrl"] : ["imageUrl", "image_url"]
|
||||
);
|
||||
if (imageUrlField && imageUrls[0]) {
|
||||
body[imageUrlField] = imageUrls[0];
|
||||
}
|
||||
|
||||
const styleImagesField = getPreferredKreaField(
|
||||
fields,
|
||||
isImageLike ? ["style_images", "styleImages"] : ["styleImages", "style_images"]
|
||||
);
|
||||
if (
|
||||
fields.has("referenceImages") &&
|
||||
styleImagesField &&
|
||||
imageUrls.length > 0 &&
|
||||
!hasAnyKreaField(new Set(Object.keys(body)), [
|
||||
"image_urls",
|
||||
"imageUrls",
|
||||
"image_url",
|
||||
"imageUrl",
|
||||
])
|
||||
) {
|
||||
body[styleImagesField] = buildStyleImages(imageUrls.slice(0, 4));
|
||||
}
|
||||
|
||||
const imageStyleReferencesField = getPreferredKreaField(fields, [
|
||||
"image_style_references",
|
||||
"imageStyleRefs",
|
||||
]);
|
||||
if (imageStyleReferencesField && imageUrls.length > 1) {
|
||||
body[imageStyleReferencesField] = buildStyleImages(imageUrls.slice(1, 5));
|
||||
}
|
||||
|
||||
const characterReferenceImagesField = getPreferredKreaField(fields, [
|
||||
"character_reference_images",
|
||||
"characterReferenceImages",
|
||||
]);
|
||||
if (characterReferenceImagesField && imageUrls.length > 0) {
|
||||
body[characterReferenceImagesField] = imageUrls.slice(0, 4);
|
||||
}
|
||||
|
||||
const referenceImagesField = getPreferredKreaField(fields, [
|
||||
"reference_images",
|
||||
"referenceImages",
|
||||
]);
|
||||
if (
|
||||
referenceImagesField &&
|
||||
imageUrls.length > 0 &&
|
||||
(!isVideo || videoMode === "reference-images")
|
||||
) {
|
||||
body.referenceImages = endpoint.includes("/image/runway/gen-4-image")
|
||||
body[referenceImagesField] = endpoint.includes("/image/runway/gen-4-image")
|
||||
? buildTaggedReferences(imageUrls, "ref", 3)
|
||||
: imageUrls.slice(0, endpoint.includes("/seedance-2") ? 9 : 4);
|
||||
}
|
||||
@ -970,27 +1036,50 @@ function buildKreaBody(params: {
|
||||
if (fields.has("subjects") && imageUrls[0] && videoParams?.mode === "reference-images") {
|
||||
body.subjects = [{ type: "character", image: [imageUrls[0]] }];
|
||||
}
|
||||
if (fields.has("styleReferences") && imageUrls.length > 0) {
|
||||
body.styleReferences = buildTaggedReferences(imageUrls, "style", 3);
|
||||
const styleReferencesField = getPreferredKreaField(fields, [
|
||||
"style_references",
|
||||
"styleReferences",
|
||||
]);
|
||||
if (styleReferencesField && imageUrls.length > 0) {
|
||||
body[styleReferencesField] = buildTaggedReferences(imageUrls, "style", 3);
|
||||
}
|
||||
if (fields.has("elementReferences") && imageUrls.length > 0) {
|
||||
body.elementReferences = [
|
||||
|
||||
const elementReferencesField = getPreferredKreaField(fields, [
|
||||
"element_references",
|
||||
"elementReferences",
|
||||
]);
|
||||
if (elementReferencesField && imageUrls.length > 0) {
|
||||
body[elementReferencesField] = [
|
||||
{
|
||||
tag: "element_1",
|
||||
images: imageUrls.slice(0, 3),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (fields.has("videoReference") && videoUrls[0]) {
|
||||
body.videoReference = { url: videoUrls[0], tag: "video_1" };
|
||||
|
||||
const videoReferenceField = getPreferredKreaField(fields, [
|
||||
"video_reference",
|
||||
"videoReference",
|
||||
]);
|
||||
if (videoReferenceField && videoUrls[0]) {
|
||||
body[videoReferenceField] = { url: videoUrls[0], tag: "video_1" };
|
||||
}
|
||||
if (fields.has("startVideo") && videoUrls[0]) body.startVideo = videoUrls[0];
|
||||
if (fields.has("init_video") && videoUrls[0]) body.init_video = videoUrls[0];
|
||||
if (fields.has("referenceVideos") && videoUrls.length > 0) {
|
||||
body.referenceVideos = videoUrls.slice(0, 3);
|
||||
|
||||
const referenceVideosField = getPreferredKreaField(fields, [
|
||||
"reference_videos",
|
||||
"referenceVideos",
|
||||
]);
|
||||
if (referenceVideosField && videoUrls.length > 0) {
|
||||
body[referenceVideosField] = videoUrls.slice(0, 3);
|
||||
}
|
||||
|
||||
if (required.has("referenceImages") && !body.referenceImages) {
|
||||
if (
|
||||
(required.has("referenceImages") || required.has("reference_images")) &&
|
||||
!body.referenceImages &&
|
||||
!body.reference_images
|
||||
) {
|
||||
throw new Error("该 Krea 模型需要至少 1 张参考图");
|
||||
}
|
||||
if ((required.has("imageUrl") || required.has("image_url")) && !body.imageUrl && !body.image_url) {
|
||||
@ -1038,7 +1127,7 @@ export function createKrea({ apiKey, baseURL }: KreaConfig) {
|
||||
preset?.requestFields ||
|
||||
(kind === "video"
|
||||
? ["prompt", "aspectRatio", "duration", "startImage", "endImage", "referenceImages"]
|
||||
: ["prompt", "width", "height", "imageUrls", "imageUrl", "batchSize"]);
|
||||
: ["prompt", "width", "height", "image_urls", "image_url"]);
|
||||
const requiredFields = preset?.requiredFields || ["prompt"];
|
||||
const requestedImageAspectRatio = normalizeText(options.imageParams?.aspectRatio).toLowerCase();
|
||||
const referenceAspectRatio =
|
||||
|
||||
23
lib/db/migrations/0085_add_system_setting_snapshots.sql
Normal file
23
lib/db/migrations/0085_add_system_setting_snapshots.sql
Normal file
@ -0,0 +1,23 @@
|
||||
CREATE TABLE IF NOT EXISTS "SystemSettingSnapshot" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"scope" varchar(64) NOT NULL,
|
||||
"label" varchar(128),
|
||||
"summary" jsonb,
|
||||
"data" jsonb NOT NULL,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"createdBy" uuid
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TABLE "SystemSettingSnapshot"
|
||||
ADD CONSTRAINT "SystemSettingSnapshot_createdBy_User_id_fk"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "public"."User"("id")
|
||||
ON DELETE NO ACTION
|
||||
ON UPDATE NO ACTION;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "SystemSettingSnapshot_scope_createdAt_idx"
|
||||
ON "SystemSettingSnapshot" ("scope", "createdAt" DESC);
|
||||
@ -498,6 +498,13 @@
|
||||
"when": 1779797137842,
|
||||
"tag": "0084_optimize_featured_home_query",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 71,
|
||||
"version": "7",
|
||||
"when": 1780075200000,
|
||||
"tag": "0085_add_system_setting_snapshots",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -260,6 +260,18 @@ export const systemSettingHistory = pgTable("SystemSettingHistory", {
|
||||
|
||||
export type SystemSettingHistory = InferSelectModel<typeof systemSettingHistory>;
|
||||
|
||||
export const systemSettingSnapshot = pgTable("SystemSettingSnapshot", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
scope: varchar("scope", { length: 64 }).notNull(),
|
||||
label: varchar("label", { length: 128 }),
|
||||
summary: jsonb("summary"),
|
||||
data: jsonb("data").notNull(),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
createdBy: uuid("createdBy").references(() => user.id),
|
||||
});
|
||||
|
||||
export type SystemSettingSnapshot = InferSelectModel<typeof systemSettingSnapshot>;
|
||||
|
||||
export const dingTalkNotificationTask = pgTable("DingTalkNotificationTask", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
title: varchar("title", { length: 100 }).notNull(),
|
||||
|
||||
132
lib/system-settings/snapshot.ts
Normal file
132
lib/system-settings/snapshot.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import {
|
||||
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||
} from "@/lib/agents/preset-library-config";
|
||||
import {
|
||||
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
||||
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||
import {
|
||||
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
|
||||
} from "@/lib/agents/workflow-brand-model-config";
|
||||
import {
|
||||
AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
|
||||
} from "@/lib/agents/workflow-goods-info-config";
|
||||
import {
|
||||
AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
|
||||
} from "@/lib/agents/workflow-material-config";
|
||||
import {
|
||||
AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
|
||||
} from "@/lib/agents/workflow-showcase-config";
|
||||
import {
|
||||
AGENT_WORKFLOW_UI_CONFIG_KEY,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
|
||||
export const AGENT_WORKFLOW_SNAPSHOT_SCOPE = "agent-workflow";
|
||||
|
||||
export const SYSTEM_SETTING_SNAPSHOT_SCOPES = {
|
||||
[AGENT_WORKFLOW_SNAPSHOT_SCOPE]: [
|
||||
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_UI_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
|
||||
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
||||
],
|
||||
} 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(
|
||||
scope: SystemSettingSnapshotScope
|
||||
) {
|
||||
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>,
|
||||
siteSettingsValues: Record<string, unknown> = {}
|
||||
) {
|
||||
const keys = getSystemSettingSnapshotScopeKeys(scope);
|
||||
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 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,
|
||||
};
|
||||
}
|
||||
@ -43,6 +43,8 @@
|
||||
"db:pull": "drizzle-kit pull",
|
||||
"db:check": "drizzle-kit check",
|
||||
"db:up": "drizzle-kit up",
|
||||
"db:snapshot:system-settings": "pnpm exec tsx scripts/system-settings/create-snapshot.ts",
|
||||
"db:restore:system-settings-snapshot": "pnpm exec tsx scripts/system-settings/restore-snapshot.ts",
|
||||
"poc:nanobanana": "pnpm exec tsx scripts/nanobanana-browser-poc.ts",
|
||||
"test": "export PLAYWRIGHT=True && pnpm exec playwright test",
|
||||
"storage:configure-upload-cors": "pnpm exec tsx scripts/storage/configure-direct-upload-cors.ts",
|
||||
|
||||
93
scripts/system-settings/create-snapshot.ts
Normal file
93
scripts/system-settings/create-snapshot.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { config as loadEnv } from "dotenv";
|
||||
import { inArray } from "drizzle-orm";
|
||||
|
||||
loadEnv({ path: ".env.local" });
|
||||
|
||||
function readArg(name: string) {
|
||||
const prefix = `${name}=`;
|
||||
const match = process.argv.find((arg) => arg.startsWith(prefix));
|
||||
return match ? match.slice(prefix.length) : null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const scopeArg = readArg("--scope");
|
||||
const label = readArg("--label");
|
||||
|
||||
if (!scopeArg) {
|
||||
throw new Error("Usage: pnpm tsx scripts/system-settings/create-snapshot.ts --scope=<scope> [--label=<label>]");
|
||||
}
|
||||
|
||||
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 { siteSettings, systemSettingSnapshot, systemSettings } = schemaModule;
|
||||
const {
|
||||
createSystemSettingSnapshotData,
|
||||
getSystemSettingSnapshotScopeKeys,
|
||||
summarizeSystemSettingSnapshotData,
|
||||
} = snapshotModule;
|
||||
|
||||
const keys = getSystemSettingSnapshotScopeKeys(
|
||||
scopeArg as keyof typeof snapshotModule.SYSTEM_SETTING_SNAPSHOT_SCOPES
|
||||
);
|
||||
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);
|
||||
const snapshotData = createSystemSettingSnapshotData(
|
||||
scopeArg as keyof typeof snapshotModule.SYSTEM_SETTING_SNAPSHOT_SCOPES,
|
||||
valueByKey,
|
||||
{
|
||||
agentWorkflowDefaultModelId:
|
||||
String(currentSiteSettings?.agentWorkflowDefaultModelId || "").trim() || null,
|
||||
}
|
||||
);
|
||||
|
||||
const [snapshot] = await db
|
||||
.insert(systemSettingSnapshot)
|
||||
.values({
|
||||
scope: scopeArg,
|
||||
label: label || `manual-${scopeArg}-snapshot`,
|
||||
summary: summarizeSystemSettingSnapshotData(snapshotData),
|
||||
data: snapshotData,
|
||||
createdAt: new Date(),
|
||||
createdBy: null,
|
||||
})
|
||||
.returning({
|
||||
id: systemSettingSnapshot.id,
|
||||
scope: systemSettingSnapshot.scope,
|
||||
label: systemSettingSnapshot.label,
|
||||
createdAt: systemSettingSnapshot.createdAt,
|
||||
});
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
snapshotId: snapshot?.id ?? null,
|
||||
scope: snapshot?.scope ?? scopeArg,
|
||||
label: snapshot?.label ?? label ?? null,
|
||||
createdAt: snapshot?.createdAt ?? null,
|
||||
keyCount: keys.length,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
130
scripts/system-settings/restore-snapshot.ts
Normal file
130
scripts/system-settings/restore-snapshot.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { config as loadEnv } from "dotenv";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
loadEnv({ path: ".env.local" });
|
||||
|
||||
function readArg(name: string) {
|
||||
const prefix = `${name}=`;
|
||||
const match = process.argv.find((arg) => arg.startsWith(prefix));
|
||||
return match ? match.slice(prefix.length) : null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const snapshotId = readArg("--snapshot-id");
|
||||
const reason = readArg("--reason") || "manual-snapshot-restore";
|
||||
|
||||
if (!snapshotId) {
|
||||
throw new Error(
|
||||
"Usage: pnpm tsx scripts/system-settings/restore-snapshot.ts --snapshot-id=<id> [--reason=<reason>]"
|
||||
);
|
||||
}
|
||||
|
||||
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 { siteSettings, systemSettingHistory, systemSettingSnapshot, systemSettings } =
|
||||
schemaModule;
|
||||
const { readSystemSettingSnapshotData } = snapshotModule;
|
||||
|
||||
const [snapshot] = await db
|
||||
.select({
|
||||
id: systemSettingSnapshot.id,
|
||||
scope: systemSettingSnapshot.scope,
|
||||
data: systemSettingSnapshot.data,
|
||||
})
|
||||
.from(systemSettingSnapshot)
|
||||
.where(eq(systemSettingSnapshot.id, snapshotId))
|
||||
.limit(1);
|
||||
|
||||
if (!snapshot) {
|
||||
throw new Error(`Snapshot not found: ${snapshotId}`);
|
||||
}
|
||||
|
||||
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) {
|
||||
const [existing] = await tx
|
||||
.select({ value: systemSettings.value })
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, key))
|
||||
.limit(1);
|
||||
|
||||
const nextValue = parsedSnapshot.systemSettings[key] ?? null;
|
||||
|
||||
await tx.insert(systemSettingHistory).values({
|
||||
key,
|
||||
previousValue: existing?.value ?? null,
|
||||
nextValue,
|
||||
reason,
|
||||
changedAt: new Date(),
|
||||
changedBy: null,
|
||||
});
|
||||
|
||||
await tx
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key,
|
||||
value: nextValue,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: null,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: nextValue,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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(
|
||||
JSON.stringify(
|
||||
{
|
||||
snapshotId,
|
||||
scope: snapshot.scope,
|
||||
restoredKeyCount: keys.length,
|
||||
reason,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user