Editor.vue 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  1. <template>
  2. <div class="w-full h-full flex flex-col">
  3. <div
  4. class="h-60px shrink-0 border-b border-b-solid border-gray-200 flex items-center justify-between px-12px"
  5. >
  6. <div class="left flex items-center gap-4">
  7. <el-breadcrumb separator="/" class="flex items-center">
  8. <el-breadcrumb-item>Workspace</el-breadcrumb-item>
  9. <el-breadcrumb-item>
  10. <Input ref="inputRef" v-model="workflow.name" variant="borderless" />
  11. </el-breadcrumb-item>
  12. </el-breadcrumb>
  13. <div class="flex gap-2" v-show="!showTagInput" @click="showTagInput = true">
  14. <el-tag type="info" v-for="tag in workflow.tags" :key="tag" :disable-transitions="false">
  15. {{ tag }}
  16. </el-tag>
  17. </div>
  18. <el-input-tag
  19. v-show="showTagInput"
  20. v-model="workflow.tags"
  21. placeholder="按回车键添加标签"
  22. aria-label="按回车键添加标签"
  23. :max="5"
  24. @blur="showTagInput = false"
  25. />
  26. <IconButton
  27. v-if="!workflow.tags?.length && !showTagInput"
  28. icon="iconoir:plus"
  29. type="primary"
  30. link
  31. @click="showTagInput = true"
  32. >标签</IconButton
  33. >
  34. </div>
  35. <div class="right flex items-center gap-2">
  36. <el-button type="default" size="small">发布</el-button>
  37. <IconButton icon="lucide:history" type="default" link></IconButton>
  38. <el-dropdown placement="bottom-end" popper-class="w-120px">
  39. <IconButton icon="fluent-mdl2:more" type="default" link></IconButton>
  40. <template #dropdown>
  41. <el-dropdown-item>描述</el-dropdown-item>
  42. <el-dropdown-item>复用</el-dropdown-item>
  43. <el-dropdown-item @click="handleRename">重命名</el-dropdown-item>
  44. <el-dropdown-item divided @click="handleDelete">删除</el-dropdown-item>
  45. </template>
  46. </el-dropdown>
  47. </div>
  48. </div>
  49. <el-splitter layout="vertical" class="flex-1">
  50. <el-splitter-panel>
  51. <div class="h-full w-full" ref="workflowWrapperRef" @drop="onDrop">
  52. <Workflow
  53. ref="workflowRef"
  54. :id="workflow?.id"
  55. :workflow="workflowWithExecutionState"
  56. :nodeMap="nodeMap"
  57. @click:node="handleSelectNode"
  58. @dblclick:node="handleNodeClick"
  59. @create:node="handleNodeCreate"
  60. @create:connection:end="onCreateConnection"
  61. @drag-and-drop="handleDrop"
  62. @run:node="handleRunNode"
  63. @update:nodes:position="handleUpdateNodesPosition"
  64. @update:node:attrs="handleUpdateNodeProps"
  65. @delete:node="handleDeleteNode"
  66. @delete:connection="handleDeleteEdge"
  67. @dragover="onDragOver"
  68. @dragleave="onDragLeave"
  69. @create:connection:cancelled="onConnectionOpenNodeLibary"
  70. @click:connection:add="handleClickConectionAdd"
  71. class="bg-#f5f5f5"
  72. >
  73. <Toolbar
  74. @create:node="handleNodeCreate"
  75. @run="handleRunSelectedNode"
  76. :env-vars="workflow?.env_variables || []"
  77. @change-env-vars="handleChangeEnvVars"
  78. />
  79. </Workflow>
  80. </div>
  81. <RunWorkflow v-model:visible="runVisible" @run="handleRunSelectedNode" />
  82. <Setter
  83. :id="nodeID"
  84. :workflow="workflow!"
  85. @update:node:data="handleUpdateNode"
  86. @run-node="handleRunNode"
  87. v-model:visible="setterVisible"
  88. />
  89. <el-popover
  90. :visible="showNodeLibary"
  91. width="360px"
  92. virtual-triggering
  93. :show-arrow="false"
  94. :append-to="workflowWrapperRef"
  95. :virtual-ref="libaryRefferenceRef"
  96. >
  97. <NodeLibary @add-node="handleNodeCreate" @mouseleave="onHideNodeLibary" />
  98. </el-popover>
  99. </el-splitter-panel>
  100. <el-splitter-panel v-model:size.lazy="footerHeight" :min="32">
  101. <EditorFooter @toggle="handleFooterToggle" />
  102. </el-splitter-panel>
  103. </el-splitter>
  104. </div>
  105. </template>
  106. <script setup lang="ts">
  107. import { computed, ref, inject, type CSSProperties, onBeforeUnmount, watch, nextTick } from 'vue'
  108. import { useRoute, useRouter } from 'vue-router'
  109. import { agent } from '@repo/api-service'
  110. import RunWorkflow from '@/components/RunWorkflow/index.vue'
  111. import EditorFooter from '@/features/editorFooter/index.vue'
  112. import NodeLibary from '@/features/nodeLibary/index.vue'
  113. import Toolbar from '@/features/toolbar/index.vue'
  114. import Setter from '@/features/setter/index.vue'
  115. import { IconButton, Input } from '@repo/ui'
  116. import { nodeMap } from '@/nodes'
  117. import { Workflow, useDragAndDrop } from '@repo/workflow'
  118. import { dayjs, ElMessage, ElMessageBox } from 'element-plus'
  119. import { useRunnerStore } from '@/store/modules/runner.store'
  120. import type {
  121. IWorkflow,
  122. XYPosition,
  123. Connection,
  124. ConnectStartEvent,
  125. IWorkflowNode,
  126. CanvasExecutionStatus
  127. } from '@repo/workflow'
  128. const layout = inject<{ setMainStyle: (style: CSSProperties) => void }>('layout')
  129. layout?.setMainStyle({
  130. padding: '0px'
  131. })
  132. const footerHeight = ref(32)
  133. const route = useRoute()
  134. const router = useRouter()
  135. const id = route.params?.id as string
  136. const runnerStore = useRunnerStore()
  137. const workflowWrapperRef = ref<HTMLElement>()
  138. const showNodeLibary = ref(false)
  139. const libaryRefferenceRef = ref<HTMLElement>()
  140. const peddingHandlePayload = ref<{
  141. by?: 'node' | 'edge'
  142. handle?: ConnectStartEvent
  143. position: XYPosition
  144. event?: MouseEvent
  145. parentId?: string
  146. connection?: Connection
  147. }>()
  148. const projectMap = JSON.parse(localStorage.getItem(`workflow-map`) || '{}') as Record<
  149. string,
  150. IWorkflow
  151. >
  152. const showTagInput = ref(false)
  153. const workflow = ref<IWorkflow>(
  154. projectMap?.[id]
  155. ? projectMap[id]
  156. : {
  157. id,
  158. name: 'workflow_1',
  159. created: dayjs().format('MM 月 DD 日'),
  160. nodes: [],
  161. edges: []
  162. }
  163. )
  164. const mapNodeExecutionStatus = (status?: string): CanvasExecutionStatus => {
  165. if (status === 'running') {
  166. return 'running'
  167. }
  168. if (status === 'success') {
  169. return 'success'
  170. }
  171. if (status === 'failed') {
  172. return 'warning'
  173. }
  174. return 'idle'
  175. }
  176. const MIN_NODE_RUNNING_EFFECT_MS = 500
  177. const displayNodeExecutionStatus = ref<Record<string, CanvasExecutionStatus>>({})
  178. const runningStatusStartedAt = new Map<string, number>()
  179. const pendingNodeStatusTimers = new Map<string, number>()
  180. const clearPendingNodeStatusTimer = (nodeId: string) => {
  181. const timer = pendingNodeStatusTimers.get(nodeId)
  182. if (timer) {
  183. window.clearTimeout(timer)
  184. pendingNodeStatusTimers.delete(nodeId)
  185. }
  186. }
  187. const applyDisplayedNodeStatus = (nodeId: string, status: CanvasExecutionStatus) => {
  188. if (status === 'idle') {
  189. delete displayNodeExecutionStatus.value[nodeId]
  190. return
  191. }
  192. displayNodeExecutionStatus.value[nodeId] = status
  193. }
  194. const syncDisplayedNodeStatus = (nodeId: string, nextStatus: CanvasExecutionStatus) => {
  195. const currentStatus = displayNodeExecutionStatus.value[nodeId] || 'idle'
  196. if (nextStatus === 'running') {
  197. clearPendingNodeStatusTimer(nodeId)
  198. runningStatusStartedAt.set(nodeId, Date.now())
  199. applyDisplayedNodeStatus(nodeId, 'running')
  200. return
  201. }
  202. const runningStartedAt = runningStatusStartedAt.get(nodeId)
  203. const shouldKeepRunning =
  204. currentStatus === 'running' &&
  205. typeof runningStartedAt === 'number' &&
  206. Date.now() - runningStartedAt < MIN_NODE_RUNNING_EFFECT_MS
  207. if (shouldKeepRunning) {
  208. clearPendingNodeStatusTimer(nodeId)
  209. const delay = MIN_NODE_RUNNING_EFFECT_MS - (Date.now() - runningStartedAt)
  210. const timer = window.setTimeout(() => {
  211. applyDisplayedNodeStatus(nodeId, nextStatus)
  212. runningStatusStartedAt.delete(nodeId)
  213. pendingNodeStatusTimers.delete(nodeId)
  214. }, delay)
  215. pendingNodeStatusTimers.set(nodeId, timer)
  216. return
  217. }
  218. clearPendingNodeStatusTimer(nodeId)
  219. applyDisplayedNodeStatus(nodeId, nextStatus)
  220. runningStatusStartedAt.delete(nodeId)
  221. }
  222. const resetDisplayedNodeStatuses = () => {
  223. pendingNodeStatusTimers.forEach((timer) => window.clearTimeout(timer))
  224. pendingNodeStatusTimers.clear()
  225. runningStatusStartedAt.clear()
  226. displayNodeExecutionStatus.value = {}
  227. }
  228. watch(
  229. () => runnerStore.currentRunnerKey,
  230. () => {
  231. resetDisplayedNodeStatuses()
  232. }
  233. )
  234. watch(
  235. () => runnerStore.nodes.map((item) => ({ nodeId: item.nodeId, status: item.status })),
  236. (nodeStates) => {
  237. const activeNodeIds = new Set(nodeStates.map((item) => item.nodeId))
  238. nodeStates.forEach((item) => {
  239. syncDisplayedNodeStatus(item.nodeId, mapNodeExecutionStatus(item.status))
  240. })
  241. Object.keys(displayNodeExecutionStatus.value).forEach((nodeId) => {
  242. if (!activeNodeIds.has(nodeId)) {
  243. syncDisplayedNodeStatus(nodeId, 'idle')
  244. }
  245. })
  246. },
  247. { immediate: true, deep: true }
  248. )
  249. const workflowWithExecutionState = computed<IWorkflow>(() => {
  250. const baseWorkflow = workflow.value
  251. const runnerNodeStatusMap = new Map(Object.entries(displayNodeExecutionStatus.value))
  252. return {
  253. ...baseWorkflow,
  254. nodes: (baseWorkflow.nodes || []).map((node) => {
  255. const executionStatus = runnerNodeStatusMap.get(node.id) || 'idle'
  256. return {
  257. ...node,
  258. executionStatus,
  259. data: {
  260. ...(node.data || {}),
  261. executionStatus
  262. }
  263. }
  264. })
  265. }
  266. })
  267. const inputRef = ref<InstanceType<typeof Input>>()
  268. const saveAgentTimer = ref<number | undefined>(undefined)
  269. const saveVarsTimer = ref<number | undefined>(undefined)
  270. const isHydrating = ref(false)
  271. const notifyTimestamps = new Map<string, number>()
  272. const { onDragOver, onDrop, onDragLeave } = useDragAndDrop({
  273. id,
  274. addNodes: (node) => {
  275. handleNodeCreate(node)
  276. }
  277. })
  278. const normalizeNodeType = (node: any) => {
  279. const sourceNodeType = node?.nodeType || node?.data?.nodeType || node?.data?.type || node?.type
  280. return sourceNodeType || 'code'
  281. }
  282. const notifySuccess = (key: string, message: string, cooldown = 1500) => {
  283. const now = Date.now()
  284. const last = notifyTimestamps.get(key) || 0
  285. if (now - last < cooldown) return
  286. notifyTimestamps.set(key, now)
  287. ElMessage.success(message)
  288. }
  289. const handleApiResult = (response: any, successMessage?: string, errorMessage?: string) => {
  290. if (response?.isSuccess) {
  291. if (successMessage) {
  292. notifySuccess(successMessage, successMessage)
  293. }
  294. return true
  295. }
  296. if (response?.code === 0 && response?.error) {
  297. ElMessage.error(response.error)
  298. return false
  299. }
  300. if (errorMessage) {
  301. ElMessage.error(errorMessage)
  302. }
  303. return false
  304. }
  305. const buildUpdateNodePayload = (node: any) => {
  306. return {
  307. ...node,
  308. appAgentId: workflow.value.id,
  309. parentId: node.parentId || node.data?.parentId || '',
  310. position: node.position || { x: 20, y: 30 },
  311. width: node.width ?? node.data?.width ?? 96,
  312. height: node.height ?? node.data?.height ?? 96,
  313. selected: !!node.selected,
  314. nodeType: node.data?.nodeType || node.nodeType,
  315. zIndex: node.zIndex ?? 1
  316. }
  317. }
  318. const toWorkflowNode = (node: any) => {
  319. const normalizedNodeType = normalizeNodeType(node)
  320. const schema = nodeMap[normalizedNodeType]?.schema
  321. const position = node?.position || schema?.position || { x: 20, y: 30 }
  322. const width = node?.width ?? schema?.width ?? 96
  323. const height = node?.height ?? schema?.height ?? 96
  324. return {
  325. ...schema,
  326. ...node,
  327. id: node.id,
  328. type: 'canvas-node',
  329. position,
  330. width,
  331. height,
  332. zIndex: node?.zIndex ?? schema?.zIndex ?? 1,
  333. selected: false,
  334. data: {
  335. ...(schema?.data || {}),
  336. ...(node?.data || {}),
  337. id: node.id,
  338. position,
  339. width,
  340. height,
  341. nodeType: normalizedNodeType
  342. }
  343. }
  344. }
  345. const normalizeEdgeEndpoints = (
  346. edge: { source: string; target: string },
  347. nodes: Array<{ id: string; parentId?: string }>
  348. ) => {
  349. const sourceNode = nodes.find((node) => node.id === edge.source)
  350. const targetNode = nodes.find((node) => node.id === edge.target)
  351. const sourceParentId = sourceNode?.parentId || ''
  352. const targetParentId = targetNode?.parentId || ''
  353. if (sourceParentId && sourceParentId !== targetParentId) {
  354. return {
  355. source: sourceParentId,
  356. target: edge.target
  357. }
  358. }
  359. if (targetParentId && targetParentId !== sourceParentId) {
  360. return {
  361. source: edge.source,
  362. target: targetParentId
  363. }
  364. }
  365. return edge
  366. }
  367. const toWorkflowEdge = (
  368. edge: any,
  369. index: number,
  370. nodes: Array<{ id: string; parentId?: string }> = []
  371. ) => {
  372. if (!edge || typeof edge !== 'object' || !edge.source || !edge.target) {
  373. return null
  374. }
  375. const normalizedEdge = normalizeEdgeEndpoints(edge, nodes)
  376. return {
  377. ...edge,
  378. ...normalizedEdge,
  379. sourceHandle: edge.sourceHandle === 'source' ? `${edge.source}-source` : edge.sourceHandle,
  380. targetHandle: edge.targetHandle === 'target' ? `${edge.target}-target` : edge.targetHandle,
  381. id: edge.id || `edge-${normalizedEdge.source}-${normalizedEdge.target}-${index}`,
  382. type: 'canvas-edge',
  383. data: edge.data || {}
  384. }
  385. }
  386. const isPendingCreate = (node: any) => {
  387. return !!(node as any)?.__pendingCreate
  388. }
  389. const loadAgentWorkflow = async (agentId: string) => {
  390. if (!agentId) return
  391. isHydrating.value = true
  392. try {
  393. const response = await agent.postAgentGetAgentInfo({ id: agentId })
  394. const result = response?.result
  395. if (!response?.isSuccess || !result) {
  396. throw new Error('获取智能体信息失败')
  397. }
  398. const normalizedNodes = (result.nodes || []).map(toWorkflowNode)
  399. workflow.value = {
  400. ...(result as unknown as IWorkflow),
  401. nodes: normalizedNodes,
  402. edges: (result.edges || []).map((edge: any, index: number) =>
  403. toWorkflowEdge(edge, index, normalizedNodes)
  404. )
  405. }
  406. await nextTick()
  407. } catch (error) {
  408. console.error('loadAgentWorkflow error', error)
  409. ElMessage.error('加载智能体流程失败')
  410. } finally {
  411. isHydrating.value = false
  412. }
  413. }
  414. const saveAgentMeta = async () => {
  415. if (!workflow.value?.id) return
  416. try {
  417. const response = await agent.postAgentDoEditAgent({
  418. data: workflow.value
  419. })
  420. handleApiResult(response, '智能体已保存', '保存智能体失败')
  421. } catch (error) {
  422. console.error('saveAgentMeta error', error)
  423. ElMessage.error('保存智能体失败')
  424. }
  425. }
  426. const saveAgentVariables = async () => {
  427. if (!workflow.value?.id) return
  428. try {
  429. const response = await agent.postAgentDoSaveAgentVariables({
  430. appAgentId: workflow.value.id,
  431. conversation_variables: workflow.value.conversation_variables || [],
  432. env_variables: (workflow.value.env_variables || []).map((item: any) => ({
  433. name: item?.name || '',
  434. value: item?.value ?? '',
  435. type: item?.type || 'string'
  436. }))
  437. })
  438. handleApiResult(response, '变量已保存', '保存变量失败')
  439. } catch (error) {
  440. console.error('saveAgentVariables error', error)
  441. ElMessage.error('保存变量失败')
  442. }
  443. }
  444. const scheduleSaveAgentMeta = () => {
  445. if (saveAgentTimer.value) window.clearTimeout(saveAgentTimer.value)
  446. if (!workflow.value?.id) return
  447. saveAgentTimer.value = window.setTimeout(() => {
  448. saveAgentMeta()
  449. }, 600)
  450. }
  451. const scheduleSaveAgentVariables = () => {
  452. if (saveVarsTimer.value) window.clearTimeout(saveVarsTimer.value)
  453. if (!workflow.value?.id) return
  454. saveVarsTimer.value = window.setTimeout(() => {
  455. saveAgentVariables()
  456. }, 600)
  457. }
  458. watch(
  459. () => workflow.value,
  460. (workflow) => {
  461. projectMap[workflow.id] = workflow
  462. localStorage.setItem(`workflow-map`, JSON.stringify(projectMap))
  463. },
  464. { deep: true }
  465. )
  466. watch(
  467. () => [workflow.value?.name, workflow.value?.description, workflow.value?.tags],
  468. () => {
  469. if (isHydrating.value) return
  470. scheduleSaveAgentMeta()
  471. },
  472. { deep: true }
  473. )
  474. watch(
  475. () => [workflow.value?.conversation_variables, workflow.value?.env_variables],
  476. () => {
  477. if (isHydrating.value) return
  478. scheduleSaveAgentVariables()
  479. },
  480. { deep: true }
  481. )
  482. /**
  483. * 监听路由参数变化
  484. */
  485. watch(
  486. () => route.params?.id,
  487. async (id) => {
  488. if (id) {
  489. await loadAgentWorkflow(id as string)
  490. }
  491. },
  492. { immediate: true }
  493. )
  494. /**
  495. * Editor
  496. */
  497. const handleFooterToggle = (open: boolean) => {
  498. footerHeight.value = open ? 200 : 32
  499. }
  500. /**
  501. * Workflow
  502. */
  503. const nodeID = ref('')
  504. const setterVisible = ref(false)
  505. const runVisible = ref(false)
  506. const pendingSetterInit = new Set<string>()
  507. const workflowRef = ref<InstanceType<typeof Workflow>>()
  508. const handleRunSelectedNode = async () => {
  509. if (!workflow.value?.id) {
  510. ElMessage.warning('请先选择需要运行的节点')
  511. return
  512. }
  513. if (!nodeID.value) {
  514. const selectedNode = workflow.value?.nodes?.find((node) => (node as any)?.selected)
  515. if (selectedNode?.id) {
  516. nodeID.value = selectedNode.id
  517. }
  518. }
  519. if (!nodeID.value) {
  520. ElMessage.warning('请选择需要测试的节点')
  521. return
  522. }
  523. try {
  524. const response = await agent.postAgentDoExecute({
  525. appAgentId: workflow.value.id,
  526. start_node_id: nodeID.value,
  527. is_debugger: true,
  528. responseType: 'ws',
  529. params: {}
  530. })
  531. const agentRunnerKey = response?.result
  532. if (agentRunnerKey) {
  533. runnerStore.startRunner(agentRunnerKey)
  534. }
  535. runVisible.value = false
  536. // handleApiResult(response, '已提交节点测试', '节点测试失败')
  537. } catch (error) {
  538. console.error('postDoTestNodeRunner error', error)
  539. ElMessage.error('节点测试失败')
  540. }
  541. }
  542. const handleRunNode = async (id: string) => {
  543. if (!workflow.value?.id) {
  544. ElMessage.warning('请先选择需要运行的节点')
  545. return
  546. }
  547. try {
  548. const response = await agent.postAgentDoExecute({
  549. appAgentId: workflow.value.id,
  550. start_node_id: id,
  551. is_debugger: true,
  552. responseType: 'ws',
  553. params: {}
  554. })
  555. const agentRunnerKey = response?.result
  556. if (agentRunnerKey) {
  557. runnerStore.startRunner(agentRunnerKey)
  558. }
  559. // handleApiResult(response, '已提交运行节点', '运行节点失败')
  560. } catch (error) {
  561. console.error('postDoTestNodeRunner error', error)
  562. ElMessage.error('运行节点失败')
  563. }
  564. }
  565. const handleNodeCreate = (value: { type: string; position?: XYPosition } | string) => {
  566. // TODO: 处理注释节点
  567. if (typeof value === 'string') {
  568. if (value === 'stickyNote') {
  569. workflow.value?.nodes.push({
  570. appAgentId: workflow.value.id,
  571. type: 'canvas-node',
  572. zIndex: -1,
  573. nodeType: 'stickyNote',
  574. position: { x: 600, y: 300 },
  575. id: 'stickyNote',
  576. name: '注释',
  577. remark: '',
  578. data: {
  579. id: '',
  580. version: ['1.0.0'],
  581. inputs: [],
  582. outputs: [],
  583. position: { x: 600, y: 300 },
  584. nodeType: 'stickyNote',
  585. content: '注释内容,可以使用 **Markdown** 语法进行格式化, 双击进入编辑。',
  586. width: 400,
  587. height: 200,
  588. color: '#fff5d6'
  589. }
  590. })
  591. }
  592. return
  593. }
  594. const nodeToAdd = nodeMap[value.type]?.schema
  595. const viewport = workflowRef.value?.getVueFlow()?.viewport
  596. // 如果存在对应节点则添加
  597. if (nodeToAdd) {
  598. const newNodeParam: any = {
  599. ...nodeToAdd,
  600. appAgentId: workflow.value?.id || '',
  601. position: value.position
  602. }
  603. // 获取当前画布的中心点
  604. if (newNodeParam.position && viewport) {
  605. newNodeParam.position = {
  606. // 计算当前中心点坐标(相对于画布)
  607. x: (-viewport.value.x + window.innerWidth / 2) / viewport.value.zoom,
  608. y: (-viewport.value.y + window.innerHeight / 2) / viewport.value.zoom
  609. }
  610. }
  611. // 需要连接前一个节点
  612. if (peddingHandlePayload.value) {
  613. const { position, handle, parentId } = peddingHandlePayload.value
  614. newNodeParam.position = position
  615. newNodeParam.prevNodeId = handle?.nodeId
  616. newNodeParam.parentId = parentId
  617. // 条件句柄
  618. if (handle?.handleId?.includes('_')) {
  619. newNodeParam.nodeHandleId = handle?.handleId
  620. }
  621. }
  622. if (!newNodeParam.position) {
  623. newNodeParam.position = nodeToAdd.position
  624. }
  625. // 根据连线添加节点
  626. if (peddingHandlePayload.value?.by === 'edge' && peddingHandlePayload.value?.connection) {
  627. const { connection } = peddingHandlePayload.value
  628. const params = {
  629. edgeId: connection.id,
  630. newNode: newNodeParam
  631. }
  632. agent.postAgentDoNewAgentNodeWithEdge(params).then(async (response) => {
  633. if (handleApiResult(response, '节点已添加', '新增节点失败')) {
  634. await loadAgentWorkflow(workflow.value.id)
  635. }
  636. })
  637. onHideNodeLibary()
  638. return
  639. }
  640. onHideNodeLibary()
  641. agent
  642. .postAgentDoNewAgentNode(newNodeParam)
  643. .then(async (response) => {
  644. if (handleApiResult(response, '节点已添加', '新增节点失败')) {
  645. await loadAgentWorkflow(workflow.value.id)
  646. }
  647. })
  648. .catch((error) => {
  649. console.error('postDoNewAgentNode error', error)
  650. ElMessage.error('新增节点失败')
  651. })
  652. }
  653. }
  654. const handleNodeClick = (id: string, _position: XYPosition) => {
  655. nodeID.value = id
  656. pendingSetterInit.add(id)
  657. setterVisible.value = true
  658. }
  659. const handleDrop = (position: XYPosition, event: DragEvent) => {
  660. const type = event.dataTransfer?.getData('application/x-node-type')
  661. if (!type) return
  662. handleNodeCreate({ type, position })
  663. }
  664. /**
  665. * 修改工作流名称
  666. */
  667. const handleRename = () => {
  668. inputRef.value?.focus()
  669. inputRef.value?.select()
  670. }
  671. /**
  672. * 删除工作流
  673. */
  674. const handleDelete = () => {
  675. ElMessageBox.confirm('确定要删除吗?', '提示', {
  676. confirmButtonText: '确定',
  677. cancelButtonText: '取消',
  678. type: 'warning'
  679. }).then(() => {
  680. localStorage.removeItem(`project_${id}`)
  681. router.push('/')
  682. })
  683. }
  684. /**
  685. * 创建连线
  686. */
  687. const normalizeConnectionEndpoints = (connection: Connection) => {
  688. return normalizeEdgeEndpoints(connection, workflow.value?.nodes || [])
  689. }
  690. const onCreateConnection = async (connection: Connection) => {
  691. const { sourceHandle } = connection
  692. const { source, target } = normalizeConnectionEndpoints(connection)
  693. const params: {
  694. appAgentId: string
  695. source: string
  696. target: string
  697. zIndex: number
  698. sourceHandle?: string
  699. } = {
  700. appAgentId: workflow.value.id,
  701. source,
  702. target,
  703. zIndex: 1
  704. }
  705. // 需要传sourceHandle的情况是中间带"_"
  706. if (sourceHandle && sourceHandle.includes('_')) {
  707. params.sourceHandle = sourceHandle
  708. }
  709. // 如果是source条件节点else的sourceHandle模式就是当前节点id
  710. // if (sourceHandle && sourceHandle.endsWith('-else')) {
  711. // params.sourceHandle = source
  712. // }
  713. if (!workflow.value?.edges.some((edge) => edge.source === source && edge.target === target)) {
  714. const response = await agent.postAgentDoNewEdge(params)
  715. if (handleApiResult(response, '连线已创建', '连线创建失败')) {
  716. await loadAgentWorkflow(workflow.value.id)
  717. }
  718. }
  719. }
  720. /**
  721. * 移动位置
  722. */
  723. const handleUpdateNodesPosition = (events: { id: string; position: XYPosition }[]) => {
  724. events?.forEach(({ id, position }) => {
  725. const node = workflow.value?.nodes.find((node) => node.id === id)
  726. if (node && !isPendingCreate(node)) {
  727. if (node.position?.x === position.x && node.position?.y === position.y) {
  728. return
  729. }
  730. node.position = position
  731. agent
  732. .postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
  733. .then((response) => {
  734. handleApiResult(response, undefined, '更新节点失败')
  735. })
  736. .catch((error) => {
  737. console.error('postDoUpdateAgentNode error', error)
  738. })
  739. }
  740. })
  741. }
  742. /**
  743. * 点击选中节点
  744. * @param id
  745. * @param position
  746. */
  747. const handleSelectNode = (id: string) => {
  748. workflow.value?.nodes.forEach((node) => {
  749. node.selected = false
  750. })
  751. const node = workflow.value?.nodes.find((node) => node.id === id)
  752. if (node) {
  753. node.selected = true
  754. }
  755. }
  756. /**
  757. * 修改节点数据
  758. */
  759. const handleUpdateNode = (node: IWorkflowNode) => {
  760. if (node && !isPendingCreate(node)) {
  761. if (pendingSetterInit.has(id)) {
  762. pendingSetterInit.delete(id)
  763. return
  764. }
  765. if (node.nodeType === 'if-else') {
  766. const cases = node.data?.cases || []
  767. const offsetHeight = (cases.length > 1 ? cases.length - 1 : 0) * 32
  768. node.height = 96 + offsetHeight
  769. }
  770. agent
  771. .postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
  772. .then((response) => {
  773. handleApiResult(response, undefined, '更新节点失败')
  774. })
  775. .catch((error) => {
  776. console.error('postDoUpdateAgentNode error', error)
  777. })
  778. }
  779. }
  780. /**
  781. * 修改节点属性
  782. */
  783. const handleUpdateNodeProps = (id: string, attrs: Record<string, unknown>) => {
  784. const node = workflow.value?.nodes.find((node) => node.id === id)
  785. if (node && !isPendingCreate(node)) {
  786. const keys = Object.keys(attrs || {})
  787. const meaningfulKeys = keys.filter((key) => !['selected', 'dragging'].includes(key))
  788. if (meaningfulKeys.length === 0) {
  789. return
  790. }
  791. if (node.data?.nodeType === 'stickyNote') {
  792. Object.assign(node.data, attrs)
  793. } else {
  794. Object.assign(node, attrs)
  795. }
  796. agent
  797. .postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
  798. .then((response) => {
  799. handleApiResult(response, undefined, '更新节点失败')
  800. })
  801. .catch((error) => {
  802. console.error('postDoUpdateAgentNode error', error)
  803. })
  804. }
  805. }
  806. /**
  807. * 删除节点
  808. */
  809. const handleDeleteNode = async (id: string) => {
  810. const index = workflow.value.nodes.findIndex((node) => node.id === id)
  811. if (index != -1) {
  812. ElMessageBox.confirm('确定要删除吗?', '提示', {
  813. confirmButtonText: '确定',
  814. cancelButtonText: '取消',
  815. type: 'warning'
  816. }).then(async () => {
  817. await agent.postAgentDoDeleteAgentNode({
  818. id: id
  819. })
  820. await loadAgentWorkflow(workflow.value.id)
  821. })
  822. }
  823. }
  824. /**
  825. * 删除连线
  826. */
  827. const handleDeleteEdge = async (connection: Connection) => {
  828. if (connection.id) {
  829. ElMessageBox.confirm('确定要删除吗?', '提示', {
  830. confirmButtonText: '确定',
  831. cancelButtonText: '取消',
  832. type: 'warning'
  833. }).then(async () => {
  834. await agent.postAgentDoDeleteEdge({
  835. id: connection.id
  836. })
  837. await loadAgentWorkflow(workflow.value.id)
  838. })
  839. }
  840. }
  841. /**
  842. * 修改环境变量
  843. */
  844. const handleChangeEnvVars = async (
  845. envVars: {
  846. name: string
  847. value: string
  848. type: 'string' | 'number' | 'boolean' | 'object' | 'array'
  849. }[]
  850. ) => {
  851. const response = await agent.postAgentDoSaveAgentVariables({
  852. appAgentId: workflow.value.id,
  853. conversation_variables: [],
  854. env_variables: envVars
  855. })
  856. handleApiResult(response, '环境变量已保存', '保存环境变量失败')
  857. await loadAgentWorkflow(workflow.value.id)
  858. }
  859. /**
  860. * 连线取消时
  861. */
  862. const onConnectionOpenNodeLibary = async (payload: {
  863. handle: ConnectStartEvent
  864. position: XYPosition
  865. event: MouseEvent
  866. parentId?: string
  867. }) => {
  868. await nextTick()
  869. const { handle } = payload
  870. libaryRefferenceRef.value = handle.event?.target as HTMLElement
  871. showNodeLibary.value = true
  872. peddingHandlePayload.value = payload
  873. }
  874. /**
  875. * 节点库隐藏时
  876. */
  877. const onHideNodeLibary = () => {
  878. peddingHandlePayload.value = undefined
  879. libaryRefferenceRef.value = undefined
  880. showNodeLibary.value = false
  881. }
  882. /**
  883. * 连线添加按钮触发
  884. */
  885. const handleClickConectionAdd = (connection: Connection) => {
  886. const el = document.querySelector(`[edge-add-btn="${connection.id}"]`) as HTMLElement
  887. const screenToFlowCoordinate = workflowRef.value?.getVueFlow()?.screenToFlowCoordinate
  888. if (el && screenToFlowCoordinate) {
  889. const rect = el.getBoundingClientRect()
  890. const position = screenToFlowCoordinate({
  891. x: rect.left,
  892. y: rect.top
  893. })
  894. libaryRefferenceRef.value = el
  895. showNodeLibary.value = true
  896. peddingHandlePayload.value = {
  897. by: 'edge',
  898. position,
  899. connection
  900. }
  901. }
  902. }
  903. onBeforeUnmount(() => {
  904. if (saveAgentTimer.value) window.clearTimeout(saveAgentTimer.value)
  905. if (saveVarsTimer.value) window.clearTimeout(saveVarsTimer.value)
  906. resetDisplayedNodeStatuses()
  907. layout?.setMainStyle({})
  908. })
  909. </script>