fix: protect workflow settings from overwrites
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-16 19:02:53 +08:00
parent c5a4b4add2
commit 8c8d2c07f5
6 changed files with 732 additions and 294 deletions

View File

@ -2737,8 +2737,13 @@ export function AdminSettingsClient() {
siteSettings.pointsSystemConfig
);
const payload = {
const siteSettingsPayload: Partial<SiteSettings> = {
...siteSettings,
};
delete siteSettingsPayload.agentWorkflowUiConfig;
const payload = {
...siteSettingsPayload,
homeAnimationConfig: {
enabled: homeAnimationConfig.enabled,
fileName: homeAnimationConfig.fileName.trim(),
@ -2762,7 +2767,10 @@ export function AdminSettingsClient() {
homeAnimationConfig: _ignoredHomeAnimationConfig,
...nextSiteSettings
} = payload;
setSiteSettings(nextSiteSettings);
setSiteSettings((prev) => ({
...prev,
...nextSiteSettings,
}));
setHomeAnimationConfig((prev) => ({
...prev,
htmlContent: normalizedHomeAnimationHtml,

View File

@ -144,6 +144,7 @@ const WORKFLOW_OPTION_LEVEL_LABELS = CHINESE_LEVEL_NUMERALS.map((item) => `${ite
const PRESET_LIBRARY_LEVEL_LABELS = ["一级分类", "二级分类", "三级分类"];
const WORKFLOW_STEP_DRAG_DATA_TYPE = "application/x-agent-workflow-step";
const WORKFLOW_SETTINGS_DRAFT_STORAGE_KEY = "agent-workflow-settings-draft:v1";
const WORKFLOW_SETTINGS_STALE_DRAFT_STORAGE_KEY = "agent-workflow-settings-stale-draft:v1";
const WORKFLOW_SETTINGS_DRAFT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
type WorkflowStepDropPosition = "before" | "after" | "inside";
@ -165,11 +166,74 @@ interface AgentWorkflowSettingsDraftState {
}
interface AgentWorkflowSettingsDraft {
version: 1;
version: 1 | 2;
updatedAt: number;
baseSignature?: string;
baseSignatures?: Partial<Record<keyof AgentWorkflowSettingsPayload, string>>;
state: AgentWorkflowSettingsDraftState;
}
function stableStringifyWorkflowDraftValue(value: unknown): string {
if (value === null || typeof value !== "object") {
return JSON.stringify(value) ?? "null";
}
if (Array.isArray(value)) {
return `[${value.map((item) => stableStringifyWorkflowDraftValue(item)).join(",")}]`;
}
const entries = Object.entries(value as Record<string, unknown>)
.filter(([, item]) => item !== undefined)
.sort(([left], [right]) => left.localeCompare(right));
return `{${entries
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringifyWorkflowDraftValue(item)}`)
.join(",")}}`;
}
function createAgentWorkflowValueSignature(value: unknown) {
const serialized = stableStringifyWorkflowDraftValue(value);
let hashA = 0xdeadbeef ^ serialized.length;
let hashB = 0x41c6ce57 ^ serialized.length;
for (let index = 0; index < serialized.length; index += 1) {
const code = serialized.charCodeAt(index);
hashA = Math.imul(hashA ^ code, 2654435761);
hashB = Math.imul(hashB ^ code, 1597334677);
}
hashA =
Math.imul(hashA ^ (hashA >>> 16), 2246822507) ^
Math.imul(hashB ^ (hashB >>> 13), 3266489909);
hashB =
Math.imul(hashB ^ (hashB >>> 16), 2246822507) ^
Math.imul(hashA ^ (hashA >>> 13), 3266489909);
return `${serialized.length}:${(hashB >>> 0).toString(16)}${(hashA >>> 0).toString(16)}`;
}
function createAgentWorkflowSettingsStateSignature(state: AgentWorkflowSettingsDraftState) {
return createAgentWorkflowValueSignature(state);
}
function createAgentWorkflowSettingsSectionSignatures(state: AgentWorkflowSettingsDraftState) {
return {
agentPresetLibraryConfig: createAgentWorkflowValueSignature(state.presetLibraryConfig),
agentWorkflowUiConfig: createAgentWorkflowValueSignature({
...state.config,
steps: normalizeAgentWorkflowStepOrdering(state.config.steps),
}),
agentWorkflowMaterialApiConfig: createAgentWorkflowValueSignature(state.materialApiConfig),
agentWorkflowGoodsInfoApiConfig: createAgentWorkflowValueSignature(state.goodsInfoApiConfig),
agentWorkflowBrandModelConfig: createAgentWorkflowValueSignature(state.brandModelConfig),
agentWorkflowShowcaseConfig: createAgentWorkflowValueSignature(state.showcaseConfig),
agentPsdTextColorConfig: createAgentWorkflowValueSignature(state.psdTextColorConfig),
agentPsdTemplateSecondaryCategoryConfig: createAgentWorkflowValueSignature(
state.psdTemplateSecondaryCategoryConfig
),
} satisfies Record<keyof AgentWorkflowSettingsPayload, string>;
}
function getWorkflowLevelLabel(depth: number, kind: "模块" | "卡片") {
const labels =
kind === "模块" ? WORKFLOW_LEVEL_LABELS : WORKFLOW_OPTION_LEVEL_LABELS;
@ -634,12 +698,19 @@ function readAgentWorkflowSettingsDraft(): AgentWorkflowSettingsDraft | null {
}
const parsed = JSON.parse(rawDraft) as Partial<AgentWorkflowSettingsDraft>;
const isLegacyDraft = parsed?.version === 1;
const isVersionedDraft =
parsed?.version === 2 &&
typeof parsed.baseSignature === "string" &&
parsed.baseSignature.length > 0;
if (
parsed?.version !== 1 ||
(!isLegacyDraft && !isVersionedDraft) ||
typeof parsed.updatedAt !== "number" ||
!parsed.state ||
typeof parsed.state !== "object"
) {
removeAgentWorkflowSettingsDraft();
return null;
}
@ -661,6 +732,46 @@ function writeAgentWorkflowSettingsDraft(draft: AgentWorkflowSettingsDraft) {
window.localStorage.setItem(WORKFLOW_SETTINGS_DRAFT_STORAGE_KEY, JSON.stringify(draft));
}
function preserveAgentWorkflowSettingsDraftForRecovery(draft: AgentWorkflowSettingsDraft) {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem(
WORKFLOW_SETTINGS_STALE_DRAFT_STORAGE_KEY,
JSON.stringify({
...draft,
preservedAt: Date.now(),
})
);
}
function readPreservedAgentWorkflowSettingsDraftForRecovery(): AgentWorkflowSettingsDraft | null {
if (typeof window === "undefined") {
return null;
}
try {
const rawDraft = window.localStorage.getItem(WORKFLOW_SETTINGS_STALE_DRAFT_STORAGE_KEY);
if (!rawDraft) {
return null;
}
const parsed = JSON.parse(rawDraft) as Partial<AgentWorkflowSettingsDraft>;
if (
(parsed?.version !== 1 && parsed?.version !== 2) ||
typeof parsed.updatedAt !== "number" ||
!parsed.state ||
typeof parsed.state !== "object"
) {
return null;
}
return parsed as AgentWorkflowSettingsDraft;
} catch {
return null;
}
}
function removeAgentWorkflowSettingsDraft() {
if (typeof window === "undefined") {
return;
@ -1115,20 +1226,99 @@ interface AgentWorkflowSettingsClientProps {
backLabel?: string;
}
interface AgentWorkflowSettingsPayload {
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
agentWorkflowUiConfig?: AgentWorkflowUiConfig;
agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig;
agentWorkflowGoodsInfoApiConfig?: AgentWorkflowGoodsInfoApiConfig;
agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig;
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
agentPsdTextColorConfig?: AgentPsdTextColorConfig;
agentPsdTemplateSecondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
}
function createAgentWorkflowSettingsStateFromPayload(
payload?: AgentWorkflowSettingsPayload | null
): AgentWorkflowSettingsDraftState {
return {
presetLibraryConfig: normalizeAgentPresetLibraryConfig(payload?.agentPresetLibraryConfig),
config: normalizeConfigOrdering(normalizeAgentWorkflowUiConfig(payload?.agentWorkflowUiConfig)),
materialApiConfig: normalizeAgentWorkflowMaterialApiConfig(
payload?.agentWorkflowMaterialApiConfig
),
goodsInfoApiConfig: normalizeAgentWorkflowGoodsInfoApiConfig(
payload?.agentWorkflowGoodsInfoApiConfig
),
showcaseConfig: normalizeAgentWorkflowShowcaseConfig(payload?.agentWorkflowShowcaseConfig),
psdTextColorConfig: normalizeAgentPsdTextColorConfig(payload?.agentPsdTextColorConfig),
psdTemplateSecondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig(
payload?.agentPsdTemplateSecondaryCategoryConfig
),
brandModelConfig: normalizeAgentWorkflowBrandModelConfig(
payload?.agentWorkflowBrandModelConfig
),
};
}
type AgentWorkflowSettingsField = keyof AgentWorkflowSettingsPayload;
const AGENT_WORKFLOW_SETTINGS_FIELD_LABELS: Record<AgentWorkflowSettingsField, string> = {
agentPresetLibraryConfig: "预设库",
agentWorkflowUiConfig: "工作流步骤",
agentWorkflowMaterialApiConfig: "素材搜索接口",
agentWorkflowGoodsInfoApiConfig: "商品信息接口",
agentWorkflowBrandModelConfig: "品牌模特",
agentWorkflowShowcaseConfig: "页面展示",
agentPsdTextColorConfig: "PSD 文字颜色",
agentPsdTemplateSecondaryCategoryConfig: "PSD 二级分类",
};
function getAgentWorkflowSettingsPayloadEntries(state: AgentWorkflowSettingsDraftState) {
return [
{
field: "agentPresetLibraryConfig",
value: state.presetLibraryConfig,
},
{
field: "agentWorkflowUiConfig",
value: state.config,
},
{
field: "agentWorkflowMaterialApiConfig",
value: state.materialApiConfig,
},
{
field: "agentWorkflowGoodsInfoApiConfig",
value: state.goodsInfoApiConfig,
},
{
field: "agentWorkflowBrandModelConfig",
value: state.brandModelConfig,
},
{
field: "agentWorkflowShowcaseConfig",
value: state.showcaseConfig,
},
{
field: "agentPsdTextColorConfig",
value: state.psdTextColorConfig,
},
{
field: "agentPsdTemplateSecondaryCategoryConfig",
value: state.psdTemplateSecondaryCategoryConfig,
},
] satisfies Array<{ field: AgentWorkflowSettingsField; value: unknown }>;
}
export function AgentWorkflowSettingsClient({
backHref = "/settings",
backLabel = "返回设置",
}: AgentWorkflowSettingsClientProps = {}) {
const { data, isLoading, mutate } = useSWR<{
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
agentWorkflowUiConfig?: AgentWorkflowUiConfig;
agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig;
agentWorkflowGoodsInfoApiConfig?: AgentWorkflowGoodsInfoApiConfig;
agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig;
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
agentPsdTextColorConfig?: AgentPsdTextColorConfig;
agentPsdTemplateSecondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
}>("/api/site-settings", fetcher);
const { data, isLoading, mutate } = useSWR<AgentWorkflowSettingsPayload>(
"/api/site-settings",
fetcher,
{ refreshInterval: 60_000 }
);
const [presetLibraryConfig, setPresetLibraryConfig] = useState<AgentPresetLibraryConfig>(
defaultAgentPresetLibraryConfig
);
@ -1178,11 +1368,17 @@ 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 fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
const brandImportInputRef = useRef<HTMLInputElement | null>(null);
const hasAppliedInitialSettingsRef = useRef(false);
const hasHydratedSettingsRef = useRef(false);
const isApplyingSettingsRef = useRef(false);
const serverDraftBaseSignatureRef = useRef<string | null>(null);
const serverDraftBaseSignaturesRef = useRef<
Record<keyof AgentWorkflowSettingsPayload, string> | null
>(null);
const hasWarnedRemoteWorkflowUpdateRef = useRef(false);
useEffect(() => {
if (!data) return;
@ -1192,24 +1388,22 @@ export function AgentWorkflowSettingsClient({
hasHydratedSettingsRef.current = false;
isApplyingSettingsRef.current = true;
const serverState: AgentWorkflowSettingsDraftState = {
presetLibraryConfig: normalizeAgentPresetLibraryConfig(data.agentPresetLibraryConfig),
config: normalizeConfigOrdering(normalizeAgentWorkflowUiConfig(data.agentWorkflowUiConfig)),
materialApiConfig: normalizeAgentWorkflowMaterialApiConfig(data.agentWorkflowMaterialApiConfig),
goodsInfoApiConfig: normalizeAgentWorkflowGoodsInfoApiConfig(
data.agentWorkflowGoodsInfoApiConfig
),
showcaseConfig: normalizeAgentWorkflowShowcaseConfig(data.agentWorkflowShowcaseConfig),
psdTextColorConfig: normalizeAgentPsdTextColorConfig(data.agentPsdTextColorConfig),
psdTemplateSecondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig(
data.agentPsdTemplateSecondaryCategoryConfig
),
brandModelConfig: normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig),
};
const serverState = createAgentWorkflowSettingsStateFromPayload(data);
const serverSignature = createAgentWorkflowSettingsStateSignature(serverState);
const serverSignatures = createAgentWorkflowSettingsSectionSignatures(serverState);
serverDraftBaseSignatureRef.current = serverSignature;
serverDraftBaseSignaturesRef.current = serverSignatures;
hasWarnedRemoteWorkflowUpdateRef.current = false;
const draft = readAgentWorkflowSettingsDraft();
const nextState: AgentWorkflowSettingsDraftState = draft
? {
let restoredDraft: AgentWorkflowSettingsDraft | null = null;
let nextState: AgentWorkflowSettingsDraftState = serverState;
if (draft) {
if (draft.version === 2 && draft.baseSignature === serverSignature) {
restoredDraft = draft;
nextState = {
presetLibraryConfig: normalizeAgentPresetLibraryConfig(draft.state.presetLibraryConfig),
config: normalizeConfigOrdering(normalizeAgentWorkflowUiConfig(draft.state.config)),
materialApiConfig: normalizeAgentWorkflowMaterialApiConfig(
@ -1224,8 +1418,18 @@ export function AgentWorkflowSettingsClient({
draft.state.psdTemplateSecondaryCategoryConfig
),
brandModelConfig: normalizeAgentWorkflowBrandModelConfig(draft.state.brandModelConfig),
}
: serverState;
};
} else {
preserveAgentWorkflowSettingsDraftForRecovery(draft);
setRecoverableDraftUpdatedAt(draft.updatedAt);
removeAgentWorkflowSettingsDraft();
toast.warning(
draft.version === 1
? "检测到旧版本地草稿,已转存为恢复副本,不会自动覆盖服务器配置"
: "服务器配置已有更新,已转存旧草稿为恢复副本,避免覆盖其他管理员保存的数据"
);
}
}
setPresetLibraryConfig(nextState.presetLibraryConfig);
setConfig(nextState.config);
@ -1241,10 +1445,18 @@ export function AgentWorkflowSettingsClient({
setExpandedSteps(new Set([nextState.config.steps[0].key]));
}
if (draft) {
setAutoDraftUpdatedAt(draft.updatedAt);
if (restoredDraft) {
setAutoDraftUpdatedAt(restoredDraft.updatedAt);
setAutoDraftStatus("restored");
toast.success("已恢复本地自动保存草稿,确认无误后点击保存配置写入服务器");
} else {
setAutoDraftUpdatedAt(null);
setAutoDraftStatus("idle");
}
const preservedDraft = readPreservedAgentWorkflowSettingsDraftForRecovery();
if (preservedDraft) {
setRecoverableDraftUpdatedAt(preservedDraft.updatedAt);
}
const hydrationTimer = window.setTimeout(() => {
@ -1255,6 +1467,28 @@ export function AgentWorkflowSettingsClient({
return () => window.clearTimeout(hydrationTimer);
}, [data]);
useEffect(() => {
if (!data || !hasHydratedSettingsRef.current) {
return;
}
const latestServerState = createAgentWorkflowSettingsStateFromPayload(data);
const latestServerSignature =
createAgentWorkflowSettingsStateSignature(latestServerState);
const baseSignature = serverDraftBaseSignatureRef.current;
if (
baseSignature &&
latestServerSignature !== baseSignature &&
!hasWarnedRemoteWorkflowUpdateRef.current
) {
hasWarnedRemoteWorkflowUpdateRef.current = true;
toast.warning(
"服务器工作流配置已有新版本。当前页面不会自动覆盖你的编辑,保存时只会提交你改过的部分。"
);
}
}, [data]);
const brandStoreEntries = useMemo(
() =>
config.inputOptions?.brandStoreEntries ||
@ -1357,8 +1591,14 @@ export function AgentWorkflowSettingsClient({
const updatedAt = Date.now();
try {
writeAgentWorkflowSettingsDraft({
version: 1,
version: 2,
updatedAt,
baseSignature:
serverDraftBaseSignatureRef.current ||
createAgentWorkflowSettingsStateSignature(autoDraftState),
baseSignatures:
serverDraftBaseSignaturesRef.current ||
createAgentWorkflowSettingsSectionSignatures(autoDraftState),
state: autoDraftState,
});
setAutoDraftUpdatedAt(updatedAt);
@ -2671,22 +2911,108 @@ export function AgentWorkflowSettingsClient({
});
};
const handleRestorePreservedDraft = () => {
const draft = readPreservedAgentWorkflowSettingsDraftForRecovery();
if (!draft) {
setRecoverableDraftUpdatedAt(null);
toast.error("没有找到可恢复的本地旧草稿");
return;
}
const nextState: AgentWorkflowSettingsDraftState = {
presetLibraryConfig: normalizeAgentPresetLibraryConfig(draft.state.presetLibraryConfig),
config: normalizeConfigOrdering(normalizeAgentWorkflowUiConfig(draft.state.config)),
materialApiConfig: normalizeAgentWorkflowMaterialApiConfig(
draft.state.materialApiConfig
),
goodsInfoApiConfig: normalizeAgentWorkflowGoodsInfoApiConfig(
draft.state.goodsInfoApiConfig
),
showcaseConfig: normalizeAgentWorkflowShowcaseConfig(draft.state.showcaseConfig),
psdTextColorConfig: normalizeAgentPsdTextColorConfig(draft.state.psdTextColorConfig),
psdTemplateSecondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig(
draft.state.psdTemplateSecondaryCategoryConfig
),
brandModelConfig: normalizeAgentWorkflowBrandModelConfig(draft.state.brandModelConfig),
};
setPresetLibraryConfig(nextState.presetLibraryConfig);
setConfig(nextState.config);
setMaterialApiConfig(nextState.materialApiConfig);
setGoodsInfoApiConfig(nextState.goodsInfoApiConfig);
setShowcaseConfig(nextState.showcaseConfig);
setPsdTextColorConfig(nextState.psdTextColorConfig);
setPsdTemplateSecondaryCategoryConfig(nextState.psdTemplateSecondaryCategoryConfig);
setBrandModelConfig(nextState.brandModelConfig);
if (nextState.config.steps[0]) {
setExpandedSteps(new Set([nextState.config.steps[0].key]));
}
setAutoDraftUpdatedAt(Date.now());
setAutoDraftStatus("restored");
toast.success("已把旧草稿恢复到当前页面,确认无误后点击保存配置写入服务器");
};
const handleSave = async () => {
try {
setIsSaving(true);
const latestSettingsResponse = await fetch("/api/site-settings", {
cache: "no-store",
});
if (!latestSettingsResponse.ok) {
throw new Error("保存前校验服务器配置失败,请刷新页面后重试");
}
const latestSettings =
(await latestSettingsResponse.json()) as AgentWorkflowSettingsPayload;
const latestServerState = createAgentWorkflowSettingsStateFromPayload(latestSettings);
const latestServerSignatures =
createAgentWorkflowSettingsSectionSignatures(latestServerState);
const baseSignatures =
serverDraftBaseSignaturesRef.current ||
createAgentWorkflowSettingsSectionSignatures(latestServerState);
const currentSignatures =
createAgentWorkflowSettingsSectionSignatures(autoDraftState);
const changedEntries = getAgentWorkflowSettingsPayloadEntries(autoDraftState).filter(
({ field }) => currentSignatures[field] !== baseSignatures[field]
);
if (changedEntries.length === 0) {
removeAgentWorkflowSettingsDraft();
setAutoDraftUpdatedAt(null);
setAutoDraftStatus("idle");
toast.success("没有检测到需要保存的修改");
return;
}
const conflictedFields = changedEntries
.filter(({ field }) => latestServerSignatures[field] !== baseSignatures[field])
.map(({ field }) => AGENT_WORKFLOW_SETTINGS_FIELD_LABELS[field]);
if (conflictedFields.length > 0) {
toast.error(
`以下配置已被其他管理员更新,本次保存已阻止:${conflictedFields.join("、")}。请刷新页面确认后再保存。`
);
return;
}
const changedPayload: Partial<AgentWorkflowSettingsPayload> & {
agentWorkflowSettingsBaseSignatures: Partial<
Record<AgentWorkflowSettingsField, string>
>;
} = {
agentWorkflowSettingsBaseSignatures: {},
};
for (const { field, value } of changedEntries) {
changedPayload[field] = value as never;
changedPayload.agentWorkflowSettingsBaseSignatures[field] = baseSignatures[field];
}
const response = await fetch("/api/site-settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agentPresetLibraryConfig: presetLibraryConfig,
agentWorkflowUiConfig: config,
agentWorkflowMaterialApiConfig: materialApiConfig,
agentWorkflowGoodsInfoApiConfig: goodsInfoApiConfig,
agentWorkflowBrandModelConfig: brandModelConfig,
agentWorkflowShowcaseConfig: showcaseConfig,
agentPsdTextColorConfig: psdTextColorConfig,
agentPsdTemplateSecondaryCategoryConfig: psdTemplateSecondaryCategoryConfig,
}),
body: JSON.stringify(changedPayload),
});
if (!response.ok) {
@ -2724,17 +3050,32 @@ export function AgentWorkflowSettingsClient({
);
const expectedStepCount = countAgentWorkflowSteps(config.steps || []);
const savedStepCount = countAgentWorkflowSteps(savedWorkflowConfig.steps || []);
if (savedStepCount !== expectedStepCount) {
const savedWorkflowConfigWasChanged = changedEntries.some(
({ field }) => field === "agentWorkflowUiConfig"
);
if (savedWorkflowConfigWasChanged && savedStepCount !== expectedStepCount) {
toast.error(
`服务端回读到的模块数量不一致(本地 ${expectedStepCount} / 服务端 ${savedStepCount}),已保留本地自动草稿,请先不要刷新页面。`
);
return;
}
const updatedServerState = createAgentWorkflowSettingsStateFromPayload(
updatedSettings || latestSettings
);
serverDraftBaseSignatureRef.current =
createAgentWorkflowSettingsStateSignature(updatedServerState);
serverDraftBaseSignaturesRef.current =
createAgentWorkflowSettingsSectionSignatures(updatedServerState);
hasWarnedRemoteWorkflowUpdateRef.current = false;
removeAgentWorkflowSettingsDraft();
setAutoDraftUpdatedAt(null);
setAutoDraftStatus("idle");
toast.success("风格化工作流配置已保存");
toast.success(
`已保存 ${changedEntries
.map(({ field }) => AGENT_WORKFLOW_SETTINGS_FIELD_LABELS[field])
.join("、")}`
);
} catch (error) {
toast.error(error instanceof Error ? error.message : "保存失败");
} finally {
@ -3916,6 +4257,16 @@ export function AgentWorkflowSettingsClient({
})}
</span>
) : null}
{recoverableDraftUpdatedAt ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleRestorePreservedDraft}
>
恢复旧草稿
</Button>
) : null}
<Button asChild variant="outline" size="sm">
<Link href="/settings/agent-workflow/psd-templates">
<Layers className="mr-2 h-4 w-4" />

View File

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
import { siteSettings, systemSettings } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { siteSettings, systemSettingHistory, systemSettings } from "@/lib/db/schema";
import { eq, inArray } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth";
import { isAgentWorkflowAdminType, isSiteAdminType } from "@/lib/middleware/admin";
import {
@ -34,6 +34,7 @@ import {
AGENT_WORKFLOW_UI_CONFIG_KEY,
AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH,
defaultAgentWorkflowUiConfig,
normalizeAgentWorkflowStepOrdering,
normalizeAgentWorkflowUiConfig,
} from "@/lib/agents/workflow-ui-config";
import {
@ -111,6 +112,112 @@ const AGENT_CATEGORY_TREE_CONFIG_KEY = "agent_category_tree_config";
const CANVAS_AGENT_MODE_ENABLED_KEY = "canvas_agent_mode_enabled";
const VIDEO_CANVAS_ENABLED_KEY = "video_canvas_enabled";
const VIDEO_CANVAS_SIDEBAR_ENABLED_KEY = "video_canvas_sidebar_enabled";
const AGENT_WORKFLOW_SETTINGS_CONFLICT_KEYS = [
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;
const AGENT_WORKFLOW_SETTINGS_FIELD_KEY_MAP = {
agentPresetLibraryConfig: AGENT_PRESET_LIBRARY_CONFIG_KEY,
agentWorkflowUiConfig: AGENT_WORKFLOW_UI_CONFIG_KEY,
agentWorkflowMaterialApiConfig: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
agentWorkflowGoodsInfoApiConfig: AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
agentWorkflowBrandModelConfig: AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
agentWorkflowShowcaseConfig: AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
agentPsdTextColorConfig: AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
agentPsdTemplateSecondaryCategoryConfig:
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
} as const;
type AgentWorkflowSettingsField = keyof typeof AGENT_WORKFLOW_SETTINGS_FIELD_KEY_MAP;
function stableStringifySettingsValue(value: unknown): string {
if (value === null || typeof value !== "object") {
return JSON.stringify(value) ?? "null";
}
if (Array.isArray(value)) {
return `[${value.map((item) => stableStringifySettingsValue(item)).join(",")}]`;
}
const entries = Object.entries(value as Record<string, unknown>)
.filter(([, item]) => item !== undefined)
.sort(([left], [right]) => left.localeCompare(right));
return `{${entries
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringifySettingsValue(item)}`)
.join(",")}}`;
}
function createSettingsValueSignature(value: unknown): string {
const serialized = stableStringifySettingsValue(value);
let hashA = 0xdeadbeef ^ serialized.length;
let hashB = 0x41c6ce57 ^ serialized.length;
for (let index = 0; index < serialized.length; index += 1) {
const code = serialized.charCodeAt(index);
hashA = Math.imul(hashA ^ code, 2654435761);
hashB = Math.imul(hashB ^ code, 1597334677);
}
hashA =
Math.imul(hashA ^ (hashA >>> 16), 2246822507) ^
Math.imul(hashB ^ (hashB >>> 13), 3266489909);
hashB =
Math.imul(hashB ^ (hashB >>> 16), 2246822507) ^
Math.imul(hashA ^ (hashA >>> 13), 3266489909);
return `${serialized.length}:${(hashB >>> 0).toString(16)}${(hashA >>> 0).toString(16)}`;
}
function createAgentWorkflowSettingsStateFromValues(valueByKey: Map<string, unknown>) {
const workflowUiConfig = normalizeAgentWorkflowUiConfig(
valueByKey.get(AGENT_WORKFLOW_UI_CONFIG_KEY)
);
return {
agentPresetLibraryConfig: normalizeAgentPresetLibraryConfig(
valueByKey.get(AGENT_PRESET_LIBRARY_CONFIG_KEY)
),
agentWorkflowUiConfig: {
...workflowUiConfig,
steps: normalizeAgentWorkflowStepOrdering(workflowUiConfig.steps),
},
agentWorkflowMaterialApiConfig: normalizeAgentWorkflowMaterialApiConfig(
valueByKey.get(AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY)
),
agentWorkflowGoodsInfoApiConfig: normalizeAgentWorkflowGoodsInfoApiConfig(
valueByKey.get(AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY)
),
agentWorkflowBrandModelConfig: normalizeAgentWorkflowBrandModelConfig(
valueByKey.get(AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY)
),
agentWorkflowShowcaseConfig: normalizeAgentWorkflowShowcaseConfig(
valueByKey.get(AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY)
),
agentPsdTextColorConfig: normalizeAgentPsdTextColorConfig(
valueByKey.get(AGENT_PSD_TEXT_COLOR_CONFIG_KEY)
),
agentPsdTemplateSecondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig(
valueByKey.get(AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY)
),
} satisfies Record<AgentWorkflowSettingsField, unknown>;
}
function createAgentWorkflowSettingsSectionSignatures(valueByKey: Map<string, unknown>) {
const state = createAgentWorkflowSettingsStateFromValues(valueByKey);
return Object.fromEntries(
Object.entries(state).map(([field, value]) => [field, createSettingsValueSignature(value)])
) as Record<AgentWorkflowSettingsField, string>;
}
function createAgentWorkflowSettingsSignature(valueByKey: Map<string, unknown>): string {
return createSettingsValueSignature(createAgentWorkflowSettingsStateFromValues(valueByKey));
}
function normalizeHomeVariant(value: unknown): "classic" | "lovart" {
return value === "lovart" ? "lovart" : "classic";
@ -853,6 +960,10 @@ const updateSchema = z.object({
text: z.array(z.string().trim().min(1).max(64)).max(200).optional(),
})
.optional(),
agentWorkflowSettingsBaseSignature: z.string().trim().min(1).max(128).optional(),
agentWorkflowSettingsBaseSignatures: z
.record(z.string().trim().min(1).max(128))
.optional(),
agentWorkflowUiConfig: z
.object({
inputOptions: z
@ -1112,6 +1223,8 @@ export async function PUT(request: NextRequest) {
if (!isAdmin) {
const allowedKeys = new Set([
"agentPresetLibraryConfig",
"agentWorkflowSettingsBaseSignature",
"agentWorkflowSettingsBaseSignatures",
"agentWorkflowUiConfig",
"agentWorkflowMaterialApiConfig",
"agentWorkflowGoodsInfoApiConfig",
@ -1138,6 +1251,8 @@ export async function PUT(request: NextRequest) {
agentPresetLibraryConfig,
agentCategoryTreeConfig,
agentSecondaryTypeConfig,
agentWorkflowSettingsBaseSignature,
agentWorkflowSettingsBaseSignatures,
agentWorkflowUiConfig,
agentWorkflowMaterialApiConfig,
agentWorkflowGoodsInfoApiConfig,
@ -1160,6 +1275,106 @@ export async function PUT(request: NextRequest) {
...siteSettingsPayload
} = data;
const updatedAgentWorkflowFields = (
[
["agentPresetLibraryConfig", agentPresetLibraryConfig],
["agentWorkflowUiConfig", agentWorkflowUiConfig],
["agentWorkflowMaterialApiConfig", agentWorkflowMaterialApiConfig],
["agentWorkflowGoodsInfoApiConfig", agentWorkflowGoodsInfoApiConfig],
["agentWorkflowBrandModelConfig", agentWorkflowBrandModelConfig],
["agentWorkflowShowcaseConfig", agentWorkflowShowcaseConfig],
["agentPsdTextColorConfig", agentPsdTextColorConfig],
["agentPsdTemplateSecondaryCategoryConfig", agentPsdTemplateSecondaryCategoryConfig],
] as const
)
.filter(([, value]) => value !== undefined)
.map(([field]) => field);
const isUpdatingAgentWorkflowSettings = updatedAgentWorkflowFields.length > 0;
if (isUpdatingAgentWorkflowSettings) {
if (!agentWorkflowSettingsBaseSignatures && !agentWorkflowSettingsBaseSignature) {
return NextResponse.json(
{
error:
"工作流配置已启用多人协作保护,请刷新页面后再保存,避免覆盖其他管理员的数据。",
},
{ status: 409 }
);
}
const currentWorkflowRows = await db
.select({
key: systemSettings.key,
value: systemSettings.value,
})
.from(systemSettings)
.where(inArray(systemSettings.key, [...AGENT_WORKFLOW_SETTINGS_CONFLICT_KEYS]));
const currentWorkflowValues = new Map<string, unknown>(
currentWorkflowRows.map((row) => [row.key, row.value])
);
const currentSectionSignatures =
createAgentWorkflowSettingsSectionSignatures(currentWorkflowValues);
const currentWorkflowSignature =
agentWorkflowSettingsBaseSignature && !agentWorkflowSettingsBaseSignatures
? createAgentWorkflowSettingsSignature(currentWorkflowValues)
: null;
const hasWorkflowConflict = agentWorkflowSettingsBaseSignatures
? updatedAgentWorkflowFields.some(
(field) =>
currentSectionSignatures[field] !== agentWorkflowSettingsBaseSignatures[field]
)
: currentWorkflowSignature !== agentWorkflowSettingsBaseSignature;
if (hasWorkflowConflict) {
return NextResponse.json(
{
error:
"服务器上的同一块工作流配置已被其他管理员更新,本次保存已阻止。请刷新页面确认最新配置后再保存。",
},
{ status: 409 }
);
}
}
const persistSystemSettingWithHistory = async (
key: string,
value: unknown,
reason: string
) => {
const [existingSetting] = await db
.select({ value: systemSettings.value })
.from(systemSettings)
.where(eq(systemSettings.key, key))
.limit(1);
await db.insert(systemSettingHistory).values({
key,
previousValue: existingSetting?.value ?? null,
nextValue: value,
reason,
changedAt: new Date(),
changedBy: session.user.id,
});
await db
.insert(systemSettings)
.values({
key,
value,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
};
const persistHomeAnimationConfig = async () => {
if (homeAnimationConfig === undefined) {
return;
@ -1416,22 +1631,11 @@ export async function PUT(request: NextRequest) {
if (agentPresetLibraryConfig !== undefined) {
const normalized = normalizeAgentPresetLibraryConfig(agentPresetLibraryConfig);
await db
.insert(systemSettings)
.values({
key: AGENT_PRESET_LIBRARY_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_PRESET_LIBRARY_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentCategoryTreeConfig !== undefined) {
@ -1476,172 +1680,73 @@ export async function PUT(request: NextRequest) {
if (agentWorkflowUiConfig !== undefined) {
const normalized = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_UI_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_UI_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentWorkflowMaterialApiConfig !== undefined) {
const normalized = normalizeAgentWorkflowMaterialApiConfig(
agentWorkflowMaterialApiConfig
);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentWorkflowGoodsInfoApiConfig !== undefined) {
const normalized = normalizeAgentWorkflowGoodsInfoApiConfig(
agentWorkflowGoodsInfoApiConfig
);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentWorkflowBrandModelConfig !== undefined) {
const normalized = normalizeAgentWorkflowBrandModelConfig(
agentWorkflowBrandModelConfig
);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentWorkflowShowcaseConfig !== undefined) {
const normalized = normalizeAgentWorkflowShowcaseConfig(agentWorkflowShowcaseConfig);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentPsdTextColorConfig !== undefined) {
const normalized = normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig);
await db
.insert(systemSettings)
.values({
key: AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentPsdTemplateSecondaryCategoryConfig !== undefined) {
const normalized = normalizeAgentPsdTemplateSecondaryCategoryConfig(
agentPsdTemplateSecondaryCategoryConfig
);
await db
.insert(systemSettings)
.values({
key: AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (agentPsdTemplateSecondaryCategoryConfig !== undefined) {
const normalized = normalizeAgentPsdTemplateSecondaryCategoryConfig(
agentPsdTemplateSecondaryCategoryConfig
await persistSystemSettingWithHistory(
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
normalized,
"agent-workflow"
);
await db
.insert(systemSettings)
.values({
key: AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (agentPsdTemplateCategoryTagConfig !== undefined) {
@ -2144,22 +2249,11 @@ export async function PUT(request: NextRequest) {
if (agentPresetLibraryConfig !== undefined) {
const normalized = normalizeAgentPresetLibraryConfig(agentPresetLibraryConfig);
await db
.insert(systemSettings)
.values({
key: AGENT_PRESET_LIBRARY_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_PRESET_LIBRARY_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentCategoryTreeConfig !== undefined) {
@ -2204,106 +2298,51 @@ export async function PUT(request: NextRequest) {
if (agentWorkflowUiConfig !== undefined) {
const normalized = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_UI_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_UI_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentWorkflowMaterialApiConfig !== undefined) {
const normalized = normalizeAgentWorkflowMaterialApiConfig(
agentWorkflowMaterialApiConfig
);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentWorkflowBrandModelConfig !== undefined) {
const normalized = normalizeAgentWorkflowBrandModelConfig(
agentWorkflowBrandModelConfig
);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentWorkflowShowcaseConfig !== undefined) {
const normalized = normalizeAgentWorkflowShowcaseConfig(agentWorkflowShowcaseConfig);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentPsdTextColorConfig !== undefined) {
const normalized = normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig);
await db
.insert(systemSettings)
.values({
key: AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
await persistSystemSettingWithHistory(
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
normalized,
"agent-workflow"
);
}
if (agentPsdTemplateCategoryTagConfig !== undefined) {

View File

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS "SystemSettingHistory" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"key" varchar(128) NOT NULL,
"previousValue" jsonb,
"nextValue" jsonb,
"reason" varchar(128),
"changedAt" timestamp DEFAULT now() NOT NULL,
"changedBy" uuid
);
DO $$ BEGIN
ALTER TABLE "SystemSettingHistory"
ADD CONSTRAINT "SystemSettingHistory_changedBy_User_id_fk"
FOREIGN KEY ("changedBy") REFERENCES "public"."User"("id")
ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE INDEX IF NOT EXISTS "SystemSettingHistory_key_changedAt_idx"
ON "SystemSettingHistory" ("key", "changedAt" DESC);

View File

@ -414,6 +414,13 @@
"when": 1776042000000,
"tag": "0072_add_krea_provider_type",
"breakpoints": true
},
{
"idx": 59,
"version": "7",
"when": 1776336730695,
"tag": "0073_add_system_setting_history",
"breakpoints": true
}
]
}

View File

@ -248,6 +248,18 @@ export const systemSettings = pgTable("SystemSettings", {
export type SystemSettings = InferSelectModel<typeof systemSettings>;
export const systemSettingHistory = pgTable("SystemSettingHistory", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
key: varchar("key", { length: 128 }).notNull(),
previousValue: jsonb("previousValue"),
nextValue: jsonb("nextValue"),
reason: varchar("reason", { length: 128 }),
changedAt: timestamp("changedAt").notNull().defaultNow(),
changedBy: uuid("changedBy").references(() => user.id),
});
export type SystemSettingHistory = InferSelectModel<typeof systemSettingHistory>;
export const toolConfig = pgTable("ToolConfig", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
toolName: varchar("toolName", { length: 128 }).notNull().unique(),