From 5ef376b657a52678d20daccc68760fb47336a6ff Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Wed, 29 Jul 2026 15:09:00 +0800 Subject: [PATCH] refactor(workflow): streamline workflow settings management - Introduced new utility functions `settingsFromWorkflowGraph` and `workflowGraphWithSettings` to encapsulate settings extraction and graph updates. - Updated `useWorkflowEditorState` to utilize the new utility functions for improved clarity and maintainability. - Refactored `AssistantPage` and `WorkflowCanvas` components to leverage the new settings management functions, ensuring consistent handling of workflow settings across the application. - Removed redundant settings handling code, enhancing overall code cleanliness. --- .../use-workflow-editor-state.ts | 46 +++------------- .../src/components/pages/AssistantPage.tsx | 14 ++--- .../components/workflow/WorkflowCanvas.tsx | 52 ++++++------------- .../src/components/workflow/graph-settings.ts | 51 ++++++++++++++++++ 4 files changed, 81 insertions(+), 82 deletions(-) create mode 100644 frontend/src/components/workflow/graph-settings.ts diff --git a/frontend/src/components/assistant-editor/use-workflow-editor-state.ts b/frontend/src/components/assistant-editor/use-workflow-editor-state.ts index c9537da..950c3f0 100644 --- a/frontend/src/components/assistant-editor/use-workflow-editor-state.ts +++ b/frontend/src/components/assistant-editor/use-workflow-editor-state.ts @@ -2,6 +2,10 @@ import { useCallback, useMemo, useState } from "react"; +import { + settingsFromWorkflowGraph, + workflowGraphWithSettings, +} from "@/components/workflow/graph-settings"; import type { WorkflowSettings } from "@/components/workflow/types"; import { defaultGraph, type WorkflowGraph } from "@/components/workflow/specs"; import type { DynamicVariableDefinition } from "@/lib/api"; @@ -13,26 +17,6 @@ type WorkflowPanel = | { type: "edge"; edgeId: string } | null; -function settingsFromGraph(graph: WorkflowGraph): WorkflowSettings { - return { - globalPrompt: graph.settings.globalPrompt, - llm: graph.settings.defaultLlmResourceId, - asr: graph.settings.defaultAsrResourceId, - tts: graph.settings.defaultTtsResourceId, - visionEnabled: graph.settings.visionEnabled, - visionModelResourceId: graph.settings.visionModelResourceId, - toolIds: graph.settings.toolIds, - knowledgeBaseId: graph.settings.knowledgeBaseId, - knowledgeRetrievalConfig: { - mode: graph.settings.knowledgeMode, - topN: graph.settings.knowledgeTopN, - scoreThreshold: graph.settings.knowledgeScoreThreshold, - }, - allowInterrupt: graph.settings.enableInterrupt, - turnConfig: graph.settings.turnConfig, - }; -} - /** Owns Workflow editor state; AssistantPage only coordinates loading and saving. */ export function useWorkflowEditorState() { const [workflowName, setWorkflowName] = useState(""); @@ -40,29 +24,11 @@ export function useWorkflowEditorState() { defaultGraph(), ); const workflowSettings = useMemo( - () => settingsFromGraph(workflowGraph), + () => settingsFromWorkflowGraph(workflowGraph), [workflowGraph], ); const setWorkflowSettings = useCallback((settings: WorkflowSettings) => { - setWorkflowGraph((graph) => ({ - ...graph, - settings: { - globalPrompt: settings.globalPrompt, - defaultLlmResourceId: settings.llm ?? "", - defaultAsrResourceId: settings.asr ?? "", - defaultTtsResourceId: settings.tts ?? "", - visionEnabled: settings.visionEnabled, - visionModelResourceId: settings.visionModelResourceId, - toolIds: settings.toolIds, - knowledgeBaseId: settings.knowledgeBaseId, - knowledgeMode: settings.knowledgeRetrievalConfig.mode, - knowledgeTopN: settings.knowledgeRetrievalConfig.topN, - knowledgeScoreThreshold: - settings.knowledgeRetrievalConfig.scoreThreshold, - enableInterrupt: settings.allowInterrupt, - turnConfig: settings.turnConfig, - }, - })); + setWorkflowGraph((graph) => workflowGraphWithSettings(graph, settings)); }, []); const [workflowDynamicVariableDefinitions, setWorkflowDynamicVariableDefinitions] = useState>({}); diff --git a/frontend/src/components/pages/AssistantPage.tsx b/frontend/src/components/pages/AssistantPage.tsx index 6b0c88e..63b18ef 100644 --- a/frontend/src/components/pages/AssistantPage.tsx +++ b/frontend/src/components/pages/AssistantPage.tsx @@ -53,6 +53,7 @@ import { type TurnConfig, } from "@/lib/api"; import type { WorkflowSettings } from "@/components/workflow/types"; +import { workflowGraphWithSettings } from "@/components/workflow/graph-settings"; import { SectionCard } from "@/components/editor/section-card"; import { defaultGraph, @@ -571,7 +572,6 @@ export function AssistantPage(props: AssistantPageProps) { JSON.stringify({ name: workflowName, graph: workflowGraph, - settings: workflowSettings, dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions, }), ); @@ -721,12 +721,14 @@ export function AssistantPage(props: AssistantPageProps) { graph.settings?.turnConfig ?? assistant.turnConfig, ), }; + // 编辑器会把兼容字段和默认值写回 graph。状态与保存基线必须使用 + // 同一个规范化结果,否则页面刚打开就会被误判为有未保存改动。 + const normalizedGraph = workflowGraphWithSettings(graph, wfSettings); setWorkflowName(assistant.name); - setWorkflowGraph(graph); - setWorkflowSettings(wfSettings); + setWorkflowGraph(normalizedGraph); const dynamicVariableDefinitions = activeWorkflowDynamicVariableDefinitions( - graph, + normalizedGraph, assistant.dynamicVariableDefinitions ?? {}, ); setWorkflowDynamicVariableDefinitions( @@ -735,8 +737,7 @@ export function AssistantPage(props: AssistantPageProps) { setSavedSnapshot( JSON.stringify({ name: assistant.name, - graph, - settings: wfSettings, + graph: normalizedGraph, dynamicVariableDefinitions, }), ); @@ -818,7 +819,6 @@ export function AssistantPage(props: AssistantPageProps) { ? JSON.stringify({ name: workflowName, graph: workflowGraph, - settings: workflowSettings, dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions, }) diff --git a/frontend/src/components/workflow/WorkflowCanvas.tsx b/frontend/src/components/workflow/WorkflowCanvas.tsx index e191042..35c580f 100644 --- a/frontend/src/components/workflow/WorkflowCanvas.tsx +++ b/frontend/src/components/workflow/WorkflowCanvas.tsx @@ -56,6 +56,7 @@ import { type WorkflowNodeType, } from "./specs"; import type { WorkflowEditorProps } from "./types"; +import { workflowGraphWithSettings } from "./graph-settings"; let nodeSeq = 0; @@ -160,47 +161,28 @@ export function WorkflowCanvas({ const [addPosition, setAddPosition] = useState<{ x: number; y: number } | null>(null); const { screenToFlowPosition } = useReactFlow(); - // 回传画布状态给外部(助手 graph)。用 ref 避免把 onChange 放进依赖导致循环。 + // 只在可持久化的画布内容真正变化时回传。React Flow 挂载时会写入节点尺寸等 + // 内部状态,这些不属于 workflow graph,不能因此把刚加载的助手标记为未保存。 const onChangeRef = useRef(onChange); + const lastSyncedGraphRef = useRef( + JSON.stringify( + workflowGraphWithSettings( + fromFlow(initial.nodes, initial.edges), + settings, + ), + ), + ); useEffect(() => { onChangeRef.current = onChange; }, [onChange]); useEffect(() => { - const graph = fromFlow(nodes, edges); - graph.settings = { - globalPrompt: settings.globalPrompt, - defaultLlmResourceId: settings.llm ?? "", - defaultAsrResourceId: settings.asr ?? "", - defaultTtsResourceId: settings.tts ?? "", - visionEnabled: settings.visionEnabled, - visionModelResourceId: settings.visionModelResourceId, - toolIds: settings.toolIds, - knowledgeBaseId: settings.knowledgeBaseId, - knowledgeMode: settings.knowledgeRetrievalConfig.mode, - knowledgeTopN: settings.knowledgeRetrievalConfig.topN, - knowledgeScoreThreshold: - settings.knowledgeRetrievalConfig.scoreThreshold, - enableInterrupt: settings.allowInterrupt, - turnConfig: settings.turnConfig, - }; + const graph = workflowGraphWithSettings(fromFlow(nodes, edges), settings); + const nextGraphJson = JSON.stringify(graph); + if (nextGraphJson === lastSyncedGraphRef.current) return; + + lastSyncedGraphRef.current = nextGraphJson; onChangeRef.current?.(graph); - }, [ - nodes, - edges, - settings.globalPrompt, - settings.llm, - settings.asr, - settings.tts, - settings.visionEnabled, - settings.visionModelResourceId, - settings.toolIds, - settings.knowledgeBaseId, - settings.knowledgeRetrievalConfig.mode, - settings.knowledgeRetrievalConfig.topN, - settings.knowledgeRetrievalConfig.scoreThreshold, - settings.allowInterrupt, - settings.turnConfig, - ]); + }, [nodes, edges, settings]); const onConnect = useCallback( (connection: Connection) => { diff --git a/frontend/src/components/workflow/graph-settings.ts b/frontend/src/components/workflow/graph-settings.ts new file mode 100644 index 0000000..8d09142 --- /dev/null +++ b/frontend/src/components/workflow/graph-settings.ts @@ -0,0 +1,51 @@ +import type { WorkflowGraph } from "@/components/workflow/specs"; +import type { WorkflowSettings } from "@/components/workflow/types"; + +/** Read the editor-facing settings from the settings stored in a workflow graph. */ +export function settingsFromWorkflowGraph( + graph: WorkflowGraph, +): WorkflowSettings { + return { + globalPrompt: graph.settings.globalPrompt, + llm: graph.settings.defaultLlmResourceId, + asr: graph.settings.defaultAsrResourceId, + tts: graph.settings.defaultTtsResourceId, + visionEnabled: graph.settings.visionEnabled, + visionModelResourceId: graph.settings.visionModelResourceId, + toolIds: graph.settings.toolIds, + knowledgeBaseId: graph.settings.knowledgeBaseId, + knowledgeRetrievalConfig: { + mode: graph.settings.knowledgeMode, + topN: graph.settings.knowledgeTopN, + scoreThreshold: graph.settings.knowledgeScoreThreshold, + }, + allowInterrupt: graph.settings.enableInterrupt, + turnConfig: graph.settings.turnConfig, + }; +} + +/** Return a graph whose persisted settings match the editor-facing settings. */ +export function workflowGraphWithSettings( + graph: WorkflowGraph, + settings: WorkflowSettings, +): WorkflowGraph { + return { + ...graph, + settings: { + globalPrompt: settings.globalPrompt, + defaultLlmResourceId: settings.llm ?? "", + defaultAsrResourceId: settings.asr ?? "", + defaultTtsResourceId: settings.tts ?? "", + visionEnabled: settings.visionEnabled, + visionModelResourceId: settings.visionModelResourceId, + toolIds: settings.toolIds, + knowledgeBaseId: settings.knowledgeBaseId, + knowledgeMode: settings.knowledgeRetrievalConfig.mode, + knowledgeTopN: settings.knowledgeRetrievalConfig.topN, + knowledgeScoreThreshold: + settings.knowledgeRetrievalConfig.scoreThreshold, + enableInterrupt: settings.allowInterrupt, + turnConfig: settings.turnConfig, + }, + }; +}