jbxl-workflow
Version:
流程图
761 lines (748 loc) • 28 kB
JavaScript
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