主要总结了下vue flow实现ai应用workflow一些核心实现点 

一、VueFlow基本构建

<template>
  <VueFlow
      class="agent-flow"
      :nodes="initData.nodes"
      :edges="initData.edges"
      :default-viewport="defaultViewport"
      :min-zoom="minZoom"
      :max-zoom="maxZoom"
      ref="vueFlowRef"
      :connection-mode="ConnectionMode.Strict"
      @init="handleInit"
  >
    <!--    自定义边-->
    <template #edge-button="buttonEdgeProps">
      <EdgeWithButton
          :id="buttonEdgeProps.id"
          :source-x="buttonEdgeProps.sourceX"
          :source-y="buttonEdgeProps.sourceY"
          :target-x="buttonEdgeProps.targetX"
          :target-y="buttonEdgeProps.targetY"
          :source-position="buttonEdgeProps.sourcePosition"
          :target-position="buttonEdgeProps.targetPosition"
          :marker-end="buttonEdgeProps.markerEnd"
          :style="buttonEdgeProps.style"
          :source-node="buttonEdgeProps.sourceNode"
          :target-node="buttonEdgeProps.targetNode"
      />
    </template>
    <Background :gap="14" :style="{ background: backgroundColor }"/>
    <!--    节点处理-->
    <template #node-start="{ id, data }">
      <AgentFlowNodeTypeCommon :id="id" :data="data"/>
    </template>
    <template #node-end="{ id, data }">
      <AgentFlowNodeTypeCommon :id="id" :data="data"/>
    </template>
    <template #node-model="{ id, data }">
      <AgentFlowNodeTypeCommon :id="id" :data="data"/>
    </template>
    <template #node-loop="{ id, data }">
      <AgentFlowNodeTypeNestParent :id="id" :data="data"/>
    </template>
    <template #node-nestedStart="{ id, data }">
      <AgentFlowNodeTypeNestStart :id="id" :data="data"/>
    </template>
  </VueFlow>
</template>

<script setup>
import {computed, nextTick, watch, ref} from "vue";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import {VueFlow, useVueFlow, Panel, ConnectionMode} from "@vue-flow/core";
import {Background} from "@vue-flow/background";
import AgentFlowNodeTypeCommon from "./AgentFlowNodeTypeCommon.vue";
import AgentFlowNodeTypeNestParent from "@/components/AgentFlowNodeTypeNestParent.vue";
import AgentFlowNodeTypeNestStart from "@/components/AgentFlowNodeTypeNestStart.vue";
import {useOperation} from "@/util/createNode.js";
import EdgeWithButton from "@/components/EdgeWithButton.vue";

const props = defineProps({
  data: {
    type: Object,
    required: true,
  },
  defaultViewport: {
    type: Object,
    required: true,
  },
  backgroundColor: {
    type: String,
    default: "#f5f5f5",
  },
  minZoom: {
    type: Number,
    default: 0.2,
  },
  maxZoom: {
    type: Number,
    default: 4,
  },
});
let initData = []
watch(props.data, () => {
  initData = props.data
}, {
  immediate: true,
})
const {
  nodes,
  onPaneClick,
  onConnect,
  edges,
  findNode,
  removeEdges,
  addEdges,
  updateNodeInternals
} = useVueFlow();
onPaneClick(e => {
  document.dispatchEvent(new MouseEvent('mousedown', {
    bubbles: true,
    cancelable: true
  }))
})
let {collectDescendantNodes} = useOperation();

onConnect((connectingEdge) => {
  // 如果已有相同连线则无需处理
  if (
      edges.value.find(
          (e) =>
              e.sourceHandle === connectingEdge.sourceHandle &&
              e.targetHandle === connectingEdge.targetHandle
      )
  ) {
    return
  }
  // 禁止形成循环
  if (connectingEdge.source === connectingEdge.target) {
    return
  }
  const targetDescendants = collectDescendantNodes(connectingEdge.target)
  if (targetDescendants.some((t) => t.id === connectingEdge.source)) {
    return
  }
  const targetId = connectingEdge.target
  const targetNode = findNode(targetId)
  const sourceId = connectingEdge.source
  const sourceNode = findNode(sourceId)
  const edgeId = parseInt(Math.random() * 1000) + '';
  // 添加连线
  const newEdge = {
    id: edgeId,
    type: "button",
    ...connectingEdge
  }
  const oldSourceEdges = edges.value.filter((e) => e.sourceHandle === connectingEdge.sourceHandle)
  const oldTargetEdges = edges.value.filter((e) => e.targetHandle === connectingEdge.targetHandle)

  const oldSourceTargets = oldSourceEdges.map((e) => e?.target)
  const oldTargetSources = oldTargetEdges.map((e) => e?.source)

  const collectRemovingEdges = []
  collectRemovingEdges.push(...oldSourceEdges)

  // 当target节点为分支合并,允许连接多个source节点
  // if (targetNode?.type !== "convergence") {
  //   collectRemovingEdges.push(...oldTargetEdges)
  // }
  const removingEdgeIds = [...collectRemovingEdges.map((e) => e?.id).filter(Boolean)]
  removeEdges(removingEdgeIds)
  addEdges([newEdge])
  nextTick(() => {
    const needUpdateNodes = [...new Set(
        [
          ...oldSourceTargets,
          ...oldTargetSources,
          connectingEdge.source,
          connectingEdge.target
        ].filter(Boolean)
    )]
    updateNodeInternals(needUpdateNodes)
  })
})
</script>

