From 67f41c08a03c81db1d00d43f5db32ed5bb99a2b9 Mon Sep 17 00:00:00 2001 From: v-zhangjc9 Date: Mon, 30 Jun 2025 10:41:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E4=BC=98=E5=8C=96=E5=86=97?= =?UTF-8?q?=E4=BD=99=E8=BE=B9=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/ai/flow/FlowChecker.test.tsx | 10 +-- .../client/src/pages/ai/flow/FlowChecker.tsx | 75 +++++++++---------- .../client/src/pages/ai/flow/FlowEditor.tsx | 8 +- .../src/pages/ai/flow/node/AmisNode.tsx | 4 +- 4 files changed, 47 insertions(+), 50 deletions(-) diff --git a/service-web/client/src/pages/ai/flow/FlowChecker.test.tsx b/service-web/client/src/pages/ai/flow/FlowChecker.test.tsx index e0542f1..3594546 100644 --- a/service-web/client/src/pages/ai/flow/FlowChecker.test.tsx +++ b/service-web/client/src/pages/ai/flow/FlowChecker.test.tsx @@ -1,5 +1,6 @@ -import {expect, test} from 'vitest' import {type Connection, type Node} from '@xyflow/react' +import {uuid} from 'licia' +import {expect, test} from 'vitest' import { atLeastOneEndNodeError, atLeastOneStartNodeError, @@ -13,9 +14,8 @@ import { nodeToSelfError, sourceNodeNotFoundError, startNodeToEndNodeError, - targetNodeNotFoundError + targetNodeNotFoundError, } from './FlowChecker.tsx' -import {uuid} from 'licia' const createNode = (id: string, type: string): Node => { return { @@ -97,9 +97,9 @@ test(hasRedundantEdgeError().message, () => { const { nodes, edges, - } = JSON.parse('{\n "nodes": [\n {\n "id": "ldoKAzHnKF",\n "type": "llm-node",\n "position": {\n "x": 207,\n "y": -38\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "1eJtMoJWs6",\n "type": "llm-node",\n "position": {\n "x": 207,\n "y": 172.5\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "7e5vQLDGTl",\n "type": "start-node",\n "position": {\n "x": -162.3520537805597,\n "y": 67.84901301708827\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "Wyqg_bXILg",\n "type": "knowledge-node",\n "position": {\n "x": 560.402133595296,\n "y": -38.892263766178665\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "7DaF-0G-yv",\n "type": "llm-node",\n "position": {\n "x": 634.9924233956513,\n "y": 172.01821084172227\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "mymIbw_W6k",\n "type": "end-node",\n "position": {\n "x": 953.9302142661356,\n "y": 172.0182108417223\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "7e5vQLDGTl",\n "target": "ldoKAzHnKF",\n "id": "xy-edge__7e5vQLDGTl-ldoKAzHnKF"\n },\n {\n "source": "ldoKAzHnKF",\n "target": "Wyqg_bXILg",\n "id": "xy-edge__ldoKAzHnKF-Wyqg_bXILg"\n },\n {\n "source": "7e5vQLDGTl",\n "target": "1eJtMoJWs6",\n "id": "xy-edge__7e5vQLDGTl-1eJtMoJWs6"\n },\n {\n "source": "Wyqg_bXILg",\n "target": "7DaF-0G-yv",\n "id": "xy-edge__Wyqg_bXILg-7DaF-0G-yv"\n },\n {\n "source": "1eJtMoJWs6",\n "target": "7DaF-0G-yv",\n "id": "xy-edge__1eJtMoJWs6-7DaF-0G-yv"\n },\n {\n "source": "7DaF-0G-yv",\n "target": "mymIbw_W6k",\n "id": "xy-edge__7DaF-0G-yv-mymIbw_W6k"\n }\n ],\n "data": {\n "7e5vQLDGTl": {\n "inputs": {\n "question": {\n "type": "text",\n "description": "问题"\n }\n }\n },\n "ldoKAzHnKF": {\n "model": "qwen3",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你是个聪明人"\n },\n "1eJtMoJWs6": {\n "model": "deepseek",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你也是个好人"\n }\n }\n}') + } = JSON.parse('{\n "nodes": [\n {\n "id": "TCxPixrdkI",\n "type": "start-node",\n "position": {\n "x": -256,\n "y": 109.5\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 83\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "tGs78_ietp",\n "type": "llm-node",\n "position": {\n "x": 108,\n "y": -2.5\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "OeZdaU7LpY",\n "type": "llm-node",\n "position": {\n "x": 111,\n "y": 196\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "LjfoCYZo-E",\n "type": "knowledge-node",\n "position": {\n "x": 497.62196259607214,\n "y": -10.792497317791003\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": true,\n "dragging": false\n },\n {\n "id": "sQM_22GYB5",\n "type": "end-node",\n "position": {\n "x": 874.3164534765615,\n "y": 151.70316541496913\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "KpMH_xc3ZZ",\n "type": "llm-node",\n "position": {\n "x": 529.6286840434341,\n "y": 150.4721376669937\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "TCxPixrdkI",\n "sourceHandle": "source",\n "target": "tGs78_ietp",\n "targetHandle": "target",\n "id": "xy-edge__TCxPixrdkIsource-tGs78_ietptarget"\n },\n {\n "source": "TCxPixrdkI",\n "sourceHandle": "source",\n "target": "OeZdaU7LpY",\n "targetHandle": "target",\n "id": "xy-edge__TCxPixrdkIsource-OeZdaU7LpYtarget"\n },\n {\n "source": "tGs78_ietp",\n "sourceHandle": "source",\n "target": "LjfoCYZo-E",\n "targetHandle": "target",\n "id": "xy-edge__tGs78_ietpsource-LjfoCYZo-Etarget"\n },\n {\n "source": "LjfoCYZo-E",\n "sourceHandle": "source",\n "target": "KpMH_xc3ZZ",\n "targetHandle": "target",\n "id": "xy-edge__LjfoCYZo-Esource-KpMH_xc3ZZtarget"\n },\n {\n "source": "OeZdaU7LpY",\n "sourceHandle": "source",\n "target": "KpMH_xc3ZZ",\n "targetHandle": "target",\n "id": "xy-edge__OeZdaU7LpYsource-KpMH_xc3ZZtarget"\n },\n {\n "source": "KpMH_xc3ZZ",\n "sourceHandle": "source",\n "target": "sQM_22GYB5",\n "targetHandle": "target",\n "id": "xy-edge__KpMH_xc3ZZsource-sQM_22GYB5target"\n }\n ],\n "data": {\n "tGs78_ietp": {\n "model": "qwen3",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你是个聪明人"\n },\n "OeZdaU7LpY": {\n "model": "qwen3",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你也是个聪明人"\n }\n }\n}') // language=JSON - checkAddConnection(JSON.parse('{\n "source": "1eJtMoJWs6",\n "sourceHandle": null,\n "target": "Wyqg_bXILg",\n "targetHandle": null\n}'), nodes, edges) + checkAddConnection(JSON.parse('{\n "source": "OeZdaU7LpY",\n "sourceHandle": "source",\n "target": "LjfoCYZo-E",\n "targetHandle": "target"\n}'), nodes, edges) }).toThrowError(hasRedundantEdgeError()) }) diff --git a/service-web/client/src/pages/ai/flow/FlowChecker.tsx b/service-web/client/src/pages/ai/flow/FlowChecker.tsx index 1c6192f..31684b5 100644 --- a/service-web/client/src/pages/ai/flow/FlowChecker.tsx +++ b/service-web/client/src/pages/ai/flow/FlowChecker.tsx @@ -1,5 +1,5 @@ -import {find, findIdx, isEqual, lpad, toStr} from 'licia' import {type Connection, type Edge, getConnectedEdges, getIncomers, getOutgoers, type Node} from '@xyflow/react' +import {clone, find, findIdx, isEqual, lpad, toStr, uuid} from 'licia' export class CheckError extends Error { readonly id: string @@ -49,29 +49,57 @@ const hasCycle = (sourceNode: Node, targetNode: Node, nodes: Node[], edges: Edge } } +/* 摘自Dify的流程合法性判断 */ + type ParallelInfoItem = { parallelNodeId: string depth: number isBranch?: boolean } + type NodeParallelInfo = { parallelNodeId: string edgeHandleId: string depth: number } + type NodeHandle = { node: Node handle: string } + type NodeStreamInfo = { upstreamNodes: Set downstreamEdges: Set } -const getParallelInfo = (nodes: Node[], edges: Edge[]) => { - let startNode +const groupBy = (array: Record[], iteratee: string) => { + const result: Record = {} + for (const item of array) { + // 获取属性值并转换为字符串键 + const key = item[iteratee] + if (!result[key]) { + result[key] = [] + } + result[key].push(item) + } + return result +} - startNode = nodes.find(node => isEqual(node.type, 'start-node')) +// @ts-ignore +export const getParallelInfo = (nodes: Node[], edges: Edge[], parentNodeId?: string) => { + // 等到有子图的时候再考虑 + /*if (parentNodeId) { + const parentNode = nodes.find(node => node.id === parentNodeId) + if (!parentNode) + throw new Error('Parent node not found') + + startNode = nodes.find(node => node.id === (parentNode.data as (IterationNodeType | LoopNodeType)).start_node_id) + } + else { + startNode = nodes.find(node => isEqual(node.type, 'start_node')) + }*/ + let startNode = nodes.find(node => isEqual(node.type, 'start-node')) if (!startNode) throw new Error('Start node not found') @@ -79,19 +107,6 @@ const getParallelInfo = (nodes: Node[], edges: Edge[]) => { const nextNodeHandles = [{node: startNode, handle: 'source'}] let hasAbnormalEdges = false - const groupBy = (array: Record[], iteratee: string) => { - const result: Record = {} - for (const item of array) { - // 获取属性值并转换为字符串键 - const key = item[iteratee] - if (!result[key]) { - result[key] = [] - } - result[key].push(item) - } - return result - } - const traverse = (firstNodeHandle: NodeHandle) => { const nodeEdgesSet = {} as Record> const totalEdgesSet = new Set() @@ -243,29 +258,11 @@ export const checkAddConnection: (connection: Connection, nodes: Node[], edges: throw nodeNotOnlyToEndNode() } - /*const hasRedundant = (source: Node, target: Node) => { - const visited = new Set() - const queue = new Queue() - queue.enqueue(source) - visited.add(source.id) - while (queue.size > 0) { - const current = queue.dequeue()! - console.log(current.id) - for (const incomer of getIncomers(current, nodes, edges)) { - if (isEqual(incomer.id, target.id)) { - return true - } - if (!visited.has(incomer.id)) { - visited.add(incomer.id) - queue.enqueue(incomer) - } - } - } - return false + let newEdges = [...clone(edges), {...connection, id: uuid()}] + let {hasAbnormalEdges} = getParallelInfo(nodes, newEdges) + if (hasAbnormalEdges) { + throw hasRedundantEdgeError() } - if (hasRedundant(sourceNode, targetNode)) { - throw new Error('出现冗余边') - }*/ } export const atLeastOneStartNodeError = () => new CheckError(300, '至少存在1个开始节点') diff --git a/service-web/client/src/pages/ai/flow/FlowEditor.tsx b/service-web/client/src/pages/ai/flow/FlowEditor.tsx index fa318a0..6076f06 100644 --- a/service-web/client/src/pages/ai/flow/FlowEditor.tsx +++ b/service-web/client/src/pages/ai/flow/FlowEditor.tsx @@ -1,5 +1,5 @@ import {PlusCircleFilled, SaveFilled} from '@ant-design/icons' -import {Background, BackgroundVariant, Controls, MiniMap, type NodeProps, ReactFlow,} from '@xyflow/react' +import {Background, BackgroundVariant, Controls, MiniMap, type NodeProps, ReactFlow} from '@xyflow/react' import {useMount} from 'ahooks' import type {Schema} from 'amis' import {Button, Drawer, Dropdown, message, Space} from 'antd' @@ -8,15 +8,15 @@ import {type JSX, type MemoExoticComponent, useState} from 'react' import styled from 'styled-components' import '@xyflow/react/dist/style.css' import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx' +import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx' import CodeNode from './node/CodeNode.tsx' import EndNode from './node/EndNode.tsx' import KnowledgeNode from './node/KnowledgeNode.tsx' import LlmNode from './node/LlmNode.tsx' import StartNode from './node/StartNode.tsx' +import SwitchNode from './node/SwitchNode.tsx' import {useDataStore} from './store/DataStore.ts' import {useFlowStore} from './store/FlowStore.ts' -import SwitchNode from './node/SwitchNode.tsx' -import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx' const FlowableDiv = styled.div` height: 100%; @@ -188,7 +188,7 @@ function FlowEditor() { useMount(() => { // language=JSON - let initialData = JSON.parse('{\n "nodes": [\n {\n "id": "ldoKAzHnKF",\n "type": "llm-node",\n "position": {\n "x": 207,\n "y": -38\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "1eJtMoJWs6",\n "type": "llm-node",\n "position": {\n "x": 207,\n "y": 172.5\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "7e5vQLDGTl",\n "type": "start-node",\n "position": {\n "x": -162.3520537805597,\n "y": 67.84901301708827\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "Wyqg_bXILg",\n "type": "knowledge-node",\n "position": {\n "x": 560.402133595296,\n "y": -38.892263766178665\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "7DaF-0G-yv",\n "type": "llm-node",\n "position": {\n "x": 634.9924233956513,\n "y": 172.01821084172227\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "mymIbw_W6k",\n "type": "end-node",\n "position": {\n "x": 953.9302142661356,\n "y": 172.0182108417223\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "7e5vQLDGTl",\n "target": "ldoKAzHnKF",\n "id": "xy-edge__7e5vQLDGTl-ldoKAzHnKF"\n },\n {\n "source": "ldoKAzHnKF",\n "target": "Wyqg_bXILg",\n "id": "xy-edge__ldoKAzHnKF-Wyqg_bXILg"\n },\n {\n "source": "7e5vQLDGTl",\n "target": "1eJtMoJWs6",\n "id": "xy-edge__7e5vQLDGTl-1eJtMoJWs6"\n },\n {\n "source": "Wyqg_bXILg",\n "target": "7DaF-0G-yv",\n "id": "xy-edge__Wyqg_bXILg-7DaF-0G-yv"\n },\n {\n "source": "1eJtMoJWs6",\n "target": "7DaF-0G-yv",\n "id": "xy-edge__1eJtMoJWs6-7DaF-0G-yv"\n },\n {\n "source": "7DaF-0G-yv",\n "target": "mymIbw_W6k",\n "id": "xy-edge__7DaF-0G-yv-mymIbw_W6k"\n }\n ],\n "data": {\n "7e5vQLDGTl": {\n "inputs": {\n "question": {\n "type": "text",\n "description": "问题"\n }\n }\n },\n "ldoKAzHnKF": {\n "model": "qwen3",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你是个聪明人"\n },\n "1eJtMoJWs6": {\n "model": "deepseek",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你也是个好人"\n }\n }\n}') + let initialData = JSON.parse('{\n "nodes": [\n {\n "id": "TCxPixrdkI",\n "type": "start-node",\n "position": {\n "x": -256,\n "y": 109.5\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 83\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "tGs78_ietp",\n "type": "llm-node",\n "position": {\n "x": 108,\n "y": -2.5\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "OeZdaU7LpY",\n "type": "llm-node",\n "position": {\n "x": 111,\n "y": 196\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 105\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "LjfoCYZo-E",\n "type": "knowledge-node",\n "position": {\n "x": 497.62196259607214,\n "y": -10.792497317791003\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": true,\n "dragging": false\n },\n {\n "id": "sQM_22GYB5",\n "type": "end-node",\n "position": {\n "x": 874.3164534765615,\n "y": 151.70316541496913\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "KpMH_xc3ZZ",\n "type": "llm-node",\n "position": {\n "x": 529.6286840434341,\n "y": 150.4721376669937\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 75\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "TCxPixrdkI",\n "sourceHandle": "source",\n "target": "tGs78_ietp",\n "targetHandle": "target",\n "id": "xy-edge__TCxPixrdkIsource-tGs78_ietptarget"\n },\n {\n "source": "TCxPixrdkI",\n "sourceHandle": "source",\n "target": "OeZdaU7LpY",\n "targetHandle": "target",\n "id": "xy-edge__TCxPixrdkIsource-OeZdaU7LpYtarget"\n },\n {\n "source": "tGs78_ietp",\n "sourceHandle": "source",\n "target": "LjfoCYZo-E",\n "targetHandle": "target",\n "id": "xy-edge__tGs78_ietpsource-LjfoCYZo-Etarget"\n },\n {\n "source": "LjfoCYZo-E",\n "sourceHandle": "source",\n "target": "KpMH_xc3ZZ",\n "targetHandle": "target",\n "id": "xy-edge__LjfoCYZo-Esource-KpMH_xc3ZZtarget"\n },\n {\n "source": "OeZdaU7LpY",\n "sourceHandle": "source",\n "target": "KpMH_xc3ZZ",\n "targetHandle": "target",\n "id": "xy-edge__OeZdaU7LpYsource-KpMH_xc3ZZtarget"\n },\n {\n "source": "KpMH_xc3ZZ",\n "sourceHandle": "source",\n "target": "sQM_22GYB5",\n "targetHandle": "target",\n "id": "xy-edge__KpMH_xc3ZZsource-sQM_22GYB5target"\n }\n ],\n "data": {\n "tGs78_ietp": {\n "model": "qwen3",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你是个聪明人"\n },\n "OeZdaU7LpY": {\n "model": "qwen3",\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "systemPrompt": "你也是个聪明人"\n }\n }\n}') // let initialData: any = {} let initialNodes = initialData?.nodes ?? [] let initialEdges = initialData?.edges ?? [] diff --git a/service-web/client/src/pages/ai/flow/node/AmisNode.tsx b/service-web/client/src/pages/ai/flow/node/AmisNode.tsx index 27040c4..e393169 100644 --- a/service-web/client/src/pages/ai/flow/node/AmisNode.tsx +++ b/service-web/client/src/pages/ai/flow/node/AmisNode.tsx @@ -187,9 +187,9 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({ {isNil(handlers) ? <> {isEqual(type, 'start') || isEqual(type, 'normal') - ? : undefined} + ? : undefined} {isEqual(type, 'end') || isEqual(type, 'normal') - ? : undefined} + ? : undefined} : handlers?.(nodeData)}