NodeView.vue 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526
  1. <template>
  2. <div
  3. class="relative h-full w-full"
  4. ref="workflowWrapperRef"
  5. @drop="onDrop"
  6. @contextmenu="handleWorkflowContextMenu"
  7. >
  8. <Workflow
  9. ref="workflowRef"
  10. :id="workflow?.id"
  11. :workflow="workflowWithExecutionState"
  12. :nodeMap="nodeMap"
  13. :autoFit="false"
  14. @click:node="handleSelectNode"
  15. @dblclick:node="handleNodeClick"
  16. @create:node="handleNodeCreate"
  17. @create:connection:end="onCreateConnection"
  18. @drag-and-drop="handleDrop"
  19. @run:node="handleRunNode"
  20. @update:nodes:position="handleUpdateNodesPosition"
  21. @update:node:attrs="handleUpdateNodeProps"
  22. @delete:node="handleDeleteNode"
  23. @delete:connection="handleDeleteEdge"
  24. @dragover="onDragOver"
  25. @dragleave="onDragLeave"
  26. @create:connection:cancelled="onConnectionOpenNodeLibary"
  27. @click:connection:add="handleClickConectionAdd"
  28. @viewport:change="handleViewportChange"
  29. class="bg-#f5f5f5"
  30. >
  31. <Toolbar
  32. @create:node="handleNodeCreate"
  33. @run="handleRunAgent"
  34. @chat="handleOpenWorkflowChat"
  35. :env-vars="workflow?.env_variables || []"
  36. :run-nodes="toolbarRunNodes"
  37. :can-chat="canRunWorkflowChat"
  38. @change-env-vars="handleChangeEnvVars"
  39. />
  40. </Workflow>
  41. <StartNodeGuide
  42. v-if="showStartNodeGuide"
  43. @create-node="handleStartNodeGuideCreate"
  44. @close="hideStartNodeGuide"
  45. />
  46. </div>
  47. <RunWorkflow
  48. v-model:visible="runVisible"
  49. :workflow="workflow"
  50. :close-on-run="closeRunWorkflowOnSubmit"
  51. :input-only="runWorkflowInputOnly"
  52. :active-node-id="runWorkflowNodeId"
  53. :initial-tab="runWorkflowInitialTab"
  54. @run-started="handleWorkflowRunStarted"
  55. />
  56. <ChatDrawer
  57. v-model:visible="workflowChatVisible"
  58. :workflow="workflow"
  59. :start-node="workflowChatStartNode"
  60. :visible-variables="workflowChatVisibleVariables"
  61. :input-values="workflowChatInputValues"
  62. :json-drafts="workflowChatJsonDrafts"
  63. :validation-errors="workflowChatValidationErrors"
  64. :base-params="workflowChatBaseParams"
  65. :is-running="workflowChatRunning"
  66. @validate-send="handleWorkflowChatValidateSend"
  67. @run-started="handleWorkflowChatRunStarted"
  68. @cancel="handleWorkflowChatCancel"
  69. />
  70. <Setter
  71. :id="nodeID"
  72. :workflow="workflow"
  73. :active-tab="setterActiveTab"
  74. @update:node:data="handleUpdateNode"
  75. @run-node="handleRunNode"
  76. v-model:visible="setterVisible"
  77. />
  78. <el-popover
  79. v-if="libaryRefferenceRef"
  80. :visible="showNodeLibary"
  81. trigger="manual"
  82. placement="bottom-start"
  83. :show-arrow="false"
  84. :append-to="workflowWrapperRef"
  85. :virtual-ref="libaryRefferenceRef"
  86. :popper-options="nodeLibaryPopperOptions"
  87. width="360px"
  88. virtual-triggering
  89. >
  90. <div ref="nodeLibraryPanelRef" class="node-library-popover-panel">
  91. <NodeLibary
  92. @add-node="handleNodeCreateFromLibrary"
  93. :parent-node-type="nodeLibaryParentType"
  94. hide-start
  95. ignore-drag
  96. />
  97. </div>
  98. </el-popover>
  99. <Teleport to="body">
  100. <div
  101. v-if="contextMenuVisible"
  102. ref="contextMenuRef"
  103. class="workflow-context-menu"
  104. :style="contextMenuStyle"
  105. @contextmenu.prevent
  106. >
  107. <button type="button" class="workflow-context-menu__item" @click="handleContextAddNode">
  108. <Icon icon="lucide:plus" class="workflow-context-menu__icon">+</Icon>
  109. <span>{{ t('pages.editor.addNode') }}</span>
  110. </button>
  111. <button type="button" class="workflow-context-menu__item" @click="handleContextAddStickyNote">
  112. <Icon icon="lucide:file-plus-corner" class="workflow-context-menu__icon"></Icon>
  113. <span>{{ t('pages.editor.addNote') }}</span>
  114. </button>
  115. <button
  116. type="button"
  117. class="workflow-context-menu__item"
  118. :class="{ 'is-disabled': !selectedNodeId }"
  119. @click="handleContextRunNode"
  120. >
  121. <Icon icon="lucide:play" class="workflow-context-menu__icon"></Icon>
  122. <span>{{ t('pages.editor.testRun') }}</span>
  123. </button>
  124. </div>
  125. </Teleport>
  126. </template>
  127. <script setup lang="ts">
  128. import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
  129. import { ElMessage, ElMessageBox } from 'element-plus'
  130. import { agent } from '@repo/api-service'
  131. import { cloneDeep, isEqual } from 'lodash-es'
  132. import RunWorkflow from '@/features/RunWorkflow/index.vue'
  133. import ChatDrawer from '@/features/ChatDrawer/index.vue'
  134. import NodeLibary from '@/features/nodeLibary/index.vue'
  135. import Toolbar from '@/features/toolbar/index.vue'
  136. import Setter from '@/features/setter/index.vue'
  137. import StartNodeGuide from './StartNodeGuide.vue'
  138. import { nodeMap } from '@/nodes'
  139. import { getNodeDisplayName } from '@/nodes/i18n'
  140. import { useI18n } from '@/composables/useI18n'
  141. import { useRunnerStore } from '@/store/modules/runner.store'
  142. import { buildExecuteParams, createEmptyValue } from '@/features/RunWorkflow/utils'
  143. import { Workflow, useDragAndDrop } from '@repo/workflow'
  144. import { useDebounceFn } from '@vueuse/core'
  145. import { Icon } from '@repo/ui'
  146. import type {
  147. CanvasExecutionStatus,
  148. Connection,
  149. ConnectStartEvent,
  150. IWorkflow,
  151. IWorkflowNode,
  152. XYPosition
  153. } from '@repo/workflow'
  154. import type { StartVariable } from '@/nodes/src/start'
  155. interface Props {
  156. workflow: IWorkflow
  157. reloadWorkflow: (agentId: string) => Promise<void>
  158. }
  159. const props = defineProps<Props>()
  160. const { t } = useI18n()
  161. // 将运行器状态映射为画布展示用的状态值。
  162. const mapNodeExecutionStatus = (status?: string): CanvasExecutionStatus => {
  163. if (status === 'running') {
  164. return 'running'
  165. }
  166. if (status === 'success') {
  167. return 'success'
  168. }
  169. if (status === 'failed') {
  170. return 'warning'
  171. }
  172. if (status === 'suspended') {
  173. return 'suspended'
  174. }
  175. return 'idle'
  176. }
  177. const MIN_NODE_RUNNING_EFFECT_MS = 500
  178. const runnerStore = useRunnerStore()
  179. const workflowWrapperRef = ref<HTMLElement>()
  180. const workflowRef = ref<InstanceType<typeof Workflow>>()
  181. const showNodeLibary = ref(false)
  182. const libaryRefferenceRef = ref<HTMLElement>()
  183. const nodeLibaryPopoverAnchorRef = ref<HTMLElement>()
  184. const nodeLibraryPanelRef = ref<HTMLElement>()
  185. const nodeLibaryPopperOptions = {
  186. modifiers: [
  187. { name: 'flip', enabled: false },
  188. { name: 'preventOverflow', enabled: false }
  189. ]
  190. }
  191. const contextMenuRef = ref<HTMLElement>()
  192. const contextMenuVisible = ref(false)
  193. const contextMenuPosition = ref({ x: 0, y: 0 })
  194. const contextMenuFlowPosition = ref<XYPosition>({ x: 0, y: 0 })
  195. const dismissedStartNodeGuideWorkflowId = ref('')
  196. const runVisible = ref(false)
  197. const closeRunWorkflowOnSubmit = ref(false)
  198. const runWorkflowInputOnly = ref(false)
  199. const runWorkflowNodeId = ref('')
  200. const runWorkflowInitialTab = ref<'input' | 'trigger' | 'result' | 'detail' | 'trace'>('input')
  201. const workflowChatVisible = ref(false)
  202. const workflowChatInputValues = ref<Record<string, any>>({})
  203. const workflowChatJsonDrafts = ref<Record<string, string>>({})
  204. const workflowChatValidationErrors = ref<Record<string, string>>({})
  205. const workflowChatBaseParams = ref<Record<string, any>>({})
  206. const setterVisible = ref(false)
  207. const nodeID = ref('')
  208. const setterActiveTab = ref<'setting' | 'last-run'>('setting')
  209. const displayNodeExecutionStatus = ref<Record<string, CanvasExecutionStatus>>({})
  210. const shouldAutoCenterAfterInit = ref(true)
  211. const peddingHandlePayload = ref<{
  212. by?: 'node' | 'edge'
  213. handle?: ConnectStartEvent
  214. position: XYPosition
  215. event?: MouseEvent
  216. parentId?: string
  217. connection?: Connection & { id: string }
  218. }>()
  219. const runningStatusStartedAt = new Map<string, number>()
  220. const pendingNodeStatusTimers = new Map<string, number>()
  221. let hideNodeLibaryTimer: number | undefined
  222. const pendingEdges = ref<
  223. Array<Connection & { id: string; type?: string; data?: Record<string, unknown> }>
  224. >([])
  225. const pendingNodes = ref<IWorkflowNode[]>([])
  226. const selectedNodeId = computed(() => {
  227. const selectedNode = props.workflow?.nodes?.find((node) => node.selected)
  228. return selectedNode?.id || ''
  229. })
  230. const contextMenuStyle = computed(() => ({
  231. left: `${contextMenuPosition.value.x}px`,
  232. top: `${contextMenuPosition.value.y}px`
  233. }))
  234. const hasWorkflowNodes = computed(() => {
  235. return !!(props.workflow?.nodes?.length || pendingNodes.value.length)
  236. })
  237. const showStartNodeGuide = computed(() => {
  238. return (
  239. !!props.workflow?.id &&
  240. !hasWorkflowNodes.value &&
  241. dismissedStartNodeGuideWorkflowId.value !== props.workflow.id
  242. )
  243. })
  244. const removeNodeLibaryPopoverAnchor = () => {
  245. nodeLibaryPopoverAnchorRef.value?.remove()
  246. nodeLibaryPopoverAnchorRef.value = undefined
  247. }
  248. const createNodeLibaryPopoverAnchor = (position?: { x: number; y: number }) => {
  249. removeNodeLibaryPopoverAnchor()
  250. if (!position) {
  251. return undefined
  252. }
  253. const anchor = document.createElement('div')
  254. anchor.style.position = 'fixed'
  255. anchor.style.left = `${position.x}px`
  256. anchor.style.top = `${position.y}px`
  257. anchor.style.width = '1px'
  258. anchor.style.height = '1px'
  259. anchor.style.pointerEvents = 'none'
  260. document.body.appendChild(anchor)
  261. nodeLibaryPopoverAnchorRef.value = anchor
  262. return anchor
  263. }
  264. const closeContextMenu = () => {
  265. contextMenuVisible.value = false
  266. }
  267. const getFlowPositionFromMouseEvent = (event: MouseEvent): XYPosition => {
  268. const screenToFlowCoordinate = workflowRef.value?.getVueFlow()?.screenToFlowCoordinate
  269. if (screenToFlowCoordinate) {
  270. return screenToFlowCoordinate({
  271. x: event.clientX,
  272. y: event.clientY
  273. })
  274. }
  275. const viewport = workflowRef.value?.getVueFlow()?.viewport?.value
  276. const bounds = workflowWrapperRef.value?.getBoundingClientRect()
  277. if (viewport && bounds) {
  278. return {
  279. x: (event.clientX - bounds.left - viewport.x) / viewport.zoom,
  280. y: (event.clientY - bounds.top - viewport.y) / viewport.zoom
  281. }
  282. }
  283. return { x: 0, y: 0 }
  284. }
  285. // 在为节点安排新的状态切换前,先清理旧的延时任务。
  286. const clearPendingNodeStatusTimer = (nodeId: string) => {
  287. const timer = pendingNodeStatusTimers.get(nodeId)
  288. if (timer) {
  289. window.clearTimeout(timer)
  290. pendingNodeStatusTimers.delete(nodeId)
  291. }
  292. }
  293. // 将节点执行状态写入展示缓存,空闲状态则直接清理。
  294. const applyDisplayedNodeStatus = (nodeId: string, status: CanvasExecutionStatus) => {
  295. if (status === 'idle') {
  296. delete displayNodeExecutionStatus.value[nodeId]
  297. return
  298. }
  299. displayNodeExecutionStatus.value[nodeId] = status
  300. }
  301. // 保证 running 态至少展示一小段时间,避免状态闪烁。
  302. const syncDisplayedNodeStatus = (nodeId: string, nextStatus: CanvasExecutionStatus) => {
  303. const currentStatus = displayNodeExecutionStatus.value[nodeId] || 'idle'
  304. if (nextStatus === 'running') {
  305. clearPendingNodeStatusTimer(nodeId)
  306. runningStatusStartedAt.set(nodeId, Date.now())
  307. applyDisplayedNodeStatus(nodeId, 'running')
  308. return
  309. }
  310. const runningStartedAt = runningStatusStartedAt.get(nodeId)
  311. const shouldKeepRunning =
  312. currentStatus === 'running' &&
  313. typeof runningStartedAt === 'number' &&
  314. Date.now() - runningStartedAt < MIN_NODE_RUNNING_EFFECT_MS
  315. if (shouldKeepRunning) {
  316. clearPendingNodeStatusTimer(nodeId)
  317. const delay = MIN_NODE_RUNNING_EFFECT_MS - (Date.now() - runningStartedAt)
  318. const timer = window.setTimeout(() => {
  319. applyDisplayedNodeStatus(nodeId, nextStatus)
  320. runningStatusStartedAt.delete(nodeId)
  321. pendingNodeStatusTimers.delete(nodeId)
  322. }, delay)
  323. pendingNodeStatusTimers.set(nodeId, timer)
  324. return
  325. }
  326. clearPendingNodeStatusTimer(nodeId)
  327. applyDisplayedNodeStatus(nodeId, nextStatus)
  328. runningStatusStartedAt.delete(nodeId)
  329. }
  330. // 在运行器切换或组件卸载时重置所有执行态缓存。
  331. const resetDisplayedNodeStatuses = () => {
  332. pendingNodeStatusTimers.forEach((timer) => window.clearTimeout(timer))
  333. pendingNodeStatusTimers.clear()
  334. runningStatusStartedAt.clear()
  335. displayNodeExecutionStatus.value = {}
  336. }
  337. watch(
  338. () => runnerStore.currentRunnerKey,
  339. () => {
  340. resetDisplayedNodeStatuses()
  341. }
  342. )
  343. watch(
  344. () => runnerStore.nodes.map((item) => ({ nodeId: item.nodeId, status: item.status })),
  345. (nodeStates) => {
  346. const activeNodeIds = new Set(nodeStates.map((item) => item.nodeId))
  347. nodeStates.forEach((item) => {
  348. syncDisplayedNodeStatus(item.nodeId, mapNodeExecutionStatus(item.status))
  349. })
  350. Object.keys(displayNodeExecutionStatus.value).forEach((nodeId) => {
  351. if (!activeNodeIds.has(nodeId)) {
  352. syncDisplayedNodeStatus(nodeId, 'idle')
  353. }
  354. })
  355. },
  356. { immediate: true, deep: true }
  357. )
  358. // 将运行时执行状态合并到当前工作流数据,再交给画布渲染。
  359. const workflowWithExecutionState = computed(() => {
  360. const baseWorkflow = props.workflow
  361. const nodes: IWorkflow['nodes'] = (baseWorkflow.nodes || []).map((node): IWorkflowNode => {
  362. const executionStatus: CanvasExecutionStatus =
  363. displayNodeExecutionStatus.value[node.id] || 'idle'
  364. return {
  365. ...node,
  366. executionStatus,
  367. data: {
  368. ...(node.data || {}),
  369. executionStatus
  370. }
  371. }
  372. })
  373. const stableNodeIds = new Set((baseWorkflow.nodes || []).map((node) => node.id))
  374. const displayPendingNodes = pendingNodes.value.filter((node) => !stableNodeIds.has(node.id))
  375. const stableEdgeKeys = new Set(
  376. (baseWorkflow.edges || []).map(
  377. (edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}`
  378. )
  379. )
  380. const displayPendingEdges = pendingEdges.value.filter(
  381. (edge) => !stableEdgeKeys.has(`${edge.source}->${edge.target}->${edge.sourceHandle || ''}`)
  382. )
  383. return {
  384. ...baseWorkflow,
  385. nodes: [...nodes, ...displayPendingNodes],
  386. edges: [...(baseWorkflow.edges || []), ...displayPendingEdges]
  387. } as IWorkflow
  388. })
  389. const getNodeTypeById = (id?: string) => {
  390. if (!id) {
  391. return ''
  392. }
  393. const node = props.workflow?.nodes?.find((item) => item.id === id)
  394. return (node as any)?.nodeType || (node as any)?.data?.nodeType || ''
  395. }
  396. const getStartVariables = (node?: IWorkflowNode | null): StartVariable[] => {
  397. const variables = (node as any)?.data?.variables
  398. return Array.isArray(variables) ? variables : []
  399. }
  400. const workflowChatStartNode = computed<IWorkflowNode | null>(() => {
  401. return (
  402. (props.workflow?.nodes || []).find((node) => {
  403. const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
  404. return nodeType === 'start'
  405. }) || null
  406. )
  407. })
  408. const workflowChatStartVariables = computed<StartVariable[]>(() =>
  409. getStartVariables(workflowChatStartNode.value)
  410. )
  411. const workflowChatVisibleVariables = computed(() =>
  412. workflowChatStartVariables.value.filter((item) => !item.is_hide)
  413. )
  414. const canRunWorkflowChat = computed(() => !!workflowChatStartNode.value)
  415. const workflowChatRunning = computed(
  416. () => runnerStore.status === 'connecting' || runnerStore.status === 'running'
  417. )
  418. const nodeLibaryParentType = computed(() => getNodeTypeById(peddingHandlePayload.value?.parentId))
  419. const toolbarRunNodes = computed(() => {
  420. return (props.workflow?.nodes || [])
  421. .filter((node) => {
  422. const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
  423. return ['start', 'trigger-schedule', 'trigger-webhook'].includes(nodeType || '')
  424. })
  425. .map((node) => {
  426. const nodeType = ((node as any)?.nodeType || (node as any)?.data?.nodeType || '') as string
  427. return {
  428. id: node.id,
  429. name: node.name || getNodeDisplayName(nodeType) || nodeType,
  430. nodeType
  431. }
  432. })
  433. })
  434. const { onDragOver, onDrop, onDragLeave } = useDragAndDrop({
  435. id: props.workflow.id,
  436. addNodes: (node) => {
  437. handleNodeCreate(node)
  438. }
  439. })
  440. // 统一处理画布内节点相关接口的成功与失败提示。
  441. const handleApiResult = (response: any, successMessage?: string, errorMessage?: string) => {
  442. if (response?.isSuccess) {
  443. if (successMessage) {
  444. ElMessage.success(successMessage)
  445. }
  446. return true
  447. }
  448. if (response?.code === 0 && response?.error) {
  449. ElMessage.error(response.error)
  450. return false
  451. }
  452. if (errorMessage) {
  453. ElMessage.error(errorMessage)
  454. }
  455. return false
  456. }
  457. // 在提交节点更新前,整理出后端需要的标准节点结构。
  458. const buildUpdateNodePayload = (node: any) => {
  459. return {
  460. ...node,
  461. appAgentId: props.workflow.id,
  462. parentId: node.parentId || node.data?.parentId || '',
  463. position: node.position || { x: 20, y: 30 },
  464. width: node.width ?? node.data?.width ?? 96,
  465. height: node.height ?? node.data?.height ?? 96,
  466. selected: !!node.selected,
  467. nodeType: node.data?.nodeType || node.nodeType,
  468. zIndex: node.zIndex ?? 1
  469. }
  470. }
  471. // 将跨循环边重新绑定到可见的父节点上,保证连线位置正确。
  472. const normalizeEdgeEndpoints = (
  473. edge: { source: string; target: string },
  474. nodes: Array<{ id: string; parentId?: string }>
  475. ) => {
  476. const sourceNode = nodes.find((node) => node.id === edge.source)
  477. const targetNode = nodes.find((node) => node.id === edge.target)
  478. const sourceParentId = sourceNode?.parentId || ''
  479. const targetParentId = targetNode?.parentId || ''
  480. if (sourceParentId && sourceParentId !== targetParentId) {
  481. return {
  482. source: sourceParentId,
  483. target: edge.target
  484. }
  485. }
  486. if (targetParentId && targetParentId !== sourceParentId) {
  487. return {
  488. source: edge.source,
  489. target: targetParentId
  490. }
  491. }
  492. return edge
  493. }
  494. // 新建连线时复用同一套端点归一化逻辑。
  495. const normalizeConnectionEndpoints = (connection: Connection) => {
  496. return normalizeEdgeEndpoints(connection, props.workflow?.nodes || [])
  497. }
  498. const openSetter = (id: string, tab: 'setting' | 'last-run' = 'setting') => {
  499. nodeID.value = id
  500. setterActiveTab.value = tab
  501. setterVisible.value = true
  502. }
  503. const centerWorkflowIntoView = async () => {
  504. const vueFlow = workflowRef.value?.getVueFlow()
  505. if (!vueFlow || !props.workflow?.nodes?.length) {
  506. return false
  507. }
  508. await nextTick()
  509. await new Promise<void>((resolve) => {
  510. window.requestAnimationFrame(() => resolve())
  511. })
  512. await vueFlow.fitView({
  513. padding: 0.2,
  514. duration: 240,
  515. maxZoom: 1
  516. })
  517. return true
  518. }
  519. const openRunWorkflow = async (
  520. nodeId: string,
  521. initialTab: 'input' | 'trigger' | 'result' | 'detail' | 'trace',
  522. options?: {
  523. closeOnRun?: boolean
  524. inputOnly?: boolean
  525. }
  526. ) => {
  527. runWorkflowNodeId.value = nodeId
  528. runWorkflowInitialTab.value = initialTab
  529. closeRunWorkflowOnSubmit.value = !!options?.closeOnRun
  530. runWorkflowInputOnly.value = !!options?.inputOnly
  531. runVisible.value = false
  532. await nextTick()
  533. runVisible.value = true
  534. }
  535. // 从节点级操作入口直接运行指定节点。
  536. const handleRunNode = async (id: string) => {
  537. if (!props.workflow?.id) {
  538. ElMessage.warning(t('pages.nodeView.messages.selectNodeFirst'))
  539. return
  540. }
  541. const targetNode = props.workflow?.nodes?.find((node) => node.id === id)
  542. const nodeType = (targetNode as any)?.nodeType || (targetNode as any)?.data?.nodeType
  543. if (nodeType === 'start') {
  544. await openRunWorkflow(id, 'input', {
  545. closeOnRun: true,
  546. inputOnly: true
  547. })
  548. return
  549. }
  550. if (['trigger-schedule', 'trigger-webhook'].includes(nodeType || '')) {
  551. try {
  552. const response = await agent.postAgentDoExecute({
  553. appAgentId: props.workflow.id,
  554. start_node_id: id,
  555. is_debugger: true,
  556. responseType: 'ws',
  557. params: {}
  558. })
  559. const agentRunnerKey = response?.result
  560. if (agentRunnerKey) {
  561. runnerStore.startRunner(agentRunnerKey, id)
  562. await openRunWorkflow(id, 'trigger')
  563. return
  564. }
  565. ElMessage.error(t('pages.nodeView.messages.runFailed'))
  566. } catch (error) {
  567. console.error('postDoTestNodeRunner error', error)
  568. ElMessage.error(t('pages.nodeView.messages.runFailed'))
  569. }
  570. return
  571. }
  572. closeRunWorkflowOnSubmit.value = false
  573. runWorkflowInputOnly.value = false
  574. try {
  575. const response = await agent.postAgentDoExecute({
  576. appAgentId: props.workflow.id,
  577. start_node_id: id,
  578. is_debugger: true,
  579. responseType: 'ws',
  580. // 如果是start用户输入则传入数据
  581. params: {}
  582. })
  583. const agentRunnerKey = response?.result
  584. if (agentRunnerKey) {
  585. runnerStore.startRunner(agentRunnerKey, id)
  586. openSetter(id, 'last-run')
  587. }
  588. } catch (error) {
  589. console.error('postDoTestNodeRunner error', error)
  590. ElMessage.error(t('pages.nodeView.messages.runFailed'))
  591. }
  592. }
  593. /**
  594. * 运行智能体
  595. */
  596. const handleRunAgent = (id?: string) => {
  597. const targetNode =
  598. (id && props.workflow?.nodes?.find((node) => node.id === id)) ||
  599. (props.workflow?.nodes || []).find((node) => {
  600. const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
  601. return ['start', 'trigger-schedule', 'trigger-webhook'].includes(nodeType || '')
  602. })
  603. if (!props.workflow?.id || !targetNode?.id) {
  604. ElMessage.warning(t('pages.nodeView.messages.missingTrigger'))
  605. return
  606. }
  607. const nodeType = (targetNode as any)?.nodeType || (targetNode as any)?.data?.nodeType
  608. if (nodeType === 'start') {
  609. openRunWorkflow(targetNode.id, 'input')
  610. return
  611. }
  612. handleRunNode(targetNode.id)
  613. }
  614. function formatJsonDraft(value: unknown, fallback = '{}') {
  615. if (value === undefined || value === null || value === '') return fallback
  616. try {
  617. return JSON.stringify(value, null, 2)
  618. } catch {
  619. return fallback
  620. }
  621. }
  622. function resetWorkflowChatValidation() {
  623. workflowChatValidationErrors.value = {}
  624. }
  625. function initializeWorkflowChatInputValues() {
  626. const values: Record<string, any> = {}
  627. const drafts: Record<string, string> = {}
  628. workflowChatStartVariables.value.forEach((variable) => {
  629. const initialValue = cloneDeep(
  630. variable.default_value !== undefined
  631. ? variable.default_value
  632. : createEmptyValue(variable.formType)
  633. )
  634. if (variable.formType === 'json_object') {
  635. values[variable.name] =
  636. initialValue && typeof initialValue === 'object' && !Array.isArray(initialValue)
  637. ? initialValue
  638. : {}
  639. drafts[variable.name] = formatJsonDraft(values[variable.name], '{}')
  640. return
  641. }
  642. values[variable.name] = initialValue
  643. })
  644. workflowChatInputValues.value = values
  645. workflowChatJsonDrafts.value = drafts
  646. }
  647. const buildWorkflowChatParams = () => {
  648. const params = buildExecuteParams({
  649. startVariables: workflowChatStartVariables.value,
  650. inputValues: workflowChatInputValues.value,
  651. jsonDrafts: workflowChatJsonDrafts.value,
  652. validationErrors: workflowChatValidationErrors.value,
  653. translateFieldRequired: (name) =>
  654. t('pages.runWorkflow.fieldRequired', {
  655. name
  656. }),
  657. translateInvalidJson: () => t('pages.runWorkflow.invalidJson'),
  658. translateFieldTooLong: (name, max) =>
  659. t('pages.runWorkflow.fieldTooLong', {
  660. name,
  661. max
  662. })
  663. })
  664. if (!params) {
  665. ElMessage.warning(t('pages.runWorkflow.inputPanel.completeRequired'))
  666. return false
  667. }
  668. return params
  669. }
  670. const handleOpenWorkflowChat = () => {
  671. if (!workflowChatStartNode.value) {
  672. ElMessage.warning(t('pages.nodeView.messages.missingTrigger'))
  673. return
  674. }
  675. initializeWorkflowChatInputValues()
  676. resetWorkflowChatValidation()
  677. workflowChatBaseParams.value = {}
  678. workflowChatVisible.value = true
  679. }
  680. const handleWorkflowChatValidateSend = (done: (params: Record<string, any> | false) => void) => {
  681. done(buildWorkflowChatParams())
  682. }
  683. const handleWorkflowChatRunStarted = (id: string) => {
  684. runWorkflowNodeId.value = id
  685. }
  686. const handleWorkflowChatCancel = () => {
  687. runnerStore.stopRunner()
  688. }
  689. const createStickyNoteNode = (position: XYPosition = { x: 600, y: 300 }) => {
  690. props.workflow?.nodes.push({
  691. appAgentId: props.workflow.id,
  692. type: 'canvas-node',
  693. zIndex: -1,
  694. nodeType: 'stickyNote',
  695. position,
  696. id: `stickyNote_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
  697. name: t('pages.nodeView.stickyNote.name'),
  698. remark: '',
  699. data: {
  700. id: '',
  701. version: ['1.0.0'],
  702. inputs: [],
  703. outputs: [],
  704. position,
  705. nodeType: 'stickyNote',
  706. content: t('pages.nodeView.stickyNote.content'),
  707. width: 400,
  708. height: 200,
  709. color: '#fff5d6'
  710. }
  711. })
  712. }
  713. const getViewportCenterFlowPosition = (): XYPosition | undefined => {
  714. const viewport = workflowRef.value?.getVueFlow()?.viewport
  715. if (!viewport) {
  716. return undefined
  717. }
  718. return {
  719. x: (-viewport.value.x + window.innerWidth / 2) / viewport.value.zoom,
  720. y: (-viewport.value.y + window.innerHeight / 2) / viewport.value.zoom
  721. }
  722. }
  723. const hideStartNodeGuide = () => {
  724. dismissedStartNodeGuideWorkflowId.value = props.workflow?.id || ''
  725. }
  726. const handleStartNodeGuideCreate = (type: string) => {
  727. const position = getViewportCenterFlowPosition()
  728. handleNodeCreate(position ? { type, position } : { type })
  729. }
  730. watch(
  731. () => props.workflow?.nodes,
  732. async (nodes) => {
  733. if (!shouldAutoCenterAfterInit.value || !nodes?.length) {
  734. return
  735. }
  736. const centered = await centerWorkflowIntoView()
  737. if (centered) {
  738. shouldAutoCenterAfterInit.value = false
  739. }
  740. },
  741. { flush: 'post' }
  742. )
  743. /**
  744. * 创建新节点
  745. */
  746. const handleNodeCreate = (value: { type: string; position?: XYPosition } | string) => {
  747. if (typeof value === 'string') {
  748. if (value === 'stickyNote') {
  749. createStickyNoteNode()
  750. }
  751. return
  752. }
  753. if (value.type === 'stickyNote') {
  754. createStickyNoteNode(value.position)
  755. return
  756. }
  757. const nodeToAdd = nodeMap[value.type]?.schema
  758. const nodeType = nodeToAdd?.nodeType || nodeToAdd?.data?.nodeType || value.type
  759. const defaultNodeTitle = getNodeDisplayName(nodeType)
  760. const viewport = workflowRef.value?.getVueFlow()?.viewport
  761. const parentNodeType = getNodeTypeById(peddingHandlePayload.value?.parentId)
  762. const isLoopContainer = ['loop', 'iteration'].includes(parentNodeType)
  763. if (value.type === 'loop-end' && !isLoopContainer) {
  764. ElMessage.warning(t('pages.nodeView.messages.loopEndOnlyInside'))
  765. onHideNodeLibary()
  766. return
  767. }
  768. if (isLoopContainer && ['loop', 'iteration'].includes(value.type)) {
  769. ElMessage.warning(t('pages.nodeView.messages.noNestedLoop'))
  770. onHideNodeLibary()
  771. return
  772. }
  773. if (nodeToAdd) {
  774. const hasExplicitPosition = !!value.position
  775. const newNodeParam: any = {
  776. ...nodeToAdd,
  777. appAgentId: props.workflow?.id || '',
  778. position: value.position,
  779. name: defaultNodeTitle,
  780. data: {
  781. ...(nodeToAdd.data || {}),
  782. title: nodeToAdd.data?.title || defaultNodeTitle
  783. }
  784. }
  785. if (!hasExplicitPosition && !peddingHandlePayload.value && viewport) {
  786. newNodeParam.position = {
  787. x: (-viewport.value.x + window.innerWidth / 2) / viewport.value.zoom,
  788. y: (-viewport.value.y + window.innerHeight / 2) / viewport.value.zoom
  789. }
  790. }
  791. if (peddingHandlePayload.value) {
  792. const { position, handle, parentId } = peddingHandlePayload.value
  793. newNodeParam.position = position
  794. newNodeParam.prevNodeId = handle?.nodeId
  795. newNodeParam.parentId = parentId
  796. // 包含下划线/纯UUID的需要传值
  797. if (
  798. handle?.handleId?.includes('_') ||
  799. (handle?.handleId && !handle.handleId.includes('-source'))
  800. ) {
  801. newNodeParam.nodeHandleId = handle?.handleId
  802. }
  803. }
  804. if (!newNodeParam.position) {
  805. newNodeParam.position = nodeToAdd.position
  806. }
  807. if (peddingHandlePayload.value?.by === 'edge' && peddingHandlePayload.value?.connection) {
  808. const { connection } = peddingHandlePayload.value
  809. const params = {
  810. edgeId: connection.id,
  811. newNode: newNodeParam
  812. }
  813. const pendingNodeId = `pending-node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
  814. pendingNodes.value.push({
  815. ...newNodeParam,
  816. id: pendingNodeId,
  817. name: 'pendding...',
  818. type: 'canvas-node',
  819. selected: false,
  820. data: {
  821. ...(newNodeParam.data || {}),
  822. pending: true
  823. }
  824. } as IWorkflowNode)
  825. agent
  826. .postAgentDoNewAgentNodeWithEdge(params)
  827. .then(async (response) => {
  828. if (
  829. handleApiResult(
  830. response,
  831. t('pages.nodeView.messages.nodeAdded'),
  832. t('pages.nodeView.messages.addNodeFailed')
  833. )
  834. ) {
  835. await props.reloadWorkflow(props.workflow.id)
  836. }
  837. })
  838. .catch((error) => {
  839. console.error('postAgentDoNewAgentNodeWithEdge error', error)
  840. ElMessage.error(t('pages.nodeView.messages.addNodeFailed'))
  841. })
  842. .finally(() => {
  843. pendingNodes.value = pendingNodes.value.filter((node) => node.id !== pendingNodeId)
  844. })
  845. onHideNodeLibary()
  846. return
  847. }
  848. onHideNodeLibary()
  849. const pendingNodeId = `pending-node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
  850. pendingNodes.value.push({
  851. ...newNodeParam,
  852. id: pendingNodeId,
  853. name: 'pendding...',
  854. type: 'canvas-node',
  855. selected: false,
  856. data: {
  857. ...(newNodeParam.data || {}),
  858. pending: true
  859. }
  860. } as IWorkflowNode)
  861. agent
  862. .postAgentDoNewAgentNode(newNodeParam)
  863. .then(async (response) => {
  864. if (
  865. handleApiResult(
  866. response,
  867. t('pages.nodeView.messages.nodeAdded'),
  868. t('pages.nodeView.messages.addNodeFailed')
  869. )
  870. ) {
  871. await props.reloadWorkflow(props.workflow.id)
  872. }
  873. })
  874. .catch((error) => {
  875. console.error('postDoNewAgentNode error', error)
  876. ElMessage.error(t('pages.nodeView.messages.addNodeFailed'))
  877. })
  878. .finally(() => {
  879. pendingNodes.value = pendingNodes.value.filter((node) => node.id !== pendingNodeId)
  880. })
  881. }
  882. }
  883. const handleNodeCreateFromLibrary = (value: { type: string; position?: XYPosition } | string) => {
  884. if (typeof value === 'string') {
  885. handleNodeCreate(value)
  886. return
  887. }
  888. handleNodeCreate({
  889. ...value,
  890. position: value.position || contextMenuFlowPosition.value
  891. })
  892. }
  893. const openNodeLibraryAtContextMenu = async () => {
  894. await nextTick()
  895. if (hideNodeLibaryTimer) {
  896. window.clearTimeout(hideNodeLibaryTimer)
  897. hideNodeLibaryTimer = undefined
  898. }
  899. libaryRefferenceRef.value = createNodeLibaryPopoverAnchor(contextMenuPosition.value)
  900. peddingHandlePayload.value = {
  901. position: contextMenuFlowPosition.value
  902. }
  903. showNodeLibary.value = true
  904. }
  905. const handleContextAddNode = () => {
  906. closeContextMenu()
  907. void openNodeLibraryAtContextMenu()
  908. }
  909. const handleContextAddStickyNote = () => {
  910. closeContextMenu()
  911. handleNodeCreate({
  912. type: 'stickyNote',
  913. position: contextMenuFlowPosition.value
  914. })
  915. }
  916. const handleContextRunNode = () => {
  917. if (!selectedNodeId.value) {
  918. ElMessage.warning(t('pages.nodeView.messages.selectNodeFirst'))
  919. closeContextMenu()
  920. return
  921. }
  922. closeContextMenu()
  923. void handleRunNode(selectedNodeId.value)
  924. }
  925. const handleWorkflowContextMenu = (event: MouseEvent) => {
  926. const target = event.target as HTMLElement | null
  927. if (target?.closest('.workflow-context-menu')) {
  928. return
  929. }
  930. const wrapperEl = workflowWrapperRef.value
  931. if (!wrapperEl?.contains(target)) {
  932. return
  933. }
  934. event.preventDefault()
  935. onHideNodeLibary()
  936. contextMenuFlowPosition.value = getFlowPositionFromMouseEvent(event)
  937. contextMenuPosition.value = {
  938. x: event.clientX,
  939. y: event.clientY
  940. }
  941. contextMenuVisible.value = true
  942. }
  943. // 双击节点时打开对应的配置面板。
  944. const handleNodeClick = (id: string, _position: XYPosition) => {
  945. openSetter(id, 'setting')
  946. }
  947. const handleWorkflowRunStarted = (id: string) => {
  948. if (!closeRunWorkflowOnSubmit.value) {
  949. return
  950. }
  951. openSetter(id, 'last-run')
  952. }
  953. // 将拖拽落点转换成统一的节点创建流程。
  954. const handleDrop = (position: XYPosition, event: DragEvent) => {
  955. const type = event.dataTransfer?.getData('application/x-node-type')
  956. if (!type) return
  957. handleNodeCreate({ type, position })
  958. }
  959. // 持久化新建连线;若相同起终点已存在,则不重复创建。
  960. const onCreateConnection = async (connection: Connection) => {
  961. const { sourceHandle } = connection
  962. const { source, target } = normalizeConnectionEndpoints(connection)
  963. const edgeKey = `${source}->${target}->${sourceHandle || ''}`
  964. const params: {
  965. appAgentId: string
  966. source: string
  967. target: string
  968. zIndex: number
  969. sourceHandle?: string
  970. } = {
  971. appAgentId: props.workflow.id,
  972. source,
  973. target,
  974. zIndex: 1
  975. }
  976. // 包含下划线或者无-source的需要传值
  977. if (
  978. (sourceHandle && sourceHandle.includes('_')) ||
  979. (sourceHandle && !sourceHandle.includes('-source'))
  980. ) {
  981. params.sourceHandle = sourceHandle
  982. }
  983. const existsInWorkflow = props.workflow?.edges.some(
  984. (edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}` === edgeKey
  985. )
  986. const existsInPending = pendingEdges.value.some(
  987. (edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}` === edgeKey
  988. )
  989. if (existsInWorkflow || existsInPending) {
  990. return
  991. }
  992. const pendingId = `pending-edge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
  993. pendingEdges.value.push({
  994. id: pendingId,
  995. type: 'canvas-edge',
  996. source,
  997. target,
  998. sourceHandle: params.sourceHandle,
  999. targetHandle: connection.targetHandle,
  1000. data: {
  1001. pending: true
  1002. }
  1003. })
  1004. try {
  1005. const response = await agent.postAgentDoNewEdge(params)
  1006. if (
  1007. handleApiResult(
  1008. response,
  1009. t('pages.nodeView.messages.edgeCreated'),
  1010. t('pages.nodeView.messages.createEdgeFailed')
  1011. )
  1012. ) {
  1013. await props.reloadWorkflow(props.workflow.id)
  1014. }
  1015. } catch (error) {
  1016. console.error('postAgentDoNewEdge error', error)
  1017. ElMessage.error(t('pages.nodeView.messages.createEdgeFailed'))
  1018. } finally {
  1019. pendingEdges.value = pendingEdges.value.filter((edge) => edge.id !== pendingId)
  1020. }
  1021. }
  1022. // 在拖拽结束后持久化节点坐标变化。
  1023. const handleUpdateNodesPosition = (events: { id: string; position: XYPosition }[]) => {
  1024. events?.forEach(({ id, position }) => {
  1025. const node = props.workflow?.nodes.find((item) => item.id === id)
  1026. if (node) {
  1027. if (node.position?.x === position.x && node.position?.y === position.y) {
  1028. return
  1029. }
  1030. node.position = position
  1031. agent
  1032. .postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
  1033. .then((response) => {
  1034. handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))
  1035. })
  1036. .catch((error) => {
  1037. console.error('postDoUpdateAgentNode error', error)
  1038. })
  1039. }
  1040. })
  1041. }
  1042. // 保持本地工作流中同一时间仅有一个节点处于选中状态。
  1043. const handleSelectNode = (id: string) => {
  1044. props.workflow?.nodes.forEach((node) => {
  1045. node.selected = false
  1046. })
  1047. const node = props.workflow?.nodes.find((item) => item.id === id)
  1048. if (node) {
  1049. node.selected = true
  1050. }
  1051. }
  1052. // 持久化配置面板提交的节点数据变更。
  1053. const handleUpdateNode = (node: IWorkflowNode) => {
  1054. if (node) {
  1055. // 根据节点配置的 inputs/outputs 数量动态调整高度(超过 2 个端口时增加高度)
  1056. const schema = nodeMap[node.nodeType]
  1057. if (schema) {
  1058. const data = node.data || {}
  1059. const inputs =
  1060. typeof schema.inputs === 'function' ? schema.inputs(data as any) : schema.inputs || []
  1061. const outputs =
  1062. typeof schema.outputs === 'function' ? schema.outputs(data as any) : schema.outputs || []
  1063. const maxPorts = Math.max(inputs.length || 0, outputs.length || 0)
  1064. // todo: 特殊情况处理
  1065. if (maxPorts > 2) {
  1066. const extraPorts = maxPorts - 2
  1067. const baseHeight = 96
  1068. const perPortOffset = 32
  1069. node.height = baseHeight + extraPorts * perPortOffset
  1070. }
  1071. }
  1072. const workflowNode = props.workflow?.nodes.find((item) => item.id === node.id)
  1073. const syncedNode = workflowNode || node
  1074. if (workflowNode) {
  1075. const mergedData = {
  1076. ...(workflowNode.data || {}),
  1077. ...(node.data || {})
  1078. }
  1079. Object.assign(workflowNode, node)
  1080. workflowNode.data = mergedData
  1081. }
  1082. agent
  1083. .postAgentDoUpdateAgentNode(buildUpdateNodePayload(syncedNode))
  1084. .then((response: any) => {
  1085. if (!handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))) {
  1086. return
  1087. }
  1088. const responseNode = response?.result
  1089. if (!responseNode || typeof responseNode !== 'object') {
  1090. return
  1091. }
  1092. const latestNode = props.workflow?.nodes.find(
  1093. (item) => item.id === (responseNode.id || node.id)
  1094. )
  1095. if (!latestNode) {
  1096. return
  1097. }
  1098. const mergedData = {
  1099. ...(latestNode.data || {}),
  1100. ...(responseNode.data || {})
  1101. }
  1102. Object.assign(latestNode, responseNode)
  1103. latestNode.data = mergedData
  1104. })
  1105. .catch((error) => {
  1106. console.error('postDoUpdateAgentNode error', error)
  1107. })
  1108. }
  1109. }
  1110. // 持久化画布自身抛出的轻量节点属性更新。
  1111. const handleUpdateNodeProps = (id: string, attrs: Record<string, unknown>) => {
  1112. const node = props.workflow?.nodes.find((item) => item.id === id)
  1113. if (node) {
  1114. const keys = Object.keys(attrs || {})
  1115. const meaningfulKeys = keys.filter((key) => !['selected', 'dragging'].includes(key))
  1116. if (meaningfulKeys.length === 0) {
  1117. return
  1118. }
  1119. if (node.data?.nodeType === 'stickyNote') {
  1120. Object.assign(node.data, attrs)
  1121. } else {
  1122. Object.assign(node, attrs)
  1123. }
  1124. agent
  1125. .postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
  1126. .then((response) => {
  1127. handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))
  1128. })
  1129. .catch((error) => {
  1130. console.error('postDoUpdateAgentNode error', error)
  1131. })
  1132. }
  1133. }
  1134. // 确认后删除节点,并重新加载工作流图数据。
  1135. const handleDeleteNode = async (id: string) => {
  1136. const index = props.workflow.nodes.findIndex((node) => node.id === id)
  1137. if (index !== -1) {
  1138. ElMessageBox.confirm(t('common.confirmDelete.message'), t('common.confirmDelete.title'), {
  1139. confirmButtonText: t('common.confirm'),
  1140. cancelButtonText: t('common.cancel'),
  1141. type: 'warning'
  1142. }).then(async () => {
  1143. await agent.postAgentDoDeleteAgentNode({
  1144. id
  1145. })
  1146. await props.reloadWorkflow(props.workflow.id)
  1147. })
  1148. }
  1149. }
  1150. // 确认后删除连线,并重新加载工作流图数据。
  1151. const handleDeleteEdge = async (connection: Connection & { id: string }) => {
  1152. if (connection.id) {
  1153. ElMessageBox.confirm(t('common.confirmDelete.message'), t('common.confirmDelete.title'), {
  1154. confirmButtonText: t('common.confirm'),
  1155. cancelButtonText: t('common.cancel'),
  1156. type: 'warning'
  1157. }).then(async () => {
  1158. await agent.postAgentDoDeleteEdge({
  1159. id: connection.id
  1160. })
  1161. await props.reloadWorkflow(props.workflow.id)
  1162. })
  1163. }
  1164. }
  1165. // 保存工具栏里修改的环境变量,并刷新当前工作流。
  1166. const handleChangeEnvVars = async (
  1167. envVars: {
  1168. name: string
  1169. value: string
  1170. type: 'string' | 'number' | 'boolean' | 'object' | 'array'
  1171. }[]
  1172. ) => {
  1173. const response = await agent.postAgentDoSaveAgentVariables({
  1174. appAgentId: props.workflow.id,
  1175. conversation_variables: [],
  1176. env_variables: envVars
  1177. })
  1178. handleApiResult(
  1179. response,
  1180. t('pages.nodeView.messages.envSaved'),
  1181. t('pages.nodeView.messages.saveEnvFailed')
  1182. )
  1183. await props.reloadWorkflow(props.workflow.id)
  1184. }
  1185. // 连线创建被取消时,打开节点库以继续补全流程。
  1186. const onConnectionOpenNodeLibary = async (payload: {
  1187. handle: ConnectStartEvent
  1188. position: XYPosition
  1189. event: MouseEvent
  1190. parentId?: string
  1191. }) => {
  1192. await nextTick()
  1193. libaryRefferenceRef.value = createNodeLibaryPopoverAnchor({
  1194. x: payload.event.clientX,
  1195. y: payload.event.clientY
  1196. })
  1197. showNodeLibary.value = true
  1198. peddingHandlePayload.value = payload
  1199. }
  1200. // 关闭节点库弹层时,清理相关的临时状态。
  1201. const onHideNodeLibary = () => {
  1202. showNodeLibary.value = false
  1203. if (hideNodeLibaryTimer) {
  1204. window.clearTimeout(hideNodeLibaryTimer)
  1205. }
  1206. hideNodeLibaryTimer = window.setTimeout(() => {
  1207. peddingHandlePayload.value = undefined
  1208. libaryRefferenceRef.value = undefined
  1209. removeNodeLibaryPopoverAnchor()
  1210. hideNodeLibaryTimer = undefined
  1211. }, 500)
  1212. }
  1213. const onGlobalPointerDown = (event: PointerEvent) => {
  1214. if (contextMenuVisible.value) {
  1215. const target = event.target as Node | null
  1216. if (!target || !contextMenuRef.value?.contains(target)) {
  1217. closeContextMenu()
  1218. }
  1219. }
  1220. if (!showNodeLibary.value) {
  1221. return
  1222. }
  1223. const target = event.target as Node | null
  1224. if (!target) {
  1225. onHideNodeLibary()
  1226. return
  1227. }
  1228. const panelEl = nodeLibraryPanelRef.value
  1229. if (panelEl?.contains(target)) {
  1230. return
  1231. }
  1232. const anchorEl = libaryRefferenceRef.value
  1233. if (anchorEl?.contains(target)) {
  1234. return
  1235. }
  1236. onHideNodeLibary()
  1237. }
  1238. const onGlobalKeyDown = (event: KeyboardEvent) => {
  1239. if (event.key === 'Escape') {
  1240. closeContextMenu()
  1241. if (showNodeLibary.value) {
  1242. onHideNodeLibary()
  1243. }
  1244. }
  1245. }
  1246. const onGlobalScroll = () => {
  1247. closeContextMenu()
  1248. }
  1249. // 根据边上加号按钮的位置打开节点库弹层。
  1250. const handleClickConectionAdd = (connection: Connection & { id: string }, parentId?: string) => {
  1251. const el = document.querySelector(`[edge-add-btn="${connection.id}"]`) as HTMLElement
  1252. const screenToFlowCoordinate = workflowRef.value?.getVueFlow()?.screenToFlowCoordinate
  1253. if (el && screenToFlowCoordinate) {
  1254. const rect = el.getBoundingClientRect()
  1255. const position = screenToFlowCoordinate({
  1256. x: rect.left,
  1257. y: rect.top
  1258. })
  1259. libaryRefferenceRef.value = createNodeLibaryPopoverAnchor({
  1260. x: rect.left,
  1261. y: rect.top
  1262. })
  1263. showNodeLibary.value = true
  1264. peddingHandlePayload.value = {
  1265. by: 'edge',
  1266. position: parentId ? { x: 50, y: 50 } : position,
  1267. connection,
  1268. parentId
  1269. }
  1270. }
  1271. }
  1272. /**
  1273. * 视图切换
  1274. */
  1275. const handleViewportChange = useDebounceFn((viewport: { x: number; y: number; zoom: number }) => {
  1276. if (!isEqual(viewport, props.workflow?.viewport)) {
  1277. agent.postAgentDoEditAgent({
  1278. data: {
  1279. ...props.workflow,
  1280. viewPort: viewport
  1281. }
  1282. })
  1283. }
  1284. }, 1000)
  1285. onBeforeUnmount(() => {
  1286. if (hideNodeLibaryTimer) {
  1287. window.clearTimeout(hideNodeLibaryTimer)
  1288. }
  1289. removeNodeLibaryPopoverAnchor()
  1290. resetDisplayedNodeStatuses()
  1291. document.removeEventListener('pointerdown', onGlobalPointerDown, true)
  1292. document.removeEventListener('keydown', onGlobalKeyDown)
  1293. window.removeEventListener('scroll', onGlobalScroll, true)
  1294. })
  1295. onMounted(() => {
  1296. document.addEventListener('pointerdown', onGlobalPointerDown, true)
  1297. document.addEventListener('keydown', onGlobalKeyDown)
  1298. window.addEventListener('scroll', onGlobalScroll, true)
  1299. })
  1300. </script>
  1301. <style scoped lang="less">
  1302. .node-library-popover-panel {
  1303. max-height: min(520px, calc(100vh - 48px));
  1304. overflow-y: auto;
  1305. }
  1306. .workflow-context-menu {
  1307. position: fixed;
  1308. z-index: 3000;
  1309. min-width: 148px;
  1310. padding: 6px;
  1311. border: 1px solid var(--border-light);
  1312. border-radius: 8px;
  1313. background: var(--bg-base);
  1314. box-shadow: var(--shadow-md);
  1315. }
  1316. .workflow-context-menu__item {
  1317. display: flex;
  1318. align-items: center;
  1319. gap: 8px;
  1320. width: 100%;
  1321. height: 34px;
  1322. padding: 0 10px;
  1323. border: 0;
  1324. border-radius: 6px;
  1325. background: transparent;
  1326. color: var(--text-primary);
  1327. font-size: 13px;
  1328. line-height: 1;
  1329. text-align: left;
  1330. cursor: pointer;
  1331. }
  1332. .workflow-context-menu__item:hover:not(.is-disabled) {
  1333. background: var(--bg-container);
  1334. color: #296dff;
  1335. }
  1336. .workflow-context-menu__item.is-disabled {
  1337. color: var(--text-tertiary);
  1338. cursor: not-allowed;
  1339. }
  1340. .workflow-context-menu__icon {
  1341. display: inline-flex;
  1342. align-items: center;
  1343. justify-content: center;
  1344. width: 16px;
  1345. height: 16px;
  1346. font-size: 12px;
  1347. font-weight: 700;
  1348. }
  1349. </style>