From 5e763637daf8e41c4a2ecf49dc24eca3e3d20248 Mon Sep 17 00:00:00 2001 From: v-zhangjc9 Date: Thu, 10 Jul 2025 12:08:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor(web):=20=E4=BC=98=E5=8C=96=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E8=8A=82=E7=82=B9=E7=9A=84=E5=AE=9A=E4=B9=89=E5=92=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/components/flow/FlowEditor.tsx | 217 +++---------- .../src/components/flow/node/AmisNode.tsx | 291 ++++++++++++------ .../src/components/flow/node/CodeNode.tsx | 92 +++--- .../components/flow/node/KnowledgeNode.tsx | 130 ++++---- .../src/components/flow/node/LlmNode.tsx | 90 +++--- .../src/components/flow/node/OutputNode.tsx | 20 +- .../src/components/flow/node/SwitchNode.tsx | 68 ++-- .../src/components/flow/store/ContextStore.ts | 4 +- .../src/components/flow/store/FlowStore.ts | 20 +- service-web/client/src/pages/Test.tsx | 48 +-- 10 files changed, 467 insertions(+), 513 deletions(-) diff --git a/service-web/client/src/components/flow/FlowEditor.tsx b/service-web/client/src/components/flow/FlowEditor.tsx index 30e907b..c10c53e 100644 --- a/service-web/client/src/components/flow/FlowEditor.tsx +++ b/service-web/client/src/components/flow/FlowEditor.tsx @@ -1,23 +1,12 @@ import {PlusCircleFilled, RollbackOutlined, SaveFilled} from '@ant-design/icons' -import { - Background, - BackgroundVariant, - Controls, - type Edge, - MiniMap, - type Node, - type NodeProps, - ReactFlow, -} from '@xyflow/react' -import type {Schema} from 'amis' -import {Button, Drawer, Dropdown, message, Popconfirm, Space} from 'antd' -import {arrToMap, find, isEqual, isNil, randomId} from 'licia' -import {type JSX, type MemoExoticComponent, useEffect, useState} from 'react' +import {Background, BackgroundVariant, Controls, type Edge, MiniMap, type Node, ReactFlow} from '@xyflow/react' +import {Button, Dropdown, message, Popconfirm, Space} from 'antd' +import {arrToMap, find, isEqual, randomId} from 'licia' +import {useEffect} from 'react' import {useNavigate} from 'react-router' import styled from 'styled-components' import '@xyflow/react/dist/style.css' -import {useShallow} from 'zustand/react/shallow' -import {amisRender, commonInfo, horizontalFormOptions} from '../../util/amis.tsx' +import {commonInfo} from '../../util/amis.tsx' import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx' import CodeNode from './node/CodeNode.tsx' import KnowledgeNode from './node/KnowledgeNode.tsx' @@ -29,8 +18,6 @@ import {useDataStore} from './store/DataStore.ts' import {useFlowStore} from './store/FlowStore.ts' const FlowableDiv = styled.div` - height: 100%; - .react-flow__node.selectable { &:focus { box-shadow: 0 0 20px 1px #e8e8e8; @@ -65,6 +52,34 @@ const FlowableDiv = styled.div` } ` +const nodeDefine = [ + { + key: 'output-node', + name: '输出', + component: OutputNode, + }, + { + key: 'llm-node', + name: '大模型', + component: LlmNode, + }, + { + key: 'knowledge-node', + name: '知识库', + component: KnowledgeNode, + }, + { + key: 'code-node', + name: '代码执行', + component: CodeNode, + }, + { + key: 'switch-node', + name: '条件分支', + component: SwitchNode, + }, +] + export type GraphData = { nodes: Node[], edges: Edge[], data: any } export type FlowEditorProps = { @@ -76,47 +91,8 @@ export type FlowEditorProps = { function FlowEditor(props: FlowEditorProps) { const navigate = useNavigate() const [messageApi, contextHolder] = message.useMessage() - const [nodeDef] = useState<{ - key: string, - name: string, - component: MemoExoticComponent<(props: NodeProps) => JSX.Element> - }[]>([ - { - key: 'output-node', - name: '输出', - component: OutputNode, - }, - { - key: 'llm-node', - name: '大模型', - component: LlmNode, - }, - { - key: 'knowledge-node', - name: '知识库', - component: KnowledgeNode, - }, - { - key: 'code-node', - name: '代码执行', - component: CodeNode, - }, - { - key: 'switch-node', - name: '条件分支', - component: SwitchNode, - }, - ]) - const [open, setOpen] = useState(false) - const {data, setData, getDataById, setDataById} = useDataStore( - useShallow(state => ({ - data: state.data, - setData: state.setData, - getDataById: state.getDataById, - setDataById: state.setDataById, - })), - ) + const {data, setData} = useDataStore() const { nodes, addNode, @@ -126,106 +102,9 @@ function FlowEditor(props: FlowEditorProps) { setEdges, onEdgesChange, onConnect, - } = useFlowStore( - useShallow(state => ({ - nodes: state.nodes, - getNodes: state.getNodes, - addNode: state.addNode, - removeNode: state.removeNode, - setNodes: state.setNodes, - onNodesChange: state.onNodesChange, - edges: state.edges, - getEdges: state.getEdges, - setEdges: state.setEdges, - onEdgesChange: state.onEdgesChange, - onConnect: state.onConnect, - })), - ) + } = useFlowStore() - const { - setInputSchema, - } = useContextStore() - - const [currentNodeForm, setCurrentNodeForm] = useState() - const editNode = (id: string, columnSchema?: Schema[]) => { - if (!isNil(columnSchema)) { - setCurrentNodeForm( - amisRender( - { - type: 'wrapper', - size: 'none', - body: [ - { - debug: commonInfo.debug, - type: 'form', - ...horizontalFormOptions(), - wrapWithPanel: false, - onEvent: { - submitSucc: { - actions: [ - { - actionType: 'custom', - // @ts-ignore - script: (context, action, event) => { - setDataById( - id, - { - ...context.props.data, - finished: true, - }, - ) - setOpen(false) - }, - }, - ], - }, - }, - body: [ - ...(columnSchema ?? []), - { - type: 'wrapper', - size: 'none', - className: 'space-x-2 text-right', - body: [ - { - type: 'action', - label: '取消', - onEvent: { - click: { - actions: [ - { - actionType: 'custom', - // @ts-ignore - script: (context, action, event) => { - setOpen(false) - }, - }, - ], - }, - }, - }, - { - type: 'submit', - label: '保存', - level: 'primary', - }, - ], - }, - ], - }, - ], - }, - getDataById(id), - ), - ) - setOpen(true) - } - } - - // 用于透传node操作到主流程 - const initialNodeHandlers = { - editNode, - } + const {setInputSchema} = useContextStore() useEffect(() => { // language=JSON @@ -236,10 +115,6 @@ function FlowEditor(props: FlowEditorProps) { let initialNodeData = props.graphData?.data ?? {} setData(initialNodeData) - - for (let node of initialNodes) { - node.data = initialNodeHandlers - } setNodes(initialNodes) setEdges(initialEdges) @@ -247,12 +122,12 @@ function FlowEditor(props: FlowEditorProps) { }, [props.graphData]) return ( - + {contextHolder} ({key: def.key, label: def.name})), + items: nodeDefine.map(def => ({key: def.key, label: def.name})), onClick: ({key}) => { try { if (commonInfo.debug) { @@ -263,7 +138,7 @@ function FlowEditor(props: FlowEditorProps) { id: randomId(10), type: key, position: {x: 100, y: 100}, - data: initialNodeHandlers, + data: {}, }) } catch (e) { // @ts-ignore @@ -303,16 +178,6 @@ function FlowEditor(props: FlowEditorProps) { 保存 - - {currentNodeForm} - def.key), - key => find(nodeDef, def => isEqual(key, def.key))!.component) + nodeDefine.map(def => def.key), + key => find(nodeDefine, def => isEqual(key, def.key))!.component) } > diff --git a/service-web/client/src/components/flow/node/AmisNode.tsx b/service-web/client/src/components/flow/node/AmisNode.tsx index 0939826..0567eb7 100644 --- a/service-web/client/src/components/flow/node/AmisNode.tsx +++ b/service-web/client/src/components/flow/node/AmisNode.tsx @@ -1,33 +1,78 @@ import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons' -import {Handle, type HandleProps, type Node, type NodeProps, Position, useNodeConnections} from '@xyflow/react' +import {type Edge, getIncomers, Handle, type Node, type NodeProps, Position} from '@xyflow/react' import type {Schema} from 'amis' -import {Button, Card} from 'antd' -import {has, isEmpty, isEqual, isNil} from 'licia' -import {type JSX} from 'react' +import {Button, Card, Drawer} from 'antd' +import {find, has, isEmpty, isEqual, unique} from 'licia' +import {type JSX, useCallback, useState} from 'react' import styled from 'styled-components' -import {horizontalFormOptions} from '../../../util/amis.tsx' -import {useContextStore} from '../store/ContextStore.ts' +import Queue from 'yocto-queue' +import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx' import {useDataStore} from '../store/DataStore.ts' import {useFlowStore} from '../store/FlowStore.ts' -export type AmisNodeType = 'normal' | 'start' | 'end' +export type InputFormOptions = { + label: string + value: string +} -export function inputsFormColumns(props: ColumnsSchemaProps): Schema[] { - let incomers = props.getAllIncomerNodeById(props.nodeId) - let groups = [] - for (const incomer of incomers) { - let data = props.getNodeDataById(incomer.id) - if (has(data, 'outputs')) { - let outputs = data?.outputs ?? [] - groups.push({ - label: incomer.id, - children: Object.keys(outputs).map(key => ({ - value: `${incomer.id}.${key}`, +export type InputFormOptionsGroup = { + group: string, + variables: InputFormOptions[], +} + +const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => { + let queue = new Queue() + queue.enqueue(find(nodes, node => isEqual(node.id, id))!) + let result: string[] = [] + while (queue.size !== 0) { + let currentNode = queue.dequeue()! + for (const incomer of getIncomers(currentNode, nodes, edges)) { + result.push(incomer.id) + queue.enqueue(incomer) + } + } + return unique(result, (a, b) => isEqual(a, b)) +} + +export function inputsFormColumns( + nodeId: string, + inputSchema: Record>, + nodes: Node[], + edges: Edge[], + data: any, +): Schema[] { + let inputSchemaVariables: InputFormOptions[] = Object.keys(inputSchema).map(key => ({ + label: inputSchema[key]?.label ?? '', + value: key, + })) + + let incomerIds = getAllIncomerNodeById(nodeId, nodes, edges) + console.log(incomerIds, nodes, edges) + let incomerVariables: InputFormOptionsGroup[] = [] + for (const incomerId of incomerIds) { + let nodeData = data[incomerId] ?? {} + if (has(nodeData, 'outputs')) { + let outputs = nodeData?.outputs ?? [] + incomerVariables.push({ + group: incomerId, + variables: Object.keys(outputs).map(key => ({ + value: `${incomerId}.${key}`, label: key, })), }) } } + + let inputVariables = [ + ...(isEmpty(inputSchemaVariables) ? [] : [ + { + group: '流程入参', + variables: inputSchemaVariables, + }, + ]), + ...incomerVariables, + ] + return [ { type: 'input-kvs', @@ -48,14 +93,10 @@ export function inputsFormColumns(props: ColumnsSchemaProps): Schema[] { required: true, selectMode: 'group', options: [ - { - label: '流程参数', - children: Object.keys(props.inputSchema).map(key => ({ - label: props.inputSchema[key]?.label ?? '', - value: key, - })), - }, - ...groups, + ...inputVariables.map(item => ({ + label: item.group, + children: item.variables, + })), ], }, ], @@ -110,35 +151,13 @@ export function outputsFormColumns(editable: boolean = false, required: boolean ] } -export const LimitHandler = (props: HandleProps & { limit: number }) => { - const connections = useNodeConnections({ - handleType: props.type, - }) - return ( - - ) -} - -type ColumnsSchemaProps = { - // getInputSchema: () => Record>, - inputSchema: Record>, - nodeId: string, - nodeData: any, - getNodeDataById: (id: string) => any, - getAllIncomerNodeById: (id: string) => Node[], -} - type AmisNodeProps = { nodeProps: NodeProps - type: AmisNodeType defaultNodeName: String defaultNodeDescription?: String extraNodeDescription?: (nodeData: any) => JSX.Element - handlers?: (nodeData: any) => JSX.Element - columnSchema?: (props: ColumnsSchemaProps) => Schema[] + handler: JSX.Element + columnSchema?: () => Schema[] } const AmisNodeContainerDiv = styled.div` @@ -151,30 +170,140 @@ const AmisNodeContainerDiv = styled.div` } ` +export const StartNodeHandler = () => { + return +} + +export const EndNodeHandler = () => { + return +} + +export const NormalNodeHandler = () => { + return ( + <> + + + + ) +} + const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ nodeProps, - type, defaultNodeName, defaultNodeDescription, extraNodeDescription, - handlers, + handler, columnSchema, }) => { - const { - removeNode, - getAllIncomerNodeById, - } = useFlowStore() - const {getDataById} = useDataStore() - const {inputSchema} = useContextStore() - - const {id, data} = nodeProps - const {editNode} = data + const {removeNode} = useFlowStore() + const {getDataById, setDataById} = useDataStore() + const {id} = nodeProps // @ts-ignore const nodeData = getDataById(id) const nodeName = isEmpty(nodeData?.node?.name) ? defaultNodeName : nodeData.node.name const nodeDescription = isEmpty(nodeData?.node?.description) ? defaultNodeDescription : nodeData.node?.description + + const [editDrawerOpen, setEditDrawerOpen] = useState(false) + const [editDrawerForm, setEditDrawerForm] = useState(<>) + const openEditDrawer = useCallback(() => { + setEditDrawerForm( + amisRender( + { + type: 'wrapper', + size: 'none', + body: [ + { + debug: commonInfo.debug, + type: 'form', + ...horizontalFormOptions(), + wrapWithPanel: false, + onEvent: { + submitSucc: { + actions: [ + { + actionType: 'custom', + // @ts-ignore + script: (context, action, event) => { + setDataById( + id, + { + ...context.props.data, + finished: true, + }, + ) + setEditDrawerOpen(false) + }, + }, + ], + }, + }, + body: [ + { + type: 'input-text', + name: 'node.name', + label: '节点名称', + placeholder: nodeName, + }, + { + type: 'textarea', + name: 'node.description', + label: '节点描述', + placeholder: nodeDescription, + }, + { + type: 'divider', + }, + ...(columnSchema?.() ?? []), + { + type: 'wrapper', + size: 'none', + className: 'space-x-2 text-right', + body: [ + { + type: 'action', + label: '取消', + onEvent: { + click: { + actions: [ + { + actionType: 'custom', + // @ts-ignore + script: (context, action, event) => { + setEditDrawerOpen(false) + }, + }, + ], + }, + }, + }, + { + type: 'submit', + label: '保存', + level: 'primary', + }, + ], + }, + ], + }, + ], + }, + getDataById(id), + ), + ) + setEditDrawerOpen(true) + }, [nodeData]) return ( + + {editDrawerForm} + JSX.Element = ({ size="small" icon={} block - // @ts-ignore - onClick={() => editNode( - id, - [ - { - type: 'input-text', - name: 'node.name', - label: '节点名称', - placeholder: nodeName, - }, - { - type: 'textarea', - name: 'node.description', - label: '节点描述', - placeholder: nodeDescription, - }, - { - type: 'divider', - }, - ...( - columnSchema?.({ - inputSchema, - nodeId: id, - nodeData, - getNodeDataById: getDataById, - getAllIncomerNodeById, - }) ?? [] - ), - ], - )} + onClick={() => openEditDrawer()} />,