UNPKG

jbxl-workflow

Version:

流程图

761 lines (748 loc) 28 kB
import {Graph} from '@antv/x6' import {History} from '@antv/x6-plugin-history' import {Selection} from '@antv/x6-plugin-selection' import {Keyboard} from '@antv/x6-plugin-keyboard' import {generateRandomId} from "@dang_8899/xl-ui" // utils import {createGraphProxy} from '@/utils/proxy' // class import WorkflowToolbar from './Toolbar.class' import {h, nextTick, render} from "vue"; import Port from "@/components/Port.vue"; import {StartNode, EndNode, LLMNode, PluginNode, ParameterNode} from "./Node.class"; import NodePool from "@/NodePool.class"; import {encodeHTMLEntities, isHTMLEncoded, processString, replaceValuesByIds} from "@/utils/common"; import {Scroller} from "@antv/x6-plugin-scroller"; class WorkflowGraph extends Graph { static instance = null constructor(opts) { if (!opts.container) { console.warn('init workflow graph error, [container] must be a required attribute in arguments') return } const graphContainer = document.createElement('div') // 创建画布的容器节点 opts.container.appendChild(graphContainer) opts._container = opts.container opts.container = graphContainer opts.interacting = (cellView) => { // console.log('ccc', cellView, cellView?.cell?.getData?.()) return cellView?.cell?.getData?.()?.movable !== false // 如果 movable 为 false,则禁止移动 } opts = Object.assign({ background: { color: '#f5f7f9' }, grid: { size: 20, // 网格大小 10px visible: true, // 绘制网格,默认绘制 dot 类型网格 }, autoResize: true, // 是否监听容器大小改变 mousewheel: { enabled: true, modifiers: ['ctrl'], // 修饰键 }, scaling: { min: 0.2, // 缩放的最小值 max: 2 // 缩放的最大值 }, onPortRendered: async (args) => { const {port, contentSelectors} = args const container = contentSelectors?.foContent if (container) { await nextTick(() => { const {node} = args || {} const {id: nodeId} = node || {} const app = h(Port, {title: "添加节点", args}) const nodes = this.getNodes() || [] if (nodes && nodes.length) { // nodes.find(item => item.id === nodeId)?.vueInstance?.$emit('addNode') const res = nodes.find(item => item.id === nodeId) res?.data?.self?.setData({portsData: args.port}) } render(app, container) port.vueInstance = app }); // 等待 DOM 更新 } } }, opts) super(opts) if (WorkflowGraph.instance) { // WorkflowGraph.instance._container = opts._container // WorkflowGraph.instance._graphContainer = graphContainer // WorkflowGraph.instance.init() // WorkflowGraph.instance.initNode(opts) Object.assign(WorkflowGraph.instance, this) return WorkflowGraph.instance } this.currentDebugStatus = 0 // 当前调试状态,1可以进行下一步,0不可以进行下一步 this.history = null this._container = opts._container // 传入的容器节点 this._graphContainer = graphContainer // 画布的容器节点 this.stopSelected = false // 是否停止选中 this.toolbar = null // 工具栏实例 this.eventBus = this.createEventBus() this.initPluginFieldsNameMap() this.id = generateRandomId() this.tableJsonList = [] // 存储节点数据 this.allLoaded = false // 所有节点是否都加载完成 this.loadedCount = 0 // 加载完成的节点数量 this.blackList = ['store', 'listeners', 'animation'] globalThis._graph = WorkflowGraph.instance = createGraphProxy(this) // WorkflowGraph.instance.disableHistory() this.init() this.initNode(opts) this?.clearHistory() return WorkflowGraph.instance } async clearHistory () { await new Promise(resolve => setTimeout(resolve, 0)) await WorkflowGraph.instance.cleanHistory() // await WorkflowGraph.instance.enableHistory() } _getTableJson () { return this.tableJsonList } clear () { WorkflowGraph.instance = null } // 回显数据 // ... 现有代码 ... /** * 回显工作流数据 * @param {Object} data 工作流配置数据 */ _echoData(data) { if (!data) return; globalThis.ISRUNNING = false WorkflowGraph.instance.nodePool.clear() // 清空节点池 const { flowJson = [], tableJson = [] } = data; const portMap = new Map() // 用于存储端口数据, nodeId与port对象 // 还原节点 const nodeMap = new Map(); const nodePromises = flowJson.map(async nodeData => { const { id, x, y, portsData } = nodeData || {} let NodeClass; switch(nodeData.shapeType) { case 'start-node-shape': NodeClass = StartNode; break; case 'end-node-shape': NodeClass = EndNode; break; case 'llm-node-shape': NodeClass = LLMNode; break; case 'plugin-node-shape': NodeClass = PluginNode; break; case 'parameter-node-shape': NodeClass = ParameterNode; break; // 可以添加其他节点类型 } // 除开始结束节点之外的节点 if (NodeClass) { let {outputParamsData, basicDataList} = nodeData || {} if (basicDataList?.length) { // 先对 outputParamsData 自身根据 id 和 name 去重 const uniqueMap = new Map(); outputParamsData = outputParamsData.filter(item => { const key = `${item.id}-${item.name}`; if (!uniqueMap.has(key)) { uniqueMap.set(key, true); return true; } return false; }); // 再过滤掉与 basicDataList 重复的项 /** * 因为houseId是前端自动传给后端的,所以需要过滤掉 * */ nodeData.outputParamsData = outputParamsData.filter(item => !basicDataList.some(basicItem => basicItem.id === item.id))?.filter(item => item.value || item.result)?.filter(item => item.name !== 'houseId') } const node = new NodeClass({ ...nodeData, x: x || 30, y: y || 50, paramsDataList: nodeData.shapeType === 'start-node-shape' ? nodeData.paramsData?.slice(2) : nodeData.paramsData, ...(nodeData.shapeType !== 'start-node-shape' ? { outputParamsDataList: nodeData.outputParamsData } : []) }); const [nodeInstance] = WorkflowGraph.instance.addNode(node.getGraphPureParams()); if (nodeData.shapeType === 'start-node-shape') { WorkflowGraph.instance.nodePool.startNodeOrigin = node } WorkflowGraph.instance.nodePool.addNode(nodeInstance) nodeMap.set(id, nodeInstance); // this.nodePool?.addNode(nodeInstance); // 设置节点数据 // nodeInstance.data.self?.setData(nodeData); // 触发端口渲染 // console.log('nodeInstance', nodeInstance) portMap.set(id, {portsData: nodeInstance.data.portsData, nodeId: nodeInstance.nodeId}); return nodeInstance; } }); Promise.all(nodePromises).then(async nodes => { if (nodes?.length) { for (const node of nodes) { const res = await node const nextList = flowJson.find(item => item.id === node.id)?.nextList // 获取下游节点列表 if (nextList?.length && res) { const items = res?.data?.portsData?.items // 获取端口数据 const sourceItem = items?.find(item => item.id.indexOf('Output') > -1) nextList.forEach(nextListItem => { const {portsData, nodeId: nextItemNodeId} = portMap.get(nextListItem.id) || {} const targetItem = portsData.items?.find(item => item.id.indexOf('InputPort') > -1) res.nextList.push(WorkflowGraph.instance.nodePool.nodes.get(nextListItem.id)) WorkflowGraph.instance.nodePool.connect(res.id, nextListItem.id) // 将有关联的节点连在一起 WorkflowGraph.instance.addEdgeFunc({ source: { cell: res.nodeId, port: sourceItem.id}, target: { cell: nextItemNodeId, port: targetItem.id }, }) }) } } } }); } // 获取画布的容器节点 _getData () { const nodes = this.getNodes() const data = this.handleFunc(nodes) const flowJson = data.map(item => { if (!Array.isArray(item.nextList)) { item.nextList = [] } else { item.nextList.map(nextItem => { // 防止后端报错 if (nextItem.shapeType === 'llm-node-shape') { nextItem.systemPromptRichText = '' nextItem.currRoadInstructionRichText = '' } }) } if (!Array.isArray(item._prevList)) { item._prevList = [] } if (!Array.isArray(item.courtUuidList) || !item.courtUuidList || !item.courtUuidList.length) { item.courtUuidList = [] } // 当是结束节点时,需要将basicDataList合并到outputParamsData中 if (item.shapeType === 'end-node-shape') { const set = new Set() item.flowId = WorkflowGraph.instance.currentWorkflowData?.flowId // item.outputParamsData = item.outputParamsData || [] if (item.basicDataList?.length) { item.basicDataList = item.basicDataList.map(item => { const {fieldName} = item || {} const id = generateRandomId() set.add(id) return { ...item, name: fieldName, id, showDesc: item.outputType !== 2, showInputParamType: false, showInputParamVal: true, showRequired: false, showTableHeaderTitle: true, tableHeaderTitle: fieldName, isBasicData: true, } }) } item.outputParamsData = [ ...(item.outputParamsDataList.some(item => item.name === 'houseId') ? [] : [{ id: generateRandomId(), name: 'houseId', showDesc: item.outputType !== 2, showInputParamType: false, showInputParamVal: true, showRequired: false, showTableHeaderTitle: true, tableHeaderTitle: '小区id', type: 'String', value: 'houseId', isCustom: true // 手动添加上去的,并不是用户自行添加标志,如果碰到这个标志,在回显时,需要过滤掉 }]), ...(item.outputType === 2 ? item.basicDataList : []), // 如果不是物业报表则不需要添加basicDataList ...(item.outputParamsDataList?.filter(item => !set.has(item.id)) || []), ] if (item.outputType < 3) { this.tableJsonList = item.outputParamsData?.length && item.outputParamsData?.map(item => { const {tableHeaderTitle, valueType, example, replaceName, filterType, name, value, isBasicData} = item || {} if (tableHeaderTitle && name) { return { value: isBasicData ? value : tableHeaderTitle, fieldName: name, type: valueType, example: example || '', replaceName: replaceName || '', filterType: filterType || [] } } }).filter(item => item) // item.tableJsonList = this.tableJsonList } } if (item.shapeType === 'llm-node-shape') { const model = {} const {list} = item.paramsData || {} if (list?.length) { for (const item of list) { const {id, result, value} = item || {} model[id] = result || value } } } item.whiteList = [] return item }) return { flowJson, tableJson: this.tableJsonList } } /** * @param nodes {Array<any>} 节点数组 * */ handleFunc(nodes) { return nodes?.map(item => { const { self } = item.data || {} const obj = {} for (const key in self) { const item = self[key] if (!this.blackList.includes(key)) { if (key) { if (key === 'paramsData' || key === 'outputParamsData') { // 特殊处理 paramsData 和 outputParamsData // console.log('ffff', item) obj[key] = { ...item, list: item?.list || [] // 保留 list 数组 } } else if (Array.isArray(item)) { // 检查数组的第一个元素类型 const firstItem = item[0] if (typeof firstItem !== 'object' || firstItem === null) { // 如果是基本类型数组,直接保留 obj[key] = item } else { // 如果是对象数组,进行安全处理 obj[key] = item.map(subItem => { const safeObj = {} for (const subKey in subItem) { if (typeof subItem[subKey] !== 'function' && typeof subItem[subKey] !== 'object') { safeObj[subKey] = subItem[subKey] } } return safeObj }) } } else if (typeof item === 'object') { // 对对象进行安全处理 const safeObj = {} for (const itemKey in item) { if (typeof item[itemKey] !== 'function' && typeof item[itemKey] !== 'object') { safeObj[itemKey] = item[itemKey] } } obj[key] = safeObj } else { // 基本类型直接赋值 obj[key] = item } } else { // 非白名单属性,如果是基本类型则直接赋值 if (typeof item !== 'function' && typeof item !== 'object') { obj[key] = item } } } } return Object.assign(obj, {...self?.getData() || {}}) }) } // 创建事件总线 createEventBus () { return { events: { 'inner-event': { // 内部事件 selectNode: [] }, 'start-node-shape': { // 开始节点 default: [], run: [], queryStatus: [], // 获取是否能进行下一步的状态 updateStatus: [], // 更新是否能进行下一步状态 }, 'plugin-node-shape': { // 插件节点 default: [], init: [], getPluginDetail: [], run: [] }, 'parameter-node-shape': { // 参数提取节点 default: [], init: [], getPluginDetail: [], run: [] }, 'llm-node-shape': { // llm节点 default: [], run: [] }, 'end-node-shape': { // 结束节点 default: [], getDefaultChecked: [], setDefaultChecked: [], open: [], submit: [], uploadFile: [], getWorkflowData: [], closeMessage: [], result: [] // 最后运行结果 }, 'flow-debug': { // 流程调试 run: [], showSelectDialoguePopup: [], confirmSelectDialogue: [] }, 'echo': { default: [] } }, on(eventName, callbacks) { // 用法on('start-node-shape:default', callbacks) // 通过不同的event来调用不同的回调函数 const [nodeName, event = 'default'] = (eventName || '').split(':') if (!this.events[nodeName]?.[event]) { throw new Error(`事件名称 ${nodeName}:${event} 不存在`) } if (nodeName === 'inner-event') { // 特殊处理内部事件的订阅事件 this.events[nodeName][event]?.push(callbacks) } else { this.events[nodeName][event] = callbacks || [] } }, emit(eventName, ...args) { const [nodeName, event = 'default'] = (eventName || '').split(':') const callbacks = this.events[nodeName]?.[event] || [] if (!callbacks.length) return false return Promise.all(callbacks?.map?.((cb) => cb(...args)) || []) // callbacks.forEach((cb) => cb(...args)) } } } addEdgeFunc ({source, target, color = '#D2D6DD', lineWidth = 2}) { WorkflowGraph.instance.addEdge({ source: {...source, anchor: { name: 'center' }, connectionPoint: 'anchor'}, target: {...target, anchor: { name: 'center' }, connectionPoint: 'anchor'}, connector: { name: 'smooth', args: { direction: 'H' // 水平方向的连线 } }, router: { name: 'er', // 使用 er 路由,避免穿过节点 args: { direction: 'H', offset: 'center' } }, attrs: { line: { stroke: color, strokeWidth: lineWidth, targetMarker: 'classic', name: 'block', size: 10, // 箭头大小与线宽一致 } }, zIndex: 1 // 降低连线的层级,确保在节点下方 }) } createEdgeFunc ({source, color = '#7B52FE', lineWidth = 2}) { return WorkflowGraph.instance.createEdge({ source: {...source, anchor: { name: 'center' }, connectionPoint: 'anchor'}, // target: {...target, anchor: { name: 'center' }, connectionPoint: 'anchor'}, connector: { name: 'smooth' }, attrs: { line: { stroke: color, strokeWidth: lineWidth, targetMarker: 'classic', name: 'block', size: 10, // 箭头大小与线宽一致 } } }) } // 自定义插件数据相关字段的映射关系 initPluginFieldsNameMap(pluginFieldsNameMap = {}) { this.pluginFieldsNameMap = Object.assign({ publishedStatusValue: 2, // 插件发布状态值, 默认为2(已发布) detailPagePath: '/plugin/detail', // 插件详情跳转路径 categoryFieldsMap: { // 插件分类列表的字段映射 key: 'key', // 分类值 value: 'value', // 分类名称 }, infoFieldsMap: { // 插件列表的字段映射 id: 'plugId', // 插件 id 的对应字段 status: 'publishStatus', // 插件状态的对应字段 category: 'plugType', // 插件分类的对应字段 iconUrl: 'plugLogoUrl', // 插件图标 url 的对应字段 name: 'plugName', // 插件名称的对应字段 description: 'plugDescr', // 插件描述的对应字段 totalCallNum: 'totalCallNum', // 插件 api 调用次数的对应字段 author: 'createUserName', // 插件创建者名称的对应字段 updateTime: 'updateTime', // 插件更新时间的对应字段 }, apiInfoFieldsMap: { // 插件 api 列表的字段映射 apiDocListFieldsName: 'apiDocList', // 插件 api 列表的对应字段 apiIdFieldsName: 'apiId', // 插件 api 列表中 id 的对应字段 apiDocFieldsName: 'apiDoc', // 插件 api 列表中 yaml 字符串的对应字段 apiRunResponseCodeFieldsName: 'code', // 插件 api 运行时响应状态码的对应字段 apiRunResponseSuccessCode: '00000', // 插件 api 运行时响应成功状态码的值 paramsDataFieldsName: 'yamlParseData', // 插件 api 详情信息的对应字段 apiFieldsName: 'apiName', // 插件 api 名字的对应字段 apiDescriptionFieldsName: 'description', // 插件 api 描述的对应字段 inputParamsFieldsName: 'reqList', // 插件 api 输入参数列表的对应字段 outputParamsFieldsName: 'respList', // 插件 api 输出参数列表的对应字段 }, inputParamsFieldsMap: { // 插件输入参数的字段映射 paramName: 'paramName', // 入参名称的对应字段 paramType: 'paramType', // 入参类型的对应字段 paramDefaultValue: 'defaultValue', // 入参默认值的对应字段 paramDescription: 'paramDesc', // 入参描述的对应字段 }, outputParamsFieldsMap: { // 插件输出参数的字段映射 paramName: 'paramName', // 出参名称的对应字段 paramType: 'paramType', // 出参类型的对应字段 paramDefaultValue: 'defaultValue', // 出参默认值的对应字段 paramDescription: 'paramDesc', // 出参描述的对应字段 children: 'children' // 出参子参数的对应字段 } }, pluginFieldsNameMap) } // 初始化画布及其插件 init() { // this.initScroller() this.initToolbar() this.initHistory() this.initSelection() this.initKeyboard() this.initNodeEvent() } // 初始化开始节点以及结束节点 initNode (options = {}) { const {defaultConfig, flowJson, tableJson} = options || {} WorkflowGraph.instance.nodePool = new NodePool() // 统一在开始时设置 nodePool WorkflowGraph.instance.eventBus?.on('end-node-shape:getWorkflowData', [(data) => { if (data && Object.keys(data)?.length) { WorkflowGraph.instance.currentWorkflowData = data } }]) WorkflowGraph.instance.eventBus.on('start-node-shape:queryStatus', [(debugStatus) => { this.currentDebugStatus = debugStatus }]) if (defaultConfig && !flowJson?.length) { const startNodeData = { title: '开始', description: '工作流的起点,设置开始工作流需要的信息', x: 30, y: 50, allIsOpen: true, } const endNodeData = { title: '结束', description: '工作流的终点节点', x: 1156, y: 50, allIsOpen: true, shapeType: 'end-node-shape', } const startNode = new StartNode(startNodeData) const endNode = new EndNode(endNodeData) const [startNodeInstance] = WorkflowGraph.instance.addNode(startNode.getGraphPureParams()) const [endNodeInstance] = WorkflowGraph.instance.addNode(endNode.getGraphPureParams()) WorkflowGraph.instance.nodePool.startNodeOrigin = startNode WorkflowGraph.instance.nodePool.addNode(startNodeInstance).addNode(endNodeInstance) } else { WorkflowGraph.instance._echoData({flowJson, tableJson}) } } // 初始化工具栏 initToolbar() { this.toolbar = new WorkflowToolbar({ container: this._container, graph: this }) } // 初始化历史记录插件 initHistory() { const history = new History() this.history = history this.use(history) } initScroller () { this.use( new Scroller({ enabled: true, pannable: true, pageVisible: false, pageBreak: false, autoResize: true, padding: 0, minVisibleWidth: '100%', minVisibleHeight: '100%' }), ) // 设置容器样式 if (this._graphContainer) { this._graphContainer.style.width = '100%' this._graphContainer.style.height = '100%' this._graphContainer.style.position = 'absolute' this._graphContainer.style.left = '0' this._graphContainer.style.top = '0' } } // 初始化选择插件 initSelection() { let timer = null, selectedNodeList = [] this.use(new Selection({ rubberband: true, // 启用框选 movable: true })) // 添加节点点击事件监听 this.on('node:click', ({ e }) => { // 如果点击源是 select 相关元素,则不触发节点选择 this.stopSelected = !!(e?.target?.closest('.el-select') || e?.target?.closest('.el-select-dropdown') || e?.target?.closest('.el-popper')); }) this.on('node:selected', (obj) => { const {node} = obj || {} selectedNodeList = [] selectedNodeList.push(node) if (timer) { clearTimeout(timer) timer = null } // timer && clearTimeout(timer) timer = setTimeout(() => { if (this.stopSelected) return this.eventBus.emit('inner-event:selectNode', [...selectedNodeList]) // selectedNodeList = [] }, 10) }) this.on('node:unselected', () => { this.eventBus.emit('inner-event:selectNode', []) }) this.on('edge:selected', ({ edge }) => { this.selectedEdgeSourceNode = edge.getSourceNode() this.selectedEdgeTargetNode = edge.getTargetNode() edge.attr('line/stroke', '#7B52FE') edge.addTools([ { name: 'button-remove', args: { distance: '50%', markup: [ { tagName: 'circle', attrs: { fill: '#7B52FE', // 背景颜色 }, } ] } } ]) }) this.on('edge:unselected', ({ edge }) => { this.selectedEdgeSourceNode = null this.selectedEdgeTargetNode = null edge.attr('line/stroke', '#D2D6DD') edge.removeTools() }) this.on('edge:removed', () => { this.toolbar.removeEdge(this.selectedEdgeSourceNode, this.selectedEdgeTargetNode) }) } // 初始化键盘插件 initKeyboard() { this.use(new Keyboard()) // meta是开平台的快捷键标识,在mac上是command键 // 撤销快捷键 (Windows: Ctrl+Z, MacOS: Command+Z) this.bindKey(['ctrl+z', 'meta+z'], () => { this.toolbar.undo() }) // 重做快捷键 (Windows: Ctrl+Y, MacOS: Command+Shift+Z) this.bindKey(['ctrl+y', 'meta+shift+z'], () => { this.toolbar.redo() }) // 拖动画布快捷键 let isKeyDown = false let longPressTimer = null // 监听 keydown 事件 document.addEventListener('keydown', (event) => { if ((event.code === 'Space' || event.key === ' ') && !isKeyDown) { isKeyDown = true longPressTimer = setTimeout(() => { this.toolbar.startDrag() // 禁用节点拖动 this.options.interacting = () => false }, 10) } }) // 监听 keyup 事件 document.addEventListener('keyup', (event) => { if (event.code === 'Space' || event.key === ' ') { isKeyDown = false clearTimeout(longPressTimer) this.toolbar.startSelect() // 恢复节点拖动 this.options.interacting = (cellView) => cellView?.cell?.getData?.()?.movable !== false } }) // 删除快捷键 this.bindKey(['backspace', 'delete'], () => { const cells = this.getSelectedCells() const nodes = cells.filter(cell => cell.isNode()) // 获取所有被选中的节点 const edges = cells.filter(cell => cell.isEdge()) // 获取所有被选中的连线 if (nodes.length) { this.toolbar.removeNode() } else { edges.forEach(edge => { this.removeEdge(edge) }) this.toolbar.removeEdge(this.selectedEdgeSourceNode, this.selectedEdgeTargetNode) } }) // 复制快捷键 (Windows: Ctrl+C, MacOS: Command+C) this.bindKey(['ctrl+c', 'meta+c'], () => { this.toolbar.copyNode() }) // 粘贴快捷键 (Windows: Ctrl+V, MacOS: Command+V) this.bindKey(['ctrl+v', 'meta+v'], () => { this.toolbar.pasteNode() }) } initNodeEvent() { this.on('node:mousedown', ({e}) => { // e.preventDefault() // 阻止默认行为 e.stopPropagation() // 阻止冒泡 }) this.on('node:moved', ({ node, x, y }) => { // 节点移动结束后更新 x, y 的值 const pos = node.position() const data = node.getData() data.self?.setData({ x: pos.x, y: pos.y }) }) } } export default WorkflowGraph