<style lang="less" scoped>
.agent-flow {
  --node-transition-duration: 0.3s;

  :deep(.vue-flow__node) {
    // 节点样式
    .agent-flow-node-content {
      transition-property: box-shadow;
      transition-duration: var(--node-transition-duration);
      transition-timing-function: ease-in-out;
    }

    &:hover .agent-flow-node-content,
    .agent-flow-node-content.agent-flow-node-content--selected {
      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.16) !important;
    }
  }

  :deep(.vue-flow__edge) {
    pointer-events: all;
  }
}
</style>

二、自定义节点

常规节点

<template>
  <AgentFlowNodeContent :id="id" :data="data"/>

  <AgentFlowSourceHandle
      :node-id="id"
      handle-type="source"
      :position="Position.Right"
  />

  <AgentFlowSourceHandle
      :node-id="id"
      handle-type="target"
      :position="Position.Left"
  />
</template>

<script setup>
import {Position} from '@vue-flow/core'
import AgentFlowSourceHandle from './AgentFlowSourceHandle.vue'
import AgentFlowNodeContent from './AgentFlowNodeContent.vue'

const props = defineProps({
  id: {
    type: String,
    required: true
  },
  data: {
    type: Object,
    required: true
  },
})
</script>

循环节点 

<template>
  <AgentFlowNodeContent :id="id" :data="data" />

  <AgentFlowSourceHandle
    v-if="sourceHandle"
    :node-id="id"
    handle-type="source"
    :position="Position.Right"
  />
  <AgentFlowSourceHandle
    v-if="targetHandle"
    :node-id="id"
    handle-type="target"
    :position="Position.Left"
  />
</template>

<script setup>
import { Position } from '@vue-flow/core'
import '@vue-flow/node-resizer/dist/style.css'
import AgentFlowSourceHandle from './AgentFlowSourceHandle.vue'
import AgentFlowNodeContent from './AgentFlowNodeContent.vue'

const props = defineProps({
  id: {
    type: String,
    required: true
  },
  data: {
    type: Object,
    required: true
  },
  sourceHandle: {
    type: Boolean,
    default: true
  },
  targetHandle: {
    type: Boolean,
    default: true
  }
})
</script>

<style lang="less" scoped></style>

循环内部开始节点

<template>
  <div class="agent-flow-node__nest-start  nodrag nopan">
    <div class="agent-start">
    </div>
    <div class="agent-add" v-if="!hasChildNode">
      <div class="line"></div>
      <a-popover
          trigger="click"
          v-model:open="open"
          :arrow="false"
          :trigger="['click']"
          placement="rightTop"
      >
        <button>
          <span>添加节点</span>
        </button>
        <template #content>
          <AgentFlowFloatAddMenu @click="addNode" :style="{position: 'static'}"></AgentFlowFloatAddMenu>
        </template>
      </a-popover>
    </div>
    <AgentFlowSourceHandle
        :node-id="id"
        :handle-id="sourceId"
        :show-add-button="false"
        handle-type="source"
        :position="Position.Right"
        :style="{top: '10px',pointerEvents: !hasChildNode ? 'none' : undefined}"
    />
  </div>

</template>

<script setup>
import {computed, inject, ref} from 'vue'
import {Position, useNode, useVueFlow} from '@vue-flow/core'
import AgentFlowSourceHandle from './AgentFlowSourceHandle.vue'
import AgentFlowFloatAddMenu from "@/components/AgentFlowFloatAddMenu.vue";
import {getSourceHandleId, useOperation} from "@/util/createNode.js";

const props = defineProps({
  id: {
    type: String,
    required: true
  },
  data: {
    type: Object,
    required: true
  }
})
const sourceId = computed(() => getSourceHandleId(props.id, "source"))
const open = ref(false);
const nestStartNode = useNode(props.id)
const parentNodeId = nestStartNode?.node?.parentNode
const {nodes} = useVueFlow()
const hasChildNode = computed(() => {
  if (!parentNodeId) {
    return []
  }
  const nestNodes = (nodes.value || []).filter((t) => t.parentNode === parentNodeId)
  const hasSomeNestNodes = nestNodes.length >= 2
  return hasSomeNestNodes
})
let {handleClick} = useOperation()
const addNode = (type) => {
  open.value = false
  handleClick(type, props.id)
}
</script>

