diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 2cae5d26a..3d2506a8f 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -2737,8 +2737,13 @@ export function AdminSettingsClient() { siteSettings.pointsSystemConfig ); - const payload = { + const siteSettingsPayload: Partial = { ...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, diff --git a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx index 42bfbaddb..e789e6a09 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -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>; 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) + .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; +} + 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; + 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; + 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 = { + 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( + "/api/site-settings", + fetcher, + { refreshInterval: 60_000 } + ); const [presetLibraryConfig, setPresetLibraryConfig] = useState( defaultAgentPresetLibraryConfig ); @@ -1178,11 +1368,17 @@ export function AgentWorkflowSettingsClient({ ); const [autoDraftUpdatedAt, setAutoDraftUpdatedAt] = useState(null); const [autoDraftStatus, setAutoDraftStatus] = useState<"idle" | "restored" | "saved">("idle"); + const [recoverableDraftUpdatedAt, setRecoverableDraftUpdatedAt] = useState(null); const fileInputRefs = useRef>({}); const brandImportInputRef = useRef(null); const hasAppliedInitialSettingsRef = useRef(false); const hasHydratedSettingsRef = useRef(false); const isApplyingSettingsRef = useRef(false); + const serverDraftBaseSignatureRef = useRef(null); + const serverDraftBaseSignaturesRef = useRef< + Record | 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 & { + agentWorkflowSettingsBaseSignatures: Partial< + Record + >; + } = { + 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({ })} ) : null} + {recoverableDraftUpdatedAt ? ( + + ) : null}