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.
This commit is contained in:
Xin Wang
2026-07-29 15:09:00 +08:00
parent 774825593d
commit 5ef376b657
4 changed files with 81 additions and 82 deletions

View File

@@ -2,6 +2,10 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import {
settingsFromWorkflowGraph,
workflowGraphWithSettings,
} from "@/components/workflow/graph-settings";
import type { WorkflowSettings } from "@/components/workflow/types"; import type { WorkflowSettings } from "@/components/workflow/types";
import { defaultGraph, type WorkflowGraph } from "@/components/workflow/specs"; import { defaultGraph, type WorkflowGraph } from "@/components/workflow/specs";
import type { DynamicVariableDefinition } from "@/lib/api"; import type { DynamicVariableDefinition } from "@/lib/api";
@@ -13,26 +17,6 @@ type WorkflowPanel =
| { type: "edge"; edgeId: string } | { type: "edge"; edgeId: string }
| null; | 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. */ /** Owns Workflow editor state; AssistantPage only coordinates loading and saving. */
export function useWorkflowEditorState() { export function useWorkflowEditorState() {
const [workflowName, setWorkflowName] = useState(""); const [workflowName, setWorkflowName] = useState("");
@@ -40,29 +24,11 @@ export function useWorkflowEditorState() {
defaultGraph(), defaultGraph(),
); );
const workflowSettings = useMemo( const workflowSettings = useMemo(
() => settingsFromGraph(workflowGraph), () => settingsFromWorkflowGraph(workflowGraph),
[workflowGraph], [workflowGraph],
); );
const setWorkflowSettings = useCallback((settings: WorkflowSettings) => { const setWorkflowSettings = useCallback((settings: WorkflowSettings) => {
setWorkflowGraph((graph) => ({ setWorkflowGraph((graph) => workflowGraphWithSettings(graph, settings));
...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 [workflowDynamicVariableDefinitions, setWorkflowDynamicVariableDefinitions] = const [workflowDynamicVariableDefinitions, setWorkflowDynamicVariableDefinitions] =
useState<Record<string, DynamicVariableDefinition>>({}); useState<Record<string, DynamicVariableDefinition>>({});

View File

@@ -53,6 +53,7 @@ import {
type TurnConfig, type TurnConfig,
} from "@/lib/api"; } from "@/lib/api";
import type { WorkflowSettings } from "@/components/workflow/types"; import type { WorkflowSettings } from "@/components/workflow/types";
import { workflowGraphWithSettings } from "@/components/workflow/graph-settings";
import { SectionCard } from "@/components/editor/section-card"; import { SectionCard } from "@/components/editor/section-card";
import { import {
defaultGraph, defaultGraph,
@@ -571,7 +572,6 @@ export function AssistantPage(props: AssistantPageProps) {
JSON.stringify({ JSON.stringify({
name: workflowName, name: workflowName,
graph: workflowGraph, graph: workflowGraph,
settings: workflowSettings,
dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions, dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions,
}), }),
); );
@@ -721,12 +721,14 @@ export function AssistantPage(props: AssistantPageProps) {
graph.settings?.turnConfig ?? assistant.turnConfig, graph.settings?.turnConfig ?? assistant.turnConfig,
), ),
}; };
// 编辑器会把兼容字段和默认值写回 graph。状态与保存基线必须使用
// 同一个规范化结果,否则页面刚打开就会被误判为有未保存改动。
const normalizedGraph = workflowGraphWithSettings(graph, wfSettings);
setWorkflowName(assistant.name); setWorkflowName(assistant.name);
setWorkflowGraph(graph); setWorkflowGraph(normalizedGraph);
setWorkflowSettings(wfSettings);
const dynamicVariableDefinitions = const dynamicVariableDefinitions =
activeWorkflowDynamicVariableDefinitions( activeWorkflowDynamicVariableDefinitions(
graph, normalizedGraph,
assistant.dynamicVariableDefinitions ?? {}, assistant.dynamicVariableDefinitions ?? {},
); );
setWorkflowDynamicVariableDefinitions( setWorkflowDynamicVariableDefinitions(
@@ -735,8 +737,7 @@ export function AssistantPage(props: AssistantPageProps) {
setSavedSnapshot( setSavedSnapshot(
JSON.stringify({ JSON.stringify({
name: assistant.name, name: assistant.name,
graph, graph: normalizedGraph,
settings: wfSettings,
dynamicVariableDefinitions, dynamicVariableDefinitions,
}), }),
); );
@@ -818,7 +819,6 @@ export function AssistantPage(props: AssistantPageProps) {
? JSON.stringify({ ? JSON.stringify({
name: workflowName, name: workflowName,
graph: workflowGraph, graph: workflowGraph,
settings: workflowSettings,
dynamicVariableDefinitions: dynamicVariableDefinitions:
effectiveWorkflowDynamicVariableDefinitions, effectiveWorkflowDynamicVariableDefinitions,
}) })

View File

@@ -56,6 +56,7 @@ import {
type WorkflowNodeType, type WorkflowNodeType,
} from "./specs"; } from "./specs";
import type { WorkflowEditorProps } from "./types"; import type { WorkflowEditorProps } from "./types";
import { workflowGraphWithSettings } from "./graph-settings";
let nodeSeq = 0; let nodeSeq = 0;
@@ -160,47 +161,28 @@ export function WorkflowCanvas({
const [addPosition, setAddPosition] = useState<{ x: number; y: number } | null>(null); const [addPosition, setAddPosition] = useState<{ x: number; y: number } | null>(null);
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
// 回传画布状态给外部(助手 graph)。用 ref 避免把 onChange 放进依赖导致循环。 // 只在可持久化的画布内容真正变化时回传。React Flow 挂载时会写入节点尺寸等
// 内部状态,这些不属于 workflow graph不能因此把刚加载的助手标记为未保存。
const onChangeRef = useRef(onChange); const onChangeRef = useRef(onChange);
const lastSyncedGraphRef = useRef(
JSON.stringify(
workflowGraphWithSettings(
fromFlow(initial.nodes, initial.edges),
settings,
),
),
);
useEffect(() => { useEffect(() => {
onChangeRef.current = onChange; onChangeRef.current = onChange;
}, [onChange]); }, [onChange]);
useEffect(() => { useEffect(() => {
const graph = fromFlow(nodes, edges); const graph = workflowGraphWithSettings(fromFlow(nodes, edges), settings);
graph.settings = { const nextGraphJson = JSON.stringify(graph);
globalPrompt: settings.globalPrompt, if (nextGraphJson === lastSyncedGraphRef.current) return;
defaultLlmResourceId: settings.llm ?? "",
defaultAsrResourceId: settings.asr ?? "", lastSyncedGraphRef.current = nextGraphJson;
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,
};
onChangeRef.current?.(graph); onChangeRef.current?.(graph);
}, [ }, [nodes, edges, settings]);
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,
]);
const onConnect = useCallback( const onConnect = useCallback(
(connection: Connection) => { (connection: Connection) => {

View File

@@ -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,
},
};
}