fix: protect workflow settings from overwrites
This commit is contained in:
parent
c5a4b4add2
commit
8c8d2c07f5
@ -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,
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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) {
|
||||
|
||||
21
lib/db/migrations/0073_add_system_setting_history.sql
Normal file
21
lib/db/migrations/0073_add_system_setting_history.sql
Normal 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);
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user