diff --git a/service-web/client/src/pages/Test.tsx b/service-web/client/src/pages/Test.tsx
index 6eee028..d90d4e6 100644
--- a/service-web/client/src/pages/Test.tsx
+++ b/service-web/client/src/pages/Test.tsx
@@ -1,486 +1,6 @@
-import {DeleteFilled, EditFilled, PlusCircleFilled, SaveFilled} from '@ant-design/icons'
-import {
- addEdge,
- applyEdgeChanges,
- applyNodeChanges,
- Background,
- BackgroundVariant,
- Controls,
- type Edge,
- Handle,
- MiniMap,
- type Node,
- type NodeProps,
- type OnConnect,
- type OnEdgesChange,
- type OnNodesChange,
- Position,
- ReactFlow,
-} from '@xyflow/react'
-import {useMount} from 'ahooks'
-import type {Schema} from 'amis'
-import {Button, Card, Drawer, Dropdown, message, Space} from 'antd'
-import {arrToMap, filter, find, findIdx, isEqual, isNil, randomId} from 'licia'
-import {type JSX, useState} from 'react'
-import styled from 'styled-components'
-import '@xyflow/react/dist/style.css'
-import {create} from 'zustand/react'
-import {amisRender, commonInfo} from '../util/amis.tsx'
-
-const FlowableDiv = styled.div`
- height: 93vh;
-
- .toolbar {
- z-index: 999;
- position: absolute;
- }
-
- .node-card {
- cursor: grab;
-
- .card-container {
- cursor: default;
- }
- }
-`
-
-type AmisNodeType = 'normal' | 'start' | 'end'
-
-const AmisNode = (
- props: NodeProps,
- type: AmisNodeType,
- name: String,
- description?: String,
- columnSchema?: Schema[],
-) => {
- const {id, data} = props
- const {getDataById, removeNode, editNode} = data
- return (
-
-
- ,
- },
- {
- key: 'remove',
- label: '删除',
- icon: ,
- },
- ],
- onClick: menu => {
- switch (menu.key) {
- case 'edit':
- // @ts-ignore
- editNode(id, name, description, columnSchema)
- break
- case 'remove':
- // @ts-ignore
- removeNode(id)
- break
- }
- },
- }}
- >
-
- {description}
-
-
-
- {isEqual(type, 'start') || isEqual(type, 'normal')
- ?
: undefined}
- {isEqual(type, 'end') || isEqual(type, 'normal')
- ? : undefined}
-
- )
-}
-
-const StartAmisNode = (props: NodeProps) => AmisNode(
- props,
- 'start',
- '开始节点',
- '定义输入变量',
- [
- {
- type: 'input-kvs',
- name: 'fields',
- addButtonText: '新增入参',
- draggable: false,
- keyItem: {
- label: '参数名称',
- },
- valueItems: [
- {
- type: 'input-text',
- name: 'description',
- label: '参数描述',
- },
- {
- type: 'select',
- name: 'type',
- label: '参数类型',
- required: true,
- selectFirst: true,
- options: [
- {
- label: '文本',
- value: 'text',
- },
- {
- label: '数字',
- value: 'number',
- },
- {
- label: '文件',
- value: 'files',
- },
- ],
- },
- ],
- },
- ],
-)
-const EndAmisNode = (props: NodeProps) => AmisNode(
- props,
- 'end',
- '结束节点',
- '定义输出变量',
- [
- {
- type: 'input-kvs',
- name: 'fields',
- addButtonText: '新增输出',
- draggable: false,
- keyItem: {
- label: '参数名称',
- },
- valueItems: [
- {
- type: 'select',
- name: 'type',
- label: '参数',
- required: true,
- selectFirst: true,
- options: [],
- },
- ],
- },
- ],
-)
-const LlmAmisNode = (props: NodeProps) => AmisNode(
- props,
- 'normal',
- '大模型节点',
- '使用大模型对话',
- [
- {
- type: 'select',
- name: 'model',
- label: '大模型',
- required: true,
- selectFirst: true,
- options: [
- {
- label: 'Qwen3',
- value: 'qwen3',
- },
- {
- label: 'Deepseek',
- value: 'deepseek',
- },
- ],
- },
- {
- type: 'textarea',
- name: 'systemPrompt',
- label: '系统提示词',
- required: true,
- },
- ],
-)
-
-const initialNodes: Node[] = [
- {
- id: 'BMFP3Eov94',
- type: 'start-amis-node',
- position: {x: 10, y: 100},
- data: {},
- },
- {
- id: 'PYK8LjduQ1',
- type: 'end-amis-node',
- position: {x: 500, y: 100},
- data: {},
- },
-]
-const initialEdges: Edge[] = []
-
-const useStore = create<{
- data: Record,
- getData: () => Record,
- setData: (data: Record) => void,
- getDataById: (id: string) => any,
- setDataById: (id: string, data: any) => void,
-}>((set, get) => ({
- data: {},
- getData: () => get().data,
- setData: (data) => set(data),
- getDataById: id => get().data[id],
- setDataById: (id, data) => {
- let updateData = get().data
- updateData[id] = data
- set({
- data: updateData,
- })
- },
-}))
-
-const useFlowStore = create<{
- nodes: Node[],
- onNodesChange: OnNodesChange,
- addNode: (node: Node) => void,
- removeNode: (id: string) => void,
- setNodes: (nodes: Node[]) => void,
-
- edges: Edge[],
- onEdgesChange: OnEdgesChange,
- setEdges: (edges: Edge[]) => void,
-
- onConnect: OnConnect,
-}>((set, get) => ({
- nodes: [],
- onNodesChange: changes => {
- set({
- nodes: applyNodeChanges(changes, get().nodes),
- })
- },
- addNode: node => set({nodes: get().nodes.concat(node)}),
- removeNode: id => {
- set({
- nodes: filter(get().nodes, node => !isEqual(node.id, id)),
- })
- },
- setNodes: nodes => set({nodes}),
-
- edges: [],
- onEdgesChange: changes => {
- set({
- edges: applyEdgeChanges(changes, get().edges),
- })
- },
- setEdges: edges => set({edges}),
-
- onConnect: connection => {
- set({
- edges: addEdge(connection, get().edges),
- })
- },
-}))
-
function Test() {
- const [messageApi, contextHolder] = message.useMessage()
- const [nodeDef] = useState<{
- key: string,
- name: string,
- component: (props: NodeProps) => JSX.Element
- }[]>([
- {
- key: 'start-amis-node',
- name: '开始',
- component: StartAmisNode,
- },
- {
- key: 'end-amis-node',
- name: '结束',
- component: EndAmisNode,
- },
- {
- key: 'llm-amis-node',
- name: '大模型',
- component: LlmAmisNode,
- },
- ])
- const [open, setOpen] = useState(false)
-
- const {getData, getDataById, setDataById} = useStore()
- const {
- nodes,
- addNode,
- removeNode,
- setNodes,
- onNodesChange,
- edges,
- setEdges,
- onEdgesChange,
- onConnect,
- } = useFlowStore()
-
- const [currentNodeForm, setCurrentNodeForm] = useState()
- const editNode = (id: string, name: string, description: string, columnSchema?: Schema[]) => {
- if (!isNil(columnSchema)) {
- setCurrentNodeForm(
- amisRender(
- {
- type: 'wrapper',
- size: 'none',
- body: [
- {
- type: 'tpl',
- className: 'text-secondary',
- tpl: description,
- },
- {
- debug: commonInfo.debug,
- title: name,
- type: 'form',
- wrapWithPanel: false,
- onEvent: {
- submitSucc: {
- actions: [
- {
- actionType: 'custom',
- // @ts-ignore
- script: (context, action, event) => {
- setDataById(id, context.props.data)
- setOpen(false)
- },
- },
- ],
- },
- },
- body: [
- ...(columnSchema ?? []),
- {
- type: 'wrapper',
- size: 'none',
- className: 'space-x-1 float-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)
- }
- }
-
- useMount(() => {
- for (let node of initialNodes) {
- node.data = {
- getDataById,
- setDataById,
- removeNode,
- editNode,
- }
- }
- setNodes(initialNodes)
- setEdges(initialEdges)
- })
-
return (
-
- {contextHolder}
-
-
- ({key: def.key, label: def.name})),
- onClick: ({key}) => {
- if (isEqual(key, 'start-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) {
- messageApi.error('只能存在1个开始节点')
- return
- }
- if (isEqual(key, 'end-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) {
- messageApi.error('只能存在1个结束节点')
- return
- }
-
- addNode({
- id: randomId(10),
- type: key,
- position: {x: 100, y: 100},
- data: {
- getDataById,
- setDataById,
- removeNode,
- editNode,
- },
- })
- },
- }}
- >
-
-
-
-
- {currentNodeForm}
-
- def.key),
- key => find(nodeDef, def => isEqual(key, def.key))!.component)
- }
- >
-
-
-
-
-
+ Test
)
}
diff --git a/service-web/client/src/pages/ai/flow/FlowEditor.tsx b/service-web/client/src/pages/ai/flow/FlowEditor.tsx
new file mode 100644
index 0000000..20bcac2
--- /dev/null
+++ b/service-web/client/src/pages/ai/flow/FlowEditor.tsx
@@ -0,0 +1,255 @@
+import {PlusCircleFilled, SaveFilled} from '@ant-design/icons'
+import {
+ Background,
+ BackgroundVariant,
+ Controls,
+ type Edge,
+ MiniMap,
+ type Node,
+ 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 styled from 'styled-components'
+import '@xyflow/react/dist/style.css'
+import {amisRender, commonInfo} from '../../../util/amis.tsx'
+import StartNode from './node/StartNode.tsx'
+import EndNode from './node/EndNode.tsx'
+import LlmNode from './node/LlmNode.tsx'
+import {useDataStore} from './store/DataStore.ts'
+import {useFlowStore} from './store/FlowStore.ts'
+
+const FlowableDiv = styled.div`
+ height: 93vh;
+
+ .toolbar {
+ z-index: 999;
+ position: absolute;
+ }
+
+ .node-card {
+ cursor: grab;
+
+ .card-container {
+ cursor: default;
+ }
+ }
+`
+
+const initialNodes: Node[] = [
+ {
+ id: 'BMFP3Eov94',
+ type: 'start-amis-node',
+ position: {x: 10, y: 100},
+ data: {},
+ },
+ {
+ id: 'PYK8LjduQ1',
+ type: 'end-amis-node',
+ position: {x: 500, y: 100},
+ data: {},
+ },
+]
+const initialEdges: Edge[] = []
+
+function FlowEditor() {
+ const [messageApi, contextHolder] = message.useMessage()
+ const [nodeDef] = useState<{
+ key: string,
+ name: string,
+ component: (props: NodeProps) => JSX.Element
+ }[]>([
+ {
+ key: 'start-amis-node',
+ name: '开始',
+ component: StartNode,
+ },
+ {
+ key: 'end-amis-node',
+ name: '结束',
+ component: EndNode,
+ },
+ {
+ key: 'llm-amis-node',
+ name: '大模型',
+ component: LlmNode,
+ },
+ ])
+ const [open, setOpen] = useState(false)
+
+ const {getData, getDataById, setDataById} = useDataStore()
+ const {
+ nodes,
+ addNode,
+ removeNode,
+ setNodes,
+ onNodesChange,
+ edges,
+ setEdges,
+ onEdgesChange,
+ onConnect,
+ } = useFlowStore()
+
+ const [currentNodeForm, setCurrentNodeForm] = useState()
+ const editNode = (id: string, name: string, description: string, columnSchema?: Schema[]) => {
+ if (!isNil(columnSchema)) {
+ setCurrentNodeForm(
+ amisRender(
+ {
+ type: 'wrapper',
+ size: 'none',
+ body: [
+ {
+ type: 'tpl',
+ className: 'text-secondary',
+ tpl: description,
+ },
+ {
+ debug: commonInfo.debug,
+ title: name,
+ type: 'form',
+ wrapWithPanel: false,
+ onEvent: {
+ submitSucc: {
+ actions: [
+ {
+ actionType: 'custom',
+ // @ts-ignore
+ script: (context, action, event) => {
+ setDataById(id, context.props.data)
+ setOpen(false)
+ },
+ },
+ ],
+ },
+ },
+ body: [
+ ...(columnSchema ?? []),
+ {
+ type: 'wrapper',
+ size: 'none',
+ className: 'space-x-1 float-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)
+ }
+ }
+
+ useMount(() => {
+ for (let node of initialNodes) {
+ node.data = {
+ getDataById,
+ setDataById,
+ removeNode,
+ editNode,
+ }
+ }
+ setNodes(initialNodes)
+ setEdges(initialEdges)
+ })
+
+ return (
+
+ {contextHolder}
+
+
+ ({key: def.key, label: def.name})),
+ onClick: ({key}) => {
+ if (isEqual(key, 'start-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) {
+ messageApi.error('只能存在1个开始节点')
+ return
+ }
+ if (isEqual(key, 'end-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) {
+ messageApi.error('只能存在1个结束节点')
+ return
+ }
+
+ addNode({
+ id: randomId(10),
+ type: key,
+ position: {x: 100, y: 100},
+ data: {
+ getDataById,
+ setDataById,
+ removeNode,
+ editNode,
+ },
+ })
+ },
+ }}
+ >
+
+
+
+
+ {currentNodeForm}
+
+ def.key),
+ key => find(nodeDef, def => isEqual(key, def.key))!.component)
+ }
+ >
+
+
+
+
+
+ )
+}
+
+export default FlowEditor
\ No newline at end of file
diff --git a/service-web/client/src/pages/ai/flow/node/AmisNode.tsx b/service-web/client/src/pages/ai/flow/node/AmisNode.tsx
new file mode 100644
index 0000000..e9416ba
--- /dev/null
+++ b/service-web/client/src/pages/ai/flow/node/AmisNode.tsx
@@ -0,0 +1,69 @@
+import {Handle, type NodeProps, Position} from '@xyflow/react'
+import type {Schema} from 'amis'
+import {Card, Dropdown} from 'antd'
+import {DeleteFilled, EditFilled} from '@ant-design/icons'
+import {isEqual} from 'licia'
+
+export type AmisNodeType = 'normal' | 'start' | 'end'
+
+const AmisNode = (
+ props: NodeProps,
+ type: AmisNodeType,
+ name: String,
+ description?: String,
+ columnSchema?: Schema[],
+) => {
+ const {id, data} = props
+ const {getDataById, removeNode, editNode} = data
+ return (
+
+
+ ,
+ },
+ {
+ key: 'remove',
+ label: '删除',
+ icon: ,
+ },
+ ],
+ onClick: menu => {
+ switch (menu.key) {
+ case 'edit':
+ // @ts-ignore
+ editNode(id, name, description, columnSchema)
+ break
+ case 'remove':
+ // @ts-ignore
+ removeNode(id)
+ break
+ }
+ },
+ }}
+ >
+
+ {description}
+
+
+
+ {isEqual(type, 'start') || isEqual(type, 'normal')
+ ?
: undefined}
+ {isEqual(type, 'end') || isEqual(type, 'normal')
+ ? : undefined}
+
+ )
+}
+
+export default AmisNode
diff --git a/service-web/client/src/pages/ai/flow/node/EndNode.tsx b/service-web/client/src/pages/ai/flow/node/EndNode.tsx
new file mode 100644
index 0000000..05cbcc5
--- /dev/null
+++ b/service-web/client/src/pages/ai/flow/node/EndNode.tsx
@@ -0,0 +1,32 @@
+import type {NodeProps} from '@xyflow/react'
+import AmisNode from './AmisNode.tsx'
+
+const EndNode = (props: NodeProps) => AmisNode(
+ props,
+ 'end',
+ '结束节点',
+ '定义输出变量',
+ [
+ {
+ type: 'input-kvs',
+ name: 'fields',
+ addButtonText: '新增输出',
+ draggable: false,
+ keyItem: {
+ label: '参数名称',
+ },
+ valueItems: [
+ {
+ type: 'select',
+ name: 'type',
+ label: '参数',
+ required: true,
+ selectFirst: true,
+ options: [],
+ },
+ ],
+ },
+ ],
+)
+
+export default EndNode
\ No newline at end of file
diff --git a/service-web/client/src/pages/ai/flow/node/LlmNode.tsx b/service-web/client/src/pages/ai/flow/node/LlmNode.tsx
new file mode 100644
index 0000000..9bbec7c
--- /dev/null
+++ b/service-web/client/src/pages/ai/flow/node/LlmNode.tsx
@@ -0,0 +1,36 @@
+import type {NodeProps} from '@xyflow/react'
+import AmisNode from './AmisNode.tsx'
+
+const LlmNode = (props: NodeProps) => AmisNode(
+ props,
+ 'normal',
+ '大模型节点',
+ '使用大模型对话',
+ [
+ {
+ type: 'select',
+ name: 'model',
+ label: '大模型',
+ required: true,
+ selectFirst: true,
+ options: [
+ {
+ label: 'Qwen3',
+ value: 'qwen3',
+ },
+ {
+ label: 'Deepseek',
+ value: 'deepseek',
+ },
+ ],
+ },
+ {
+ type: 'textarea',
+ name: 'systemPrompt',
+ label: '系统提示词',
+ required: true,
+ },
+ ],
+)
+
+export default LlmNode
\ No newline at end of file
diff --git a/service-web/client/src/pages/ai/flow/node/StartNode.tsx b/service-web/client/src/pages/ai/flow/node/StartNode.tsx
new file mode 100644
index 0000000..617e85e
--- /dev/null
+++ b/service-web/client/src/pages/ai/flow/node/StartNode.tsx
@@ -0,0 +1,50 @@
+import type {NodeProps} from '@xyflow/react'
+import AmisNode from './AmisNode.tsx'
+
+const StartNode = (props: NodeProps) => AmisNode(
+ props,
+ 'start',
+ '开始节点',
+ '定义输入变量',
+ [
+ {
+ type: 'input-kvs',
+ name: 'fields',
+ addButtonText: '新增入参',
+ draggable: false,
+ keyItem: {
+ label: '参数名称',
+ },
+ valueItems: [
+ {
+ type: 'input-text',
+ name: 'description',
+ label: '参数描述',
+ },
+ {
+ type: 'select',
+ name: 'type',
+ label: '参数类型',
+ required: true,
+ selectFirst: true,
+ options: [
+ {
+ label: '文本',
+ value: 'text',
+ },
+ {
+ label: '数字',
+ value: 'number',
+ },
+ {
+ label: '文件',
+ value: 'files',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+)
+
+export default StartNode
\ No newline at end of file
diff --git a/service-web/client/src/pages/ai/flow/store/DataStore.ts b/service-web/client/src/pages/ai/flow/store/DataStore.ts
new file mode 100644
index 0000000..25e3aa6
--- /dev/null
+++ b/service-web/client/src/pages/ai/flow/store/DataStore.ts
@@ -0,0 +1,21 @@
+import {create} from 'zustand/react'
+
+export const useDataStore = create<{
+ data: Record,
+ getData: () => Record,
+ setData: (data: Record) => void,
+ getDataById: (id: string) => any,
+ setDataById: (id: string, data: any) => void,
+}>((set, get) => ({
+ data: {},
+ getData: () => get().data,
+ setData: (data) => set(data),
+ getDataById: id => get().data[id],
+ setDataById: (id, data) => {
+ let updateData = get().data
+ updateData[id] = data
+ set({
+ data: updateData,
+ })
+ },
+}))
\ No newline at end of file
diff --git a/service-web/client/src/pages/ai/flow/store/FlowStore.ts b/service-web/client/src/pages/ai/flow/store/FlowStore.ts
new file mode 100644
index 0000000..a1425d4
--- /dev/null
+++ b/service-web/client/src/pages/ai/flow/store/FlowStore.ts
@@ -0,0 +1,54 @@
+import {create} from 'zustand/react'
+import {
+ addEdge,
+ applyEdgeChanges,
+ applyNodeChanges,
+ type Edge,
+ type Node,
+ type OnConnect,
+ type OnEdgesChange,
+ type OnNodesChange
+} from '@xyflow/react'
+import {filter, isEqual} from 'licia'
+
+export const useFlowStore = create<{
+ nodes: Node[],
+ onNodesChange: OnNodesChange,
+ addNode: (node: Node) => void,
+ removeNode: (id: string) => void,
+ setNodes: (nodes: Node[]) => void,
+
+ edges: Edge[],
+ onEdgesChange: OnEdgesChange,
+ setEdges: (edges: Edge[]) => void,
+
+ onConnect: OnConnect,
+}>((set, get) => ({
+ nodes: [],
+ onNodesChange: changes => {
+ set({
+ nodes: applyNodeChanges(changes, get().nodes),
+ })
+ },
+ addNode: node => set({nodes: get().nodes.concat(node)}),
+ removeNode: id => {
+ set({
+ nodes: filter(get().nodes, node => !isEqual(node.id, id)),
+ })
+ },
+ setNodes: nodes => set({nodes}),
+
+ edges: [],
+ onEdgesChange: changes => {
+ set({
+ edges: applyEdgeChanges(changes, get().edges),
+ })
+ },
+ setEdges: edges => set({edges}),
+
+ onConnect: connection => {
+ set({
+ edges: addEdge(connection, get().edges),
+ })
+ },
+}))
diff --git a/service-web/client/src/route.tsx b/service-web/client/src/route.tsx
index 9ec7ce7..b9a6185 100644
--- a/service-web/client/src/route.tsx
+++ b/service-web/client/src/route.tsx
@@ -4,6 +4,7 @@ import {
ClusterOutlined,
CompressOutlined,
DatabaseOutlined,
+ GatewayOutlined,
InfoCircleOutlined,
OpenAIOutlined,
QuestionOutlined,
@@ -32,6 +33,7 @@ import Yarn from './pages/overview/Yarn.tsx'
import YarnCluster from './pages/overview/YarnCluster.tsx'
import Test from './pages/Test.tsx'
import {commonInfo} from './util/amis.tsx'
+import FlowEditor from './pages/ai/flow/FlowEditor.tsx'
export const routes: RouteObject[] = [
{
@@ -109,6 +111,10 @@ export const routes: RouteObject[] = [
path: 'knowledge/detail/:knowledge_id/segment/:group_id',
Component: DataSegment,
},
+ {
+ path: 'flowable',
+ Component: FlowEditor,
+ },
],
},
{
@@ -211,6 +217,11 @@ export const menus = {
name: '知识库',
icon: ,
},
+ {
+ path: '/ai/flowable',
+ name: '流程编排',
+ icon: ,
+ },
],
},
],