<style lang="less" scoped>
.static-positon {
  position: static;
}

.agent-flow-node__nest-start {
  //background-color: red;
  position: relative;


  .agent-start {
    width: 20px;
    height: 20px;
    border-radius: 10px;
    //background: #00f;
    background-color: red;
  }

  .agent-add {
    position: absolute;
    width: 200px;
    display: flex;
    align-items: center;
    top: -1px;
    left: 20px;

    .line {
      height: 2px;
      width: 50px;
      background: #606266;
    }
  }

  .line {
    height: 1px;
  }
}
</style>

三、自定义边

<script setup>
import {BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow} from '@vue-flow/core'
import {computed, ref} from 'vue'
import AgentFlowFloatAddMenu from "@/components/AgentFlowFloatAddMenu.vue";
import {useOperation} from "@/util/createNode.js";

const props = defineProps({
  id: {
    type: String,
    required: true,
  },
  sourceX: {
    type: Number,
    required: true,
  },
  sourceY: {
    type: Number,
    required: true,
  },
  targetX: {
    type: Number,
    required: true,
  },
  targetY: {
    type: Number,
    required: true,
  },
  sourcePosition: {
    type: String,
    required: true,
  },
  targetPosition: {
    type: String,
    required: true,
  },
  markerEnd: {
    type: String,
    required: false,
  },
  style: {
    type: Object,
    required: false,
  },
  targetNode: {
    type: Object,
    required: false,
  },
  sourceNode: {
    type: Object,
    required: false,
  },
})

let {createNode, createEdge, insertNodeAt} = useOperation()
let {addNodes, addEdges, onNodesInitialized, updateNodeInternals, removeEdges, findNode, nodes} = useVueFlow()
const path = computed(() => getBezierPath(props))
let open = ref(false)
let addNodeFromEdge = (type) => {
  open.value = false
  let parentNodeId = props.sourceNode.parentNode
  let sourceNodeId = props.sourceNode.id
  let targetNodeId = props.targetNode.id
  removeEdges([props.id])
  const newNode = createNode(type);
  // 处理嵌套节点
  if (parentNodeId) {
    newNode.parentNode = parentNodeId
    newNode.expandParent = true
    newNode.data.bizData.parentNode = parentNodeId
  }
  const newEdgeOne = createEdge(sourceNodeId, newNode.id)
  const newEdgeTwo = createEdge(newNode.id, targetNodeId)
  // debugger
  addNodes(newNode);
  addEdges([newEdgeOne, newEdgeTwo]);

  const {off} = onNodesInitialized(() => {

    updateNodeInternals([sourceNodeId, targetNodeId])
    insertNodeAt(newNode.id, targetNodeId)
    if (parentNodeId) {
      const parentNode = findNode(parentNodeId)
      // 更新所有子节点、父节点
      const childrenIds = nodes.value
          .filter((t) => t.parentNode === parentNodeId)
          .map((t) => t.id)
      updateNodeInternals([parentNodeId, ...childrenIds])
    }
    off()
  })
}
</script>

<script>
export default {
  inheritAttrs: false,
}
</script>

<template>
  <!-- You can use the `BaseEdge` component to create your own custom edge more easily -->
  <BaseEdge :id="id" :style="style" :path="path[0]"/>

  <!-- Use the `EdgeLabelRenderer` to escape the SVG world of edges and render your own custom label in a `<div>` ctx -->
  <EdgeLabelRenderer>
    <a-popover
        v-model:open="open"
        :arrow="false"
        :trigger="['click']"
        placement="rightTop"
        :overlay-inner-style="{
      padding: '8px'
    }"
    >
      <div
          :style="{
        pointerEvents: 'all',
         cursor: 'pointer',
        position: 'absolute',
        transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,
      }"
          class="nodrag nopan"
      >
        <img
            src='/img/node-icon-add-next.svg'
            class="agent-flow-source-handle__add-icon"
        />
      </div>
      <template #content>
        <AgentFlowFloatAddMenu @click="addNodeFromEdge" :style="{position: 'static'}"></AgentFlowFloatAddMenu>
      </template>
    </a-popover>
  </EdgeLabelRenderer>
</template>
<style scoped>
.agent-flow-source-handle__add-icon {
  cursor: pointer;
}
</style>

三、 demo地址

https://github.com/chenjixue/flowEditor

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