feat(web): 增加分支节点,增加流程检查的测试

This commit is contained in:
2025-06-29 19:02:19 +08:00
parent 8884495a89
commit 779fd0eb18
11 changed files with 648 additions and 110 deletions

View File

@@ -0,0 +1,113 @@
import {expect, test} from 'vitest'
import {type Connection, type Node} from '@xyflow/react'
import {
atLeastOneEndNodeError,
atLeastOneStartNodeError,
checkAddConnection,
checkAddNode,
checkSave,
hasCycleError,
hasRedundantEdgeError,
multiEndNodeError,
multiStartNodeError,
nodeToSelfError,
sourceNodeNotFoundError,
startNodeToEndNodeError,
targetNodeNotFoundError
} from './FlowChecker.tsx'
import {uuid} from 'licia'
const createNode = (id: string, type: string): Node => {
return {
data: {},
position: {
x: 0,
y: 0
},
id,
type,
}
}
const createStartNode = (id: string): Node => createNode(id, 'start-node')
const createEndNode = (id: string): Node => createNode(id, 'end-node')
const createConnection = function (source: string, target: string, sourceHandle: string | null = null, targetHandle: string | null = null): Connection {
return {
source,
target,
sourceHandle,
targetHandle,
}
}
/* check add node */
test(multiStartNodeError().message, () => {
expect(() => checkAddNode('start-node', [createStartNode(uuid())], [])).toThrowError(multiStartNodeError())
})
test(multiEndNodeError().message, () => {
expect(() => checkAddNode('end-node', [createEndNode(uuid())], [])).toThrowError(multiEndNodeError())
})
/* check add connection */
test(sourceNodeNotFoundError().message, () => {
expect(() => checkAddConnection(createConnection('a', 'b'), [], []))
})
test(targetNodeNotFoundError().message, () => {
expect(() => checkAddConnection(createConnection('a', 'b'), [createStartNode('a')], []))
})
test(startNodeToEndNodeError().message, () => {
expect(() => checkAddConnection(
createConnection('a', 'b'),
[createStartNode('a'), createEndNode('b')],
[]
))
})
test(nodeToSelfError().message, () => {
expect(() => {
// language=JSON
const {
nodes,
edges
} = JSON.parse('{\n "nodes": [\n {\n "id": "P14abHl4uY",\n "type": "start-node",\n "position": {\n "x": 100,\n "y": 100\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 82\n }\n },\n {\n "id": "3YDRebKqCX",\n "type": "end-node",\n "position": {\n "x": 773.3027344262372,\n "y": 101.42648884412338\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "YXJ91nHVaz",\n "type": "llm-node",\n "position": {\n "x": 430.94541183662506,\n "y": 101.42648884412338\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": true,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "P14abHl4uY",\n "target": "YXJ91nHVaz",\n "id": "xy-edge__P14abHl4uY-YXJ91nHVaz"\n },\n {\n "source": "YXJ91nHVaz",\n "target": "3YDRebKqCX",\n "id": "xy-edge__YXJ91nHVaz-3YDRebKqCX"\n }\n ],\n "data": {}\n}')
checkAddConnection(createConnection('YXJ91nHVaz', 'YXJ91nHVaz'), nodes, edges)
}).toThrowError(nodeToSelfError())
})
test(hasCycleError().message, () => {
expect(() => {
// language=JSON
const {
nodes,
edges,
} = JSON.parse('{\n "nodes": [\n {\n "id": "-DKfXm7r3f",\n "type": "start-node",\n "position": {\n "x": -75.45812782717618,\n "y": 14.410669352596976\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 82\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "2uL3Hw2CAW",\n "type": "end-node",\n "position": {\n "x": 734.7875356349059,\n "y": -1.2807079327602473\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "yp-yYfKUzC",\n "type": "llm-node",\n "position": {\n "x": 338.2236369686051,\n "y": -92.5759939566568\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "N4HQPN-NYZ",\n "type": "llm-node",\n "position": {\n "x": 332.51768159211156,\n "y": 114.26488844123382\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 74\n },\n "selected": true,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "-DKfXm7r3f",\n "target": "yp-yYfKUzC",\n "id": "xy-edge__-DKfXm7r3f-yp-yYfKUzC"\n },\n {\n "source": "yp-yYfKUzC",\n "target": "2uL3Hw2CAW",\n "id": "xy-edge__yp-yYfKUzC-2uL3Hw2CAW"\n },\n {\n "source": "-DKfXm7r3f",\n "target": "N4HQPN-NYZ",\n "id": "xy-edge__-DKfXm7r3f-N4HQPN-NYZ"\n },\n {\n "source": "N4HQPN-NYZ",\n "target": "yp-yYfKUzC",\n "id": "xy-edge__N4HQPN-NYZ-yp-yYfKUzC"\n }\n ],\n "data": {}\n}')
// language=JSON
checkAddConnection(JSON.parse('{\n "source": "yp-yYfKUzC",\n "sourceHandle": null,\n "target": "N4HQPN-NYZ",\n "targetHandle": null\n}'), nodes, edges)
}).toThrowError(hasCycleError())
})
test(hasRedundantEdgeError().message, () => {
expect(() => {
// language=JSON
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}')
// language=JSON
checkAddConnection(JSON.parse('{\n "source": "1eJtMoJWs6",\n "sourceHandle": null,\n "target": "Wyqg_bXILg",\n "targetHandle": null\n}'), nodes, edges)
}).toThrowError(hasRedundantEdgeError())
})
/* check save */
test(atLeastOneStartNodeError().message, () => {
expect(() => checkSave([], [], {})).toThrowError(atLeastOneStartNodeError())
})
test(atLeastOneEndNodeError().message, () => {
expect(() => checkSave([createStartNode(uuid())], [], {})).toThrowError(atLeastOneEndNodeError())
})

