From 8064bb7152e9525e7d0cbf4fab306a5a7d31915b Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Mon, 3 Aug 2026 15:17:01 +0800 Subject: [PATCH] refactor(workflow): simplify edge configuration --- backend/services/brains/workflow_brain.py | 28 +--- backend/services/node_specs.py | 8 +- backend/services/workflow_engine.py | 8 -- backend/tests/test_brains.py | 30 +--- backend/tests/test_pipeline_events.py | 4 +- backend/tests/test_workflow_v3.py | 11 ++ .../assistant-editor/editor-controls.tsx | 7 +- .../components/workflow/WorkflowCanvas.tsx | 26 +++- .../workflow/panels/EdgeSettingsPanel.tsx | 134 +----------------- frontend/src/components/workflow/specs.ts | 2 +- 10 files changed, 53 insertions(+), 205 deletions(-) diff --git a/backend/services/brains/workflow_brain.py b/backend/services/brains/workflow_brain.py index 634c54e..a68363e 100644 --- a/backend/services/brains/workflow_brain.py +++ b/backend/services/brains/workflow_brain.py @@ -785,22 +785,9 @@ class WorkflowBrain(BaseBrain): triggering_user_message: dict[str, Any] | None = None, ) -> NodeConfig: await self._begin_edge_transition(edge) - context_messages = list(leading_messages or []) - speech = self._engine.edge_transition_speech(edge) - if speech: - content = self._store.render(speech).strip() - if content: - await self._queue_visible_speech( - content, - source="workflow-edge-transition", - node_id=str(edge.get("target") or "") or None, - ) - context_message = fixed_speech_context_message(content) - if context_message is not None: - context_messages.append(context_message) return await self._resolve_path( str(edge.get("target") or ""), - leading_messages=context_messages, + leading_messages=list(leading_messages or []), triggering_user_text=triggering_user_text, triggering_user_message=triggering_user_message, ) @@ -863,19 +850,6 @@ class WorkflowBrain(BaseBrain): if not edge: return self._passive_node_config(node_id, context_messages) await self._begin_edge_transition(edge) - speech = self._engine.edge_transition_speech(edge) - if speech: - content = self._store.render(speech).strip() - if content: - target_id = str(edge.get("target") or "") - await self._queue_visible_speech( - content, - source="workflow-edge-transition", - node_id=target_id or None, - ) - context_message = fixed_speech_context_message(content) - if context_message is not None: - context_messages.append(context_message) node_id = str(edge.get("target") or "") raise RuntimeError("工作流连续自动跳转超过安全上限") diff --git a/backend/services/node_specs.py b/backend/services/node_specs.py index 04f57a9..d12a02b 100644 --- a/backend/services/node_specs.py +++ b/backend/services/node_specs.py @@ -138,17 +138,17 @@ def node_types_response() -> dict[str, Any]: def _edge_data_v3(edge: dict) -> dict: data = deepcopy(edge.get("data") or {}) + data.pop("transitionSpeech", None) + data.pop("transition_speech", None) if data.get("mode") in EDGE_MODES: data.setdefault("priority", 10) return data condition = str(data.pop("condition", "") or "").strip() - transition = data.pop("transition_speech", data.get("transitionSpeech", "")) data.update( { "mode": "llm" if condition else "always", "priority": 10, "condition": condition, - "transitionSpeech": transition, } ) return data @@ -234,6 +234,8 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]: _normalize_settings(settings) source.setdefault("nodes", []) source.setdefault("edges", []) + for edge in source["edges"]: + edge["data"] = _edge_data_v3(edge) for node in source["nodes"]: data = node.setdefault("data", {}) if node.get("type") == "start": @@ -327,7 +329,7 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]: "id": f"e-{start_id}-{synthetic_id}", "source": start_id, "target": synthetic_id, - "data": {"mode": "always", "priority": 0, "transitionSpeech": ""}, + "data": {"mode": "always", "priority": 0}, } ) diff --git a/backend/services/workflow_engine.py b/backend/services/workflow_engine.py index d9bb96c..238b59f 100644 --- a/backend/services/workflow_engine.py +++ b/backend/services/workflow_engine.py @@ -113,14 +113,6 @@ class WorkflowEngine: return f"当满足以下条件时转到「{target}」:{condition}" return f"当当前阶段任务完成时转到「{target}」。" - def edge_transition_speech(self, edge: dict | None) -> str: - if not edge: - return "" - data = edge.get("data") or {} - return str( - data.get("transitionSpeech") or data.get("transition_speech") or "" - ) - def global_prompt(self) -> str: return str(self.settings.get("globalPrompt") or "").strip() diff --git a/backend/tests/test_brains.py b/backend/tests/test_brains.py index 90a1e32..2687d55 100644 --- a/backend/tests/test_brains.py +++ b/backend/tests/test_brains.py @@ -2393,7 +2393,6 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): "mode": "llm", "priority": 10, "condition": "需求已收集", - "transitionSpeech": "正在为你结束流程", }, } ], @@ -2569,29 +2568,6 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(call_end.ending) self.assertTrue(call_end.armed) self.assertTrue(any(getattr(frame, "text", "") == "感谢来电" for frame in queued)) - transition_context_frames = [ - frame - for frame in worker.frames - if isinstance(frame, LLMMessagesAppendFrame) - and frame.messages - == [ - { - "role": "system", - "content": ( - f"{FIXED_SPEECH_CONTEXT_MARKER}\n正在为你结束流程" - ), - } - ] - ] - self.assertTrue(transition_context_frames) - transition_events = [ - frame.message - for frame in queued - if isinstance(frame, OutputTransportMessageUrgentFrame) - and frame.message.get("source") == "workflow-edge-transition" - ] - self.assertEqual(transition_events[0]["content"], "正在为你结束流程") - self.assertEqual(transition_events[0]["nodeId"], "end") assistant_transcripts = [ frame.message.get("content") for frame in queued @@ -2601,11 +2577,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): ] self.assertEqual( assistant_transcripts, - ["正在为你结束流程", "感谢来电"], - ) - self.assertIn( - "正在为你结束流程", - brain._store.values["system__conversation_history"], + ["感谢来电"], ) self.assertIn( "感谢来电", diff --git a/backend/tests/test_pipeline_events.py b/backend/tests/test_pipeline_events.py index 8af5eb9..e7450a1 100644 --- a/backend/tests/test_pipeline_events.py +++ b/backend/tests/test_pipeline_events.py @@ -64,7 +64,7 @@ class _Brain: async def on_client_ready(self): for content, timestamp in ( - ("Start Edge 过渡语", "2026-07-14T10:00:00.200+00:00"), + ("Message 节点播报", "2026-07-14T10:00:00.200+00:00"), ("Agent 固定进入语", "2026-07-14T10:00:00.300+00:00"), ): await self.worker.queue_frame( @@ -118,7 +118,7 @@ class PipelineEventTest(unittest.IsolatedAsyncioTestCase): ordered = sorted(transcripts, key=lambda message: message["timestamp"]) self.assertEqual( [message["content"] for message in ordered], - ["助手开场白", "Start Edge 过渡语", "Agent 固定进入语"], + ["助手开场白", "Message 节点播报", "Agent 固定进入语"], ) self.assertEqual(transcripts[0]["timestamp"], greeting_time) self.assertEqual(brain.prepared_greeting, "助手开场白") diff --git a/backend/tests/test_workflow_v3.py b/backend/tests/test_workflow_v3.py index 4217b41..4d1b077 100644 --- a/backend/tests/test_workflow_v3.py +++ b/backend/tests/test_workflow_v3.py @@ -132,6 +132,17 @@ class WorkflowGraphTests(unittest.TestCase): self.assertEqual(cleaned_agent["data"]["entryMode"], "wait_user") self.assertNotIn("entrySpeech", cleaned_agent["data"]) + def test_edge_speech_fields_are_removed(self): + graph = valid_graph() + graph["edges"][0]["data"]["transitionSpeech"] = "不再播放" + graph["edges"][1]["data"]["transition_speech"] = "旧字段也不再播放" + + normalized = normalize_graph(graph) + + for edge in normalized["edges"]: + self.assertNotIn("transitionSpeech", edge["data"]) + self.assertNotIn("transition_speech", edge["data"]) + def test_action_defaults_preserve_legacy_result_assignment_behavior(self): graph = valid_graph() graph["nodes"].extend( diff --git a/frontend/src/components/assistant-editor/editor-controls.tsx b/frontend/src/components/assistant-editor/editor-controls.tsx index 7ed4dfe..4704071 100644 --- a/frontend/src/components/assistant-editor/editor-controls.tsx +++ b/frontend/src/components/assistant-editor/editor-controls.tsx @@ -93,12 +93,16 @@ export function EditableTitle({ placeholder = "未命名助手", editLabel = "助手名称", variant = "page", + allowEmpty = false, + maxLength, }: { value: string; onChange: (value: string) => void; placeholder?: string; editLabel?: string; variant?: "page" | "panel"; + allowEmpty?: boolean; + maxLength?: number; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value); @@ -118,7 +122,7 @@ export function EditableTitle({ function commit() { const next = draft.trim(); - if (next) { + if (next || allowEmpty) { onChange(next); } setEditing(false); @@ -129,6 +133,7 @@ export function EditableTitle({ setDraft(event.target.value)} onBlur={commit} onKeyDown={(event) => { diff --git a/frontend/src/components/workflow/WorkflowCanvas.tsx b/frontend/src/components/workflow/WorkflowCanvas.tsx index 11e6c0e..498ae48 100644 --- a/frontend/src/components/workflow/WorkflowCanvas.tsx +++ b/frontend/src/components/workflow/WorkflowCanvas.tsx @@ -53,6 +53,7 @@ import { type NodeSpecMap, type RuntimeNodeSpec, type WorkflowGraph, + type WorkflowEdgeData, type WorkflowNodeData, type WorkflowNodeType, } from "./specs"; @@ -396,7 +397,7 @@ export function WorkflowCanvas({ const updateEdgeData = useCallback( ( id: string, - patch: WorkflowGraph["edges"][number]["data"], + patch: Partial, ) => { setEdges((es) => es.map((e) => { @@ -535,6 +536,7 @@ export function WorkflowCanvas({ const editingNode = nodes.find((n) => n.id === editingNodeId); const editingSpec = editingNode ? specsByType[editingNode.type as string] : null; const editingEdge = edges.find((e) => e.id === editingEdgeId); + const editingEdgeData = editingEdge?.data as WorkflowEdgeData | undefined; const addableSpecs = Object.values(specsByType).filter((s) => s.addable); const canAddSpec = useCallback( (spec: RuntimeNodeSpec) => { @@ -683,7 +685,27 @@ export function WorkflowCanvas({ : settingsOpen ? "工作流设置" : editingEdge - ? "编辑连接条件" + ? ( + + updateEdgeData(editingEdge.id, { + label: label || undefined, + }) + } + /> + ) : editingNode && editingSpec ? ( void; + onChange: (patch: Partial) => void; }) { const data = (edge.data ?? { mode: "always", priority: 10 }) as WorkflowEdgeData; const [mode, setMode] = useState(data.mode ?? "always"); - const [priority, setPriority] = useState(data.priority ?? 10); - const [label, setLabel] = useState(data.label ?? ""); const [condition, setCondition] = useState(data.condition ?? ""); - const [transitionSpeech, setTransitionSpeech] = useState(data.transitionSpeech ?? ""); const [combinator, setCombinator] = useState<"and" | "or">( data.expression?.combinator ?? "and", ); @@ -57,33 +51,22 @@ export function EdgeSettingsPanel({ const publish = ({ nextMode = mode, - nextPriority = priority, - nextLabel = label, nextCondition = condition, - nextTransitionSpeech = transitionSpeech, nextCombinator = combinator, nextRules = rules, }: { nextMode?: WorkflowEdgeData["mode"]; - nextPriority?: number; - nextLabel?: string; nextCondition?: string; - nextTransitionSpeech?: string; nextCombinator?: "and" | "or"; nextRules?: ExpressionRule[]; }) => onChange({ mode: nextMode, - priority: nextPriority, - label: nextLabel.trim() ? nextLabel : undefined, condition: nextMode === "llm" ? nextCondition : undefined, expression: nextMode === "expression" ? { combinator: nextCombinator, rules: nextRules } : undefined, - transitionSpeech: nextTransitionSpeech.trim() - ? nextTransitionSpeech - : undefined, }); const setRule = (index: number, patch: Partial) => { @@ -94,24 +77,6 @@ export function EdgeSettingsPanel({ publish({ nextRules }); }; - const applyActionResultPreset = (status: "ok" | "error") => { - const nextRules: ExpressionRule[] = [ - { - variable: "system__last_action_status", - operator: "eq", - value: status, - }, - ]; - setMode("expression"); - setCombinator("and"); - setRules(nextRules); - publish({ - nextMode: "expression", - nextCombinator: "and", - nextRules, - }); - }; - const parseValue = (value: string): unknown => { if (value === "true") return true; if (value === "false") return false; @@ -159,82 +124,13 @@ export function EdgeSettingsPanel({ Agent 不能只有默认路径;请改为条件路径,或删除连接以持续对话。 )} - {sourceType === "action" && ( -
-
- Action 常用结果条件 -
-
- - -
-
- )} - {mode !== "always" && ( - - )} } title="触发条件" - description="配置画布标签以及这条连接被命中的条件" + description="配置这条连接被命中的条件" > - - {mode === "llm" && (