From a3c22502850cd0130aab998fa9a71c7dbb4dd6a3 Mon Sep 17 00:00:00 2001 From: v-zhangjc9 Date: Wed, 16 Jul 2025 19:54:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=AE=8C=E6=88=90=E5=88=86?= =?UTF-8?q?=E6=94=AF=E8=8A=82=E7=82=B9=E5=8A=A8=E6=80=81=E6=94=B9=E5=8F=98?= =?UTF-8?q?handle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/components/flow/FlowEditor.tsx | 6 +- .../src/components/flow/node/AmisNode.tsx | 6 +- .../src/components/flow/node/CodeNode.tsx | 23 +++-- .../components/flow/node/KnowledgeNode.tsx | 4 +- .../src/components/flow/node/LlmNode.tsx | 23 +++-- .../src/components/flow/node/LoopNode.tsx | 42 +++++---- .../src/components/flow/node/OutputNode.tsx | 4 +- .../src/components/flow/node/SwitchNode.tsx | 93 +++++++++++-------- .../src/components/flow/node/TemplateNode.tsx | 26 +++--- .../src/components/flow/store/FlowStore.ts | 14 ++- service-web/client/src/pages/Test.tsx | 2 +- 11 files changed, 138 insertions(+), 105 deletions(-) diff --git a/service-web/client/src/components/flow/FlowEditor.tsx b/service-web/client/src/components/flow/FlowEditor.tsx index 5ec1872..27a1b9c 100644 --- a/service-web/client/src/components/flow/FlowEditor.tsx +++ b/service-web/client/src/components/flow/FlowEditor.tsx @@ -7,6 +7,7 @@ import {useNavigate} from 'react-router' import styled from 'styled-components' import '@xyflow/react/dist/style.css' import {commonInfo} from '../../util/amis.tsx' +import AddNodeButton from './component/AddNodeButton.tsx' import {checkAddConnection, checkSave} from './FlowChecker.tsx' import {useNodeDrag} from './Helper.tsx' import {NodeRegistryMap} from './NodeRegistry.tsx' @@ -14,7 +15,6 @@ import {useContextStore} from './store/ContextStore.ts' import {useDataStore} from './store/DataStore.ts' import {useFlowStore} from './store/FlowStore.ts' import {flowDotColor, type FlowEditorProps} from './types.ts' -import AddNodeButton from './component/AddNodeButton.tsx' const FlowableDiv = styled.div` .react-flow__node.selectable { @@ -48,10 +48,9 @@ function FlowEditor(props: FlowEditorProps) { const navigate = useNavigate() const [messageApi, contextHolder] = message.useMessage() - const {data, setData, setDataById} = useDataStore() + const {data, setData} = useDataStore() const { nodes, - addNode, setNodes, onNodesChange, edges, @@ -109,6 +108,7 @@ function FlowEditor(props: FlowEditorProps) { onNodeDragStart={onNodeDragStart} onNodeDrag={onNodeDrag} onNodeDragStop={onNodeDragEnd} + onEdgesDelete={() => console.info('delete')} > diff --git a/service-web/client/src/components/flow/node/AmisNode.tsx b/service-web/client/src/components/flow/node/AmisNode.tsx index a4227b7..c7b6706 100644 --- a/service-web/client/src/components/flow/node/AmisNode.tsx +++ b/service-web/client/src/components/flow/node/AmisNode.tsx @@ -88,7 +88,7 @@ type AmisNodeProps = { nodeProps: NodeProps extraNodeDescription?: JSX.Element handler: JSX.Element - columnSchema?: () => Schema[] + columnSchema?: Schema[] resize?: { minWidth: number, minHeight: number } } @@ -183,7 +183,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ { type: 'divider', }, - ...(columnSchema?.() ?? []), + ...(columnSchema ?? []), { type: 'wrapper', size: 'none', @@ -221,7 +221,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ ), ) setEditDrawerOpen(true) - }, [nodeData]) + }, [id]) const onRemoveClick = useCallback(() => { removeNode(id) removeDataById(id) diff --git a/service-web/client/src/components/flow/node/CodeNode.tsx b/service-web/client/src/components/flow/node/CodeNode.tsx index fc9a6ed..fc2f0b7 100644 --- a/service-web/client/src/components/flow/node/CodeNode.tsx +++ b/service-web/client/src/components/flow/node/CodeNode.tsx @@ -1,6 +1,6 @@ import type {NodeProps} from '@xyflow/react' import {Tag} from 'antd' -import React, {useCallback} from 'react' +import React, {useMemo} from 'react' import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' @@ -19,7 +19,7 @@ const CodeNode = (props: NodeProps) => { const nodeData = getDataById(props.id) - const columnsSchema = useCallback(() => [ + const columnsSchema = useMemo(() => [ ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), { type: 'divider', @@ -47,18 +47,21 @@ const CodeNode = (props: NodeProps) => { }, ...outputsFormColumns(true, true), ], [props.id]) + + const extraNodeDescription = useMemo(() => { + return nodeData?.type + ?
+ 代码类型 + {languageMap[nodeData.type]} +
+ : <> + }, [nodeData]) + return ( - 代码类型 - {languageMap[nodeData.type]} - - : <> - } + extraNodeDescription={extraNodeDescription} columnSchema={columnsSchema} handler={} /> diff --git a/service-web/client/src/components/flow/node/KnowledgeNode.tsx b/service-web/client/src/components/flow/node/KnowledgeNode.tsx index 00892c5..e845fdd 100644 --- a/service-web/client/src/components/flow/node/KnowledgeNode.tsx +++ b/service-web/client/src/components/flow/node/KnowledgeNode.tsx @@ -1,5 +1,5 @@ import type {NodeProps} from '@xyflow/react' -import React, {useCallback, useEffect} from 'react' +import React, {useEffect, useMemo} from 'react' import {commonInfo} from '../../../util/amis.tsx' import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' @@ -24,7 +24,7 @@ const KnowledgeNode = (props: NodeProps) => { ) }, [props.id]) - const columnsSchema = useCallback(() => [ + const columnsSchema = useMemo(() => [ ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), { type: 'divider', diff --git a/service-web/client/src/components/flow/node/LlmNode.tsx b/service-web/client/src/components/flow/node/LlmNode.tsx index de4adb8..ea48c41 100644 --- a/service-web/client/src/components/flow/node/LlmNode.tsx +++ b/service-web/client/src/components/flow/node/LlmNode.tsx @@ -1,6 +1,6 @@ import type {NodeProps} from '@xyflow/react' import {Tag} from 'antd' -import React, {useCallback, useEffect} from 'react' +import React, {useEffect, useMemo} from 'react' import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' @@ -31,7 +31,7 @@ const LlmNode = (props: NodeProps) => { ) }, [props.id]) - const columnsSchema = useCallback(() => [ + const columnsSchema = useMemo(() => [ ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), { type: 'divider', @@ -55,18 +55,21 @@ const LlmNode = (props: NodeProps) => { }, ...outputsFormColumns(false, true), ], [props.id]) + + const extraNodeDescription = useMemo(() => { + return nodeData?.model + ?
+ 模型名称 + {modelMap[nodeData.model]} +
+ : <> + }, [nodeData]) + return ( - 模型名称 - {modelMap[nodeData.model]} - - : <> - } + extraNodeDescription={extraNodeDescription} columnSchema={columnsSchema} handler={} /> diff --git a/service-web/client/src/components/flow/node/LoopNode.tsx b/service-web/client/src/components/flow/node/LoopNode.tsx index 9a853c5..8b96937 100644 --- a/service-web/client/src/components/flow/node/LoopNode.tsx +++ b/service-web/client/src/components/flow/node/LoopNode.tsx @@ -1,6 +1,6 @@ import {Background, BackgroundVariant, type NodeProps} from '@xyflow/react' import {classnames} from 'amis' -import React, {useCallback, useEffect} from 'react' +import React, {useEffect, useMemo} from 'react' import AddNodeButton from '../component/AddNodeButton.tsx' import {useDataStore} from '../store/DataStore.ts' import {flowBackgroundColor, flowDotColor} from '../types.ts' @@ -22,7 +22,7 @@ const LoopNode = (props: NodeProps) => { ) }, [props.id]) - const columnsSchema = useCallback(() => [ + const columnsSchema = useMemo(() => [ { type: 'switch', name: 'failFast', @@ -41,6 +41,26 @@ const LoopNode = (props: NodeProps) => { ...outputsFormColumns(false, true), ], [props.id]) + const extraNodeDescription = useMemo(() => { + return ( +
+ + +
+ ) + }, [props.id]) + return ( { minHeight: '290px', }} nodeProps={props} - extraNodeDescription={ -
- - -
- } + extraNodeDescription={extraNodeDescription} columnSchema={columnsSchema} handler={} resize={{ diff --git a/service-web/client/src/components/flow/node/OutputNode.tsx b/service-web/client/src/components/flow/node/OutputNode.tsx index 61951b7..3441e35 100644 --- a/service-web/client/src/components/flow/node/OutputNode.tsx +++ b/service-web/client/src/components/flow/node/OutputNode.tsx @@ -1,5 +1,5 @@ import type {NodeProps} from '@xyflow/react' -import React, {useCallback} from 'react' +import React, {useMemo} from 'react' import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx' import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' @@ -11,7 +11,7 @@ const OutputNode = (props: NodeProps) => { const {getData} = useDataStore() const {getInputSchema} = useContextStore() - const columnsSchema = useCallback( + const columnsSchema = useMemo( () => [ { type: 'select', diff --git a/service-web/client/src/components/flow/node/SwitchNode.tsx b/service-web/client/src/components/flow/node/SwitchNode.tsx index d105f2d..87f07c1 100644 --- a/service-web/client/src/components/flow/node/SwitchNode.tsx +++ b/service-web/client/src/components/flow/node/SwitchNode.tsx @@ -1,30 +1,24 @@ import {Handle, type NodeProps, Position} from '@xyflow/react' +import type {ConditionValue} from 'amis' import {Tag} from 'antd' -import React, {useCallback} from 'react' +import {contain, isEqual} from 'licia' +import React, {useMemo} from 'react' import {generateAllIncomerOutputVariablesConditions} from '../Helper.tsx' import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' import AmisNode, {nodeClassName} from './AmisNode.tsx' -const cases = [ - { - index: 1, - }, - { - index: 2, - }, - { - index: 3, - }, -] - const SwitchNode = (props: NodeProps) => { - const {getNodes, getEdges} = useFlowStore() - const {getData} = useDataStore() + const {getNodes, getEdges, removeEdges} = useFlowStore() + const {getData, getDataById} = useDataStore() const {getInputSchema} = useContextStore() - const columnsSchema = useCallback(() => [ + const nodeData = getDataById(props.id) + // @ts-ignore + const conditions: ConditionValue[] = nodeData?.conditions?.map(c => c.condition) ?? [] + + const columnsSchema = useMemo(() => [ { type: 'combo', name: 'conditions', @@ -45,38 +39,55 @@ const SwitchNode = (props: NodeProps) => { getData(), ), }, - ] - } + ], + }, ], [props.id]) + const extraNodeDescription = useMemo(() => { + return ( +
+ {conditions.map((item, index) => ( +
+ 分支 {index + 1} +
+ ))} +
+ ) + }, [nodeData]) + + const handler = useMemo(() => { + // @ts-ignore + const conditions: ConditionValue[] = nodeData?.conditions?.map(c => c.condition) ?? [] + + // 移除不该存在的边 + const conditionIds = conditions.map(c => c.id) + const removeEdgeIds = getEdges() + .filter(edge => isEqual(edge.source, props.id) && !contain(conditionIds, edge.sourceHandle)) + .map(edge => edge.id) + removeEdges(removeEdgeIds) + return ( + <> + + {conditions.map((item, index) => ( + + ))} + + ) + }, [nodeData]) + return ( - {cases.map(item => ( -
- 分支 {item.index} -
- ))} - - } + extraNodeDescription={extraNodeDescription} columnSchema={columnsSchema} - handler={ - <> - - {cases.map((item, index) => ( - - ))} - - } + handler={handler} /> ) } diff --git a/service-web/client/src/components/flow/node/TemplateNode.tsx b/service-web/client/src/components/flow/node/TemplateNode.tsx index 14ad09a..28552ae 100644 --- a/service-web/client/src/components/flow/node/TemplateNode.tsx +++ b/service-web/client/src/components/flow/node/TemplateNode.tsx @@ -1,6 +1,6 @@ import type {NodeProps} from '@xyflow/react' import {Tag} from 'antd' -import React, {useCallback, useEffect} from 'react' +import React, {useEffect, useMemo} from 'react' import {useContextStore} from '../store/ContextStore.ts' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' @@ -33,7 +33,7 @@ const TemplateNode = (props: NodeProps) => { ) }, [props.id]) - const columnsSchema = useCallback( + const columnsSchema = useMemo( () => [ ...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()), { @@ -69,22 +69,22 @@ const TemplateNode = (props: NodeProps) => { }, }, ...outputsFormColumns(false, true), - ], - [props.id], - ) + ], [props.id]) + + const extraNodeDescription = useMemo(() => { + return nodeData?.type + ?
+ 模板类型 + {typeMap[nodeData.type]} +
+ : <> + }, [nodeData]) return ( - 模板类型 - {typeMap[nodeData.type]} - - : <> - } + extraNodeDescription={extraNodeDescription} columnSchema={columnsSchema} handler={} /> diff --git a/service-web/client/src/components/flow/store/FlowStore.ts b/service-web/client/src/components/flow/store/FlowStore.ts index a51ab0c..f6fc502 100644 --- a/service-web/client/src/components/flow/store/FlowStore.ts +++ b/service-web/client/src/components/flow/store/FlowStore.ts @@ -8,7 +8,7 @@ import { type OnEdgesChange, type OnNodesChange, } from '@xyflow/react' -import {filter, find, isEqual} from 'licia' +import {contain, filter, find, isEqual} from 'licia' import {create} from 'zustand/react' export const useFlowStore = create<{ @@ -24,6 +24,8 @@ export const useFlowStore = create<{ edges: Edge[], getEdges: () => Edge[], onEdgesChange: OnEdgesChange, + removeEdge: (id: string) => void, + removeEdges: (ids: string[]) => void, setEdges: (edges: Edge[]) => void, onConnect: OnConnect, @@ -61,6 +63,16 @@ export const useFlowStore = create<{ edges: applyEdgeChanges(changes, get().edges), }) }, + removeEdge: id => { + set({ + edges: filter(get().edges, edge => !isEqual(edge.id, id)), + }) + }, + removeEdges: ids => { + set({ + edges: filter(get().edges, edge => !contain(ids, edge.id)), + }) + }, setEdges: edges => set({edges}), onConnect: connection => { diff --git a/service-web/client/src/pages/Test.tsx b/service-web/client/src/pages/Test.tsx index 62172ab..f5c9c89 100644 --- a/service-web/client/src/pages/Test.tsx +++ b/service-web/client/src/pages/Test.tsx @@ -4,7 +4,7 @@ import type {GraphData} from '../components/flow/types.ts' function Test() { // language=JSON - const [graphData] = useState(JSON.parse('{\n "nodes": [\n {\n "id": "QxNrkChBWQ",\n "type": "loop-node",\n "position": {\n "x": 742,\n "y": 119\n },\n "data": {},\n "measured": {\n "width": 458,\n "height": 368\n },\n "selected": false,\n "dragging": false,\n "width": 458,\n "height": 368,\n "resizing": false\n },\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 135\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 108\n },\n "selected": false,\n "dragging": false,\n "extent": "parent",\n "parentId": "QxNrkChBWQ"\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 100,\n "y": 188\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 108\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "JsUwvjkJCW",\n "type": "switch-node",\n "position": {\n "x": 400,\n "y": 267\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 159\n },\n "selected": true,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "bivXSpiLaI",\n "sourceHandle": "source",\n "target": "JsUwvjkJCW",\n "id": "xy-edge__bivXSpiLaIsource-JsUwvjkJCW"\n }\n ],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "text"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "text": {\n "type": "text"\n },\n "condition": {\n "type": "boolean"\n },\n "count": {\n "type": "number"\n },\n "person": {\n "type": "object"\n },\n "words": {\n "type": "array-text"\n },\n "people": {\n "type": "array-object"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n },\n "QxNrkChBWQ": {\n "node": {\n "name": "循环",\n "description": "实现循环执行流程"\n },\n "finished": true\n },\n "JsUwvjkJCW": {\n "node": {\n "name": "分支",\n "description": "根据不同的情况前往不同的分支"\n }\n }\n }\n}')) + const [graphData] = useState(JSON.parse('{"nodes":[{"id":"QxNrkChBWQ","type":"loop-node","position":{"x":890,"y":119},"data":{},"measured":{"width":458,"height":368},"selected":false,"dragging":false,"width":458,"height":368,"resizing":false},{"id":"MzEitlOusl","type":"llm-node","position":{"x":47,"y":135},"data":{},"measured":{"width":256,"height":110},"selected":false,"dragging":false,"extent":"parent","parentId":"QxNrkChBWQ"},{"id":"bivXSpiLaI","type":"code-node","position":{"x":100,"y":188},"data":{},"measured":{"width":256,"height":110},"selected":false,"dragging":false},{"id":"JsUwvjkJCW","type":"switch-node","position":{"x":495,"y":219},"data":{},"measured":{"width":256,"height":162},"selected":false,"dragging":false},{"id":"sRWQqqshAE","type":"llm-node","position":{"x":938,"y":551},"data":{},"measured":{"width":256,"height":110},"selected":false,"dragging":false}],"edges":[{"source":"bivXSpiLaI","sourceHandle":"source","target":"JsUwvjkJCW","id":"xy-edge__bivXSpiLaIsource-JsUwvjkJCW"},{"source":"JsUwvjkJCW","sourceHandle":"736a724a5de4","target":"QxNrkChBWQ","targetHandle":"target","id":"xy-edge__JsUwvjkJCW736a724a5de4-QxNrkChBWQtarget"},{"source":"JsUwvjkJCW","sourceHandle":"d4e42668119c","target":"sRWQqqshAE","targetHandle":"target","id":"xy-edge__JsUwvjkJCWd4e42668119c-sRWQqqshAEtarget"}],"data":{"MzEitlOusl":{"node":{"name":"大模型","description":"使用大模型对话"},"outputs":{"text":{"type":"text"}},"model":"qwen3","systemPrompt":"你是个好人","finished":true},"bivXSpiLaI":{"node":{"name":"代码执行","description":"执行自定义的处理代码"},"outputs":{"text":{"type":"text"},"condition":{"type":"boolean"},"count":{"type":"number"},"person":{"type":"object"},"words":{"type":"array-text"},"people":{"type":"array-object"}},"type":"javascript","content":"console.log(\'hello\')","inputs":{"text":{"variable":"MzEitlOusl.text"}},"finished":true},"QxNrkChBWQ":{"node":{"name":"循环","description":"实现循环执行流程"},"finished":true,"outputs":{"output":{"type":"array-object"}}},"JsUwvjkJCW":{"node":{"name":"分支","description":"根据不同的情况前往不同的分支"},"conditions":[{"condition":{"id":"736a724a5de4","conjunction":"and","children":[{"id":"1db9e7a90aae","left":{"type":"field","field":"bivXSpiLaI.text"},"op":"equal","right":"1"},{"id":"98cc5c39eed4","left":{"type":"field","field":"bivXSpiLaI.condition"},"op":"is_true"},{"id":"1cbccc438a64","left":{"type":"field","field":"bivXSpiLaI.text"},"op":"equal","right":"2"}]}},{"condition":{"id":"406bf6637c64","conjunction":"and","children":[{"id":"ea37e95e6d1b","left":{"type":"field","field":"bivXSpiLaI.text"},"op":"equal","right":"3"}]}},{"condition":{"id":"d4e42668119c","conjunction":"and","children":[{"id":"62618f0083ec","left":{"type":"field","field":"bivXSpiLaI.count"},"op":"equal","right":2}]}}],"finished":true},"sRWQqqshAE":{"node":{"name":"大模型","description":"使用大模型对话"},"outputs":{"text":{"type":"text"}},"model":"deepseek","systemPrompt":"Hello","finished":true}}}')) return (