View File

@@ -0,0 +1,113 @@
import {find, findIdx, isEqual, lpad, toStr} from 'licia'
import {type Connection, type Edge, getOutgoers, type Node} from '@xyflow/react'
export class CheckError extends Error {
readonly id: string
constructor(
id: number,
message: string,
) {
super(message)
this.id = `E${lpad(toStr(id), 6, '0')}`
}
public toString(): string {
return `${this.id}: ${this.message}`
}
}
export const multiStartNodeError = () => new CheckError(100, '只能存在1个开始节点')
export const multiEndNodeError = () => new CheckError(101, '只能存在1个结束节点')
const getNodeById = (id: string, nodes: Node[]) => find(nodes, (n: Node) => isEqual(n.id, id))
// @ts-ignore
export const checkAddNode: (type: string, nodes: Node[], edges: Edge[]) => void = (type, nodes, edges) => {
if (isEqual(type, 'start-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) {
throw multiStartNodeError()
}
if (isEqual(type, 'end-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) {
throw multiEndNodeError()
}
}
export const sourceNodeNotFoundError = () => new CheckError(200, '连线起始节点未找到')
export const targetNodeNotFoundError = () => new CheckError(201, '连线目标节点未找到')
export const startNodeToEndNodeError = () => new CheckError(202, '开始节点不能直连结束节点')
export const nodeToSelfError = () => new CheckError(203, '节点不能直连自身')
export const hasCycleError = () => new CheckError(204, '禁止流程循环')
export const nodeNotOnlyToEndNode = () => new CheckError(206, '直连结束节点的节点不允许连接其他节点')
export const hasRedundantEdgeError = () => new CheckError(207, '禁止出现冗余边')
export const checkAddConnection: (connection: Connection, nodes: Node[], edges: Edge[]) => void = (connection, nodes, edges) => {
let sourceNode = getNodeById(connection.source, nodes)
if (!sourceNode) {
throw sourceNodeNotFoundError()
}
let targetNode = getNodeById(connection.target, nodes)
if (!targetNode) {
throw targetNodeNotFoundError()
}
// 禁止短路整个流程
if (isEqual('start-node', sourceNode.type) && isEqual('end-node', targetNode.type)) {
throw startNodeToEndNodeError()
}
// 禁止流程出现环,必须是有向无环图
const hasCycle = (node: Node, visited = new Set<string>()) => {
if (visited.has(node.id)) return false
visited.add(node.id)
for (const outgoer of getOutgoers(node, nodes, edges)) {
if (isEqual(outgoer.id, sourceNode?.id)) return true
if (hasCycle(outgoer, visited)) return true
}
}
if (isEqual(sourceNode.id, targetNode.id)) {
throw nodeToSelfError()
} else if (hasCycle(targetNode)) {
throw hasCycleError()
}
let outgoers = [targetNode, ...getOutgoers(sourceNode, nodes, edges)]
if (outgoers.length > 1 && findIdx(outgoers, (node: Node) => isEqual(node.type, 'end-node')) > -1) {
throw nodeNotOnlyToEndNode()
}
/*const hasRedundant = (source: Node, target: Node) => {
const visited = new Set<string>()
const queue = new Queue<Node>()
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
}
if (hasRedundant(sourceNode, targetNode)) {
throw new Error('出现冗余边')
}*/
}
export const atLeastOneStartNodeError = () => new CheckError(300, '至少存在1个开始节点')
export const atLeastOneEndNodeError = () => new CheckError(301, '至少存在1个结束节点')
// @ts-ignore
export const checkSave: (nodes: Node[], edges: Edge[], data: any) => void = (nodes, edges, data) => {
if (nodes.filter(n => isEqual('start-node', n.type)).length < 1) {
throw atLeastOneStartNodeError()
}
if (nodes.filter(n => isEqual('end-node', n.type)).length < 1) {
throw atLeastOneEndNodeError()
}
}

View File

@@ -1,25 +1,13 @@
import {PlusCircleFilled, SaveFilled} from '@ant-design/icons'
import {
Background,
BackgroundVariant,
type Connection,
Controls,
type Edge,
getOutgoers,
MiniMap,
type Node,
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'
import {arrToMap, find, findIdx, isEqual, isNil, randomId} from 'licia'
import {type JSX, useState} from 'react'
import {arrToMap, find, isEqual, isNil, randomId} from 'licia'
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 {buildEL} from './ElParser.tsx'
import CodeNode from './node/CodeNode.tsx'
import EndNode from './node/EndNode.tsx'
import KnowledgeNode from './node/KnowledgeNode.tsx'
@@ -27,6 +15,8 @@ import LlmNode from './node/LlmNode.tsx'
import StartNode from './node/StartNode.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%;
@@ -70,7 +60,7 @@ function FlowEditor() {
const [nodeDef] = useState<{
key: string,
name: string,
component: (props: NodeProps) => JSX.Element
component: MemoExoticComponent<(props: NodeProps) => JSX.Element>
}[]>([
{
key: 'start-node',
@@ -97,13 +87,17 @@ function FlowEditor() {
name: '代码执行',
component: CodeNode,
},
{
key: 'switch-node',
name: '条件分支',
component: SwitchNode,
},
])
const [open, setOpen] = useState(false)
const {data, setData, getDataById, setDataById} = useDataStore()
const {
nodes,
getNodeById,
addNode,
removeNode,
setNodes,
@@ -184,79 +178,6 @@ function FlowEditor() {
}
}
const checkNode = (type: string) => {
if (isEqual(type, 'start-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) {
throw new Error('只能存在1个开始节点')
}
if (isEqual(type, 'end-node') && findIdx(nodes, (node: Node) => isEqual(type, node.type)) > -1) {
throw new Error('只能存在1个结束节点')
}
}
const checkConnection = (connection: Connection) => {
let sourceNode = getNodeById(connection.source)
if (!sourceNode) {
throw new Error('连线起始节点未找到')
}
let targetNode = getNodeById(connection.target)
if (!targetNode) {
throw new Error('连线目标节点未找到')
}
// 禁止短路整个流程
if (isEqual('start-node', sourceNode.type) && isEqual('end-node', targetNode.type)) {
throw new Error('开始节点不能直连结束节点')
}
// 禁止流程出现环,必须是有向无环图
const hasCycle = (node: Node, visited = new Set<string>()) => {
if (visited.has(node.id)) return false
visited.add(node.id)
for (const outgoer of getOutgoers(node, nodes, edges)) {
if (isEqual(outgoer.id, sourceNode?.id)) return true
if (hasCycle(outgoer, visited)) return true
}
}
if (isEqual(sourceNode.id, targetNode.id)) {
throw new Error('节点不能直连自身')
} else if (hasCycle(targetNode)) {
throw new Error('禁止流程循环')
}
/*const hasRedundant = (source: Node, target: Node) => {
const visited = new Set<string>()
const queue = new Queue<Node>()
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
}
if (hasRedundant(sourceNode, targetNode)) {
throw new Error('出现冗余边')
}*/
}
// @ts-ignore
const checkSave = (nodes: Node[], edges: Edge[], data: any) => {
if (nodes.filter(n => isEqual('start-node', n.type)).length < 1) {
throw new Error('至少存在1个开始节点')
}
if (nodes.filter(n => isEqual('end-node', n.type)).length < 1) {
throw new Error('至少存在1个结束节点')
}
}
// 用于透传node操作到主流程
const initialNodeHandlers = {
getDataById,
@@ -268,10 +189,11 @@ 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 initialNodes = initialData['nodes'] ?? []
let initialEdges = initialData['edges'] ?? []
// let initialData: any = {}
let initialNodes = initialData?.nodes ?? []
let initialEdges = initialData?.edges ?? []
let initialNodeData = initialData['data'] ?? {}
let initialNodeData = initialData?.data ?? {}
setData(initialNodeData)
for (let node of initialNodes) {
@@ -290,7 +212,10 @@ function FlowEditor() {
items: nodeDef.map(def => ({key: def.key, label: def.name})),
onClick: ({key}) => {
try {
checkNode(key)
if (commonInfo.debug) {
console.info('Add', key, JSON.stringify({nodes, edges, data}))
}
checkAddNode(key, nodes, edges)
addNode({
id: randomId(10),
type: key,
@@ -299,7 +224,7 @@ function FlowEditor() {
})
} catch (e) {
// @ts-ignore
messageApi.error(e.message)
messageApi.error(e.toString())
}
},
}}
@@ -311,13 +236,15 @@ function FlowEditor() {
</Dropdown>
<Button type="primary" onClick={() => {
try {
if (commonInfo.debug) {
console.info('Save', JSON.stringify({nodes, edges, data}))
}
checkSave(nodes, edges, data)
let saveData = {nodes, edges, data}
console.log(JSON.stringify(saveData, null, 2))
console.log(buildEL(nodes, edges))
// let saveData = {nodes, edges, data}
// console.log(buildEL(nodes, edges))
} catch (e) {
// @ts-ignore
messageApi.error(e.message)
messageApi.error(e.toString())
}
}}>
<SaveFilled/>
@@ -341,11 +268,14 @@ function FlowEditor() {
onEdgesChange={onEdgesChange}
onConnect={(connection) => {
try {
checkConnection(connection)
if (commonInfo.debug) {
console.info('Connection', JSON.stringify(connection), JSON.stringify({nodes, edges, data}))
}
checkAddConnection(connection, nodes, edges)
onConnect(connection)
} catch (e) {
// @ts-ignore
messageApi.error(e.message)
messageApi.error(e.toString())
}
}}
// @ts-ignore

View File

@@ -1,5 +1,6 @@
import type {NodeProps} from '@xyflow/react'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const CodeNode = (props: NodeProps) => AmisNode({
nodeProps: props,
@@ -48,4 +49,4 @@ const CodeNode = (props: NodeProps) => AmisNode({
],
})
export default CodeNode
export default React.memo(CodeNode)

View File

@@ -1,5 +1,6 @@
import type {NodeProps} from '@xyflow/react'
import AmisNode, {outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const EndNode = (props: NodeProps) => AmisNode({
nodeProps: props,
@@ -9,4 +10,4 @@ const EndNode = (props: NodeProps) => AmisNode({
columnSchema: outputsFormColumns(true),
})
export default EndNode
export default React.memo(EndNode)

View File

@@ -1,6 +1,7 @@
import type {NodeProps} from '@xyflow/react'
import {commonInfo} from '../../../../util/amis.tsx'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const KnowledgeNode = (props: NodeProps) => AmisNode({
nodeProps: props,
@@ -62,4 +63,4 @@ const KnowledgeNode = (props: NodeProps) => AmisNode({
],
})
export default KnowledgeNode
export default React.memo(KnowledgeNode)

View File

@@ -1,6 +1,7 @@
import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd'
import AmisNode, {inputsFormColumns, outputsFormColumns} from './AmisNode.tsx'
import React from 'react'
const modelMap: Record<string, string> = {
qwen3: 'Qwen3',
@@ -47,4 +48,4 @@ const LlmNode = (props: NodeProps) => AmisNode({
],
})
export default LlmNode
export default React.memo(LlmNode)

View File

@@ -1,7 +1,7 @@
import type {NodeProps} from '@xyflow/react'
import {Tag} from 'antd'
import {each} from 'licia'
import type {JSX} from 'react'
import React, {type JSX} from 'react'
import {horizontalFormOptions} from '../../../../util/amis.tsx'
import AmisNode from './AmisNode.tsx'
@@ -18,7 +18,7 @@ const StartNode = (props: NodeProps) => AmisNode({
defaultNodeDescription: '定义输入变量',
extraNodeDescription: nodeData => {
const variables: JSX.Element[] = []
const inputs = (nodeData['inputs'] ?? {}) as Record<string, { type: string, description: string }>
const inputs = (nodeData?.inputs ?? {}) as Record<string, { type: string, description: string }>
each(inputs, (value, key) => {
variables.push(
<div className="mt-1 flex justify-between" key={key}>
@@ -65,4 +65,4 @@ const StartNode = (props: NodeProps) => AmisNode({
],
})
export default StartNode
export default React.memo(StartNode)

View File

@@ -0,0 +1,53 @@
import {Handle, type NodeProps, Position} from '@xyflow/react'
import AmisNode from './AmisNode.tsx'
import {Tag} from 'antd'
import React from 'react'
const cases = [
{
index: 1
},
{
index: 2
},
{
index: 3
}
]
const SwitchNode = (props: NodeProps) => AmisNode({
nodeProps: props,
type: 'normal',
defaultNodeName: '分支节点',
defaultNodeDescription: '根据不同的情况前往不同的分支',
columnSchema: [],
extraNodeDescription: nodeData => {
return (
<div className="mt-2">
{cases.map(item => (
<div key={item.index} className="mt-1">
<Tag className="m-0" color="blue"> {item.index}</Tag>
</div>
))}
</div>
)
},
handlers: nodeData => {
return (
<>
<Handle type="target" position={Position.Left}/>
{cases.map((item, index) => (
<Handle
type="source"
position={Position.Right}
key={item.index}
id={`${item.index}`}
style={{top: 85 + (25 * index)}}
/>
))}
</>
)
}
})
export default React.memo(SwitchNode)