You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

use-workflow.ts 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. import {
  2. useCallback,
  3. } from 'react'
  4. import { uniqBy } from 'lodash-es'
  5. import { useTranslation } from 'react-i18next'
  6. import {
  7. getIncomers,
  8. getOutgoers,
  9. useStoreApi,
  10. } from 'reactflow'
  11. import type {
  12. Connection,
  13. } from 'reactflow'
  14. import type {
  15. BlockEnum,
  16. Edge,
  17. Node,
  18. ValueSelector,
  19. } from '../types'
  20. import {
  21. WorkflowRunningStatus,
  22. } from '../types'
  23. import {
  24. useStore,
  25. useWorkflowStore,
  26. } from '../store'
  27. import { getParallelInfo } from '../utils'
  28. import {
  29. PARALLEL_DEPTH_LIMIT,
  30. SUPPORT_OUTPUT_VARS_NODE,
  31. } from '../constants'
  32. import type { IterationNodeType } from '../nodes/iteration/types'
  33. import type { LoopNodeType } from '../nodes/loop/types'
  34. import { CUSTOM_NOTE_NODE } from '../note-node/constants'
  35. import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
  36. import { useAvailableBlocks } from './use-available-blocks'
  37. import { useStore as useAppStore } from '@/app/components/app/store'
  38. import {
  39. fetchAllBuiltInTools,
  40. fetchAllCustomTools,
  41. fetchAllMCPTools,
  42. fetchAllWorkflowTools,
  43. } from '@/service/tools'
  44. import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
  45. import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
  46. import { basePath } from '@/utils/var'
  47. import { canFindTool } from '@/utils'
  48. import { MAX_PARALLEL_LIMIT } from '@/config'
  49. import { useNodesMetaData } from '.'
  50. export const useIsChatMode = () => {
  51. const appDetail = useAppStore(s => s.appDetail)
  52. return appDetail?.mode === 'advanced-chat'
  53. }
  54. export const useWorkflow = () => {
  55. const { t } = useTranslation()
  56. const store = useStoreApi()
  57. const workflowStore = useWorkflowStore()
  58. const { getAvailableBlocks } = useAvailableBlocks()
  59. const { nodesMap } = useNodesMetaData()
  60. const getNodeById = useCallback((nodeId: string) => {
  61. const {
  62. getNodes,
  63. } = store.getState()
  64. const nodes = getNodes()
  65. const currentNode = nodes.find(node => node.id === nodeId)
  66. return currentNode
  67. }, [store])
  68. const getTreeLeafNodes = useCallback((nodeId: string) => {
  69. const {
  70. getNodes,
  71. edges,
  72. } = store.getState()
  73. const nodes = getNodes()
  74. const currentNode = nodes.find(node => node.id === nodeId)
  75. let startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
  76. if (currentNode?.parentId) {
  77. const startNode = nodes.find(node => node.parentId === currentNode.parentId && (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_LOOP_START_NODE))
  78. if (startNode)
  79. startNodes = [startNode]
  80. }
  81. if (!startNodes.length)
  82. return []
  83. const list: Node[] = []
  84. const preOrder = (root: Node, callback: (node: Node) => void) => {
  85. if (root.id === nodeId)
  86. return
  87. const outgoers = getOutgoers(root, nodes, edges)
  88. if (outgoers.length) {
  89. outgoers.forEach((outgoer) => {
  90. preOrder(outgoer, callback)
  91. })
  92. }
  93. else {
  94. if (root.id !== nodeId)
  95. callback(root)
  96. }
  97. }
  98. startNodes.forEach((startNode) => {
  99. preOrder(startNode, (node) => {
  100. list.push(node)
  101. })
  102. })
  103. const incomers = getIncomers({ id: nodeId } as Node, nodes, edges)
  104. list.push(...incomers)
  105. return uniqBy(list, 'id').filter((item: Node) => {
  106. return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
  107. })
  108. }, [store, nodesMap])
  109. const getBeforeNodesInSameBranch = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
  110. const {
  111. getNodes,
  112. edges,
  113. } = store.getState()
  114. const nodes = newNodes || getNodes()
  115. const currentNode = nodes.find(node => node.id === nodeId)
  116. const list: Node[] = []
  117. if (!currentNode)
  118. return list
  119. if (currentNode.parentId) {
  120. const parentNode = nodes.find(node => node.id === currentNode.parentId)
  121. if (parentNode) {
  122. const parentList = getBeforeNodesInSameBranch(parentNode.id)
  123. list.push(...parentList)
  124. }
  125. }
  126. const traverse = (root: Node, callback: (node: Node) => void) => {
  127. if (root) {
  128. const incomers = getIncomers(root, nodes, newEdges || edges)
  129. if (incomers.length) {
  130. incomers.forEach((node) => {
  131. if (!list.find(n => node.id === n.id)) {
  132. callback(node)
  133. traverse(node, callback)
  134. }
  135. })
  136. }
  137. }
  138. }
  139. traverse(currentNode, (node) => {
  140. list.push(node)
  141. })
  142. const length = list.length
  143. if (length) {
  144. return uniqBy(list, 'id').reverse().filter((item: Node) => {
  145. return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
  146. })
  147. }
  148. return []
  149. }, [store])
  150. const getBeforeNodesInSameBranchIncludeParent = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
  151. const nodes = getBeforeNodesInSameBranch(nodeId, newNodes, newEdges)
  152. const {
  153. getNodes,
  154. } = store.getState()
  155. const allNodes = getNodes()
  156. const node = allNodes.find(n => n.id === nodeId)
  157. const parentNodeId = node?.parentId
  158. const parentNode = allNodes.find(n => n.id === parentNodeId)
  159. if (parentNode)
  160. nodes.push(parentNode)
  161. return nodes
  162. }, [getBeforeNodesInSameBranch, store])
  163. const getAfterNodesInSameBranch = useCallback((nodeId: string) => {
  164. const {
  165. getNodes,
  166. edges,
  167. } = store.getState()
  168. const nodes = getNodes()
  169. const currentNode = nodes.find(node => node.id === nodeId)!
  170. if (!currentNode)
  171. return []
  172. const list: Node[] = [currentNode]
  173. const traverse = (root: Node, callback: (node: Node) => void) => {
  174. if (root) {
  175. const outgoers = getOutgoers(root, nodes, edges)
  176. if (outgoers.length) {
  177. outgoers.forEach((node) => {
  178. callback(node)
  179. traverse(node, callback)
  180. })
  181. }
  182. }
  183. }
  184. traverse(currentNode, (node) => {
  185. list.push(node)
  186. })
  187. return uniqBy(list, 'id')
  188. }, [store])
  189. const getBeforeNodeById = useCallback((nodeId: string) => {
  190. const {
  191. getNodes,
  192. edges,
  193. } = store.getState()
  194. const nodes = getNodes()
  195. const node = nodes.find(node => node.id === nodeId)!
  196. return getIncomers(node, nodes, edges)
  197. }, [store])
  198. const getIterationNodeChildren = useCallback((nodeId: string) => {
  199. const {
  200. getNodes,
  201. } = store.getState()
  202. const nodes = getNodes()
  203. return nodes.filter(node => node.parentId === nodeId)
  204. }, [store])
  205. const getLoopNodeChildren = useCallback((nodeId: string) => {
  206. const {
  207. getNodes,
  208. } = store.getState()
  209. const nodes = getNodes()
  210. return nodes.filter(node => node.parentId === nodeId)
  211. }, [store])
  212. const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
  213. const { getNodes, setNodes } = store.getState()
  214. const afterNodes = getAfterNodesInSameBranch(nodeId)
  215. const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
  216. if (effectNodes.length > 0) {
  217. const newNodes = getNodes().map((node) => {
  218. if (effectNodes.find(n => n.id === node.id))
  219. return updateNodeVars(node, oldValeSelector, newVarSelector)
  220. return node
  221. })
  222. setNodes(newNodes)
  223. }
  224. }, [store])
  225. const isVarUsedInNodes = useCallback((varSelector: ValueSelector) => {
  226. const nodeId = varSelector[0]
  227. const afterNodes = getAfterNodesInSameBranch(nodeId)
  228. const effectNodes = findUsedVarNodes(varSelector, afterNodes)
  229. return effectNodes.length > 0
  230. }, [getAfterNodesInSameBranch])
  231. const removeUsedVarInNodes = useCallback((varSelector: ValueSelector) => {
  232. const nodeId = varSelector[0]
  233. const { getNodes, setNodes } = store.getState()
  234. const afterNodes = getAfterNodesInSameBranch(nodeId)
  235. const effectNodes = findUsedVarNodes(varSelector, afterNodes)
  236. if (effectNodes.length > 0) {
  237. const newNodes = getNodes().map((node) => {
  238. if (effectNodes.find(n => n.id === node.id))
  239. return updateNodeVars(node, varSelector, [])
  240. return node
  241. })
  242. setNodes(newNodes)
  243. }
  244. }, [getAfterNodesInSameBranch, store])
  245. const isNodeVarsUsedInNodes = useCallback((node: Node, isChatMode: boolean) => {
  246. const outputVars = getNodeOutputVars(node, isChatMode)
  247. const isUsed = outputVars.some((varSelector) => {
  248. return isVarUsedInNodes(varSelector)
  249. })
  250. return isUsed
  251. }, [isVarUsedInNodes])
  252. const checkParallelLimit = useCallback((nodeId: string, nodeHandle = 'source') => {
  253. const {
  254. edges,
  255. } = store.getState()
  256. const connectedEdges = edges.filter(edge => edge.source === nodeId && edge.sourceHandle === nodeHandle)
  257. if (connectedEdges.length > MAX_PARALLEL_LIMIT - 1) {
  258. const { setShowTips } = workflowStore.getState()
  259. setShowTips(t('workflow.common.parallelTip.limit', { num: MAX_PARALLEL_LIMIT }))
  260. return false
  261. }
  262. return true
  263. }, [store, workflowStore, t])
  264. const getRootNodesById = useCallback((nodeId: string) => {
  265. const {
  266. getNodes,
  267. edges,
  268. } = store.getState()
  269. const nodes = getNodes()
  270. const currentNode = nodes.find(node => node.id === nodeId)
  271. const rootNodes: Node[] = []
  272. if (!currentNode)
  273. return rootNodes
  274. if (currentNode.parentId) {
  275. const parentNode = nodes.find(node => node.id === currentNode.parentId)
  276. if (parentNode) {
  277. const parentList = getRootNodesById(parentNode.id)
  278. rootNodes.push(...parentList)
  279. }
  280. }
  281. const traverse = (root: Node, callback: (node: Node) => void) => {
  282. if (root) {
  283. const incomers = getIncomers(root, nodes, edges)
  284. if (incomers.length) {
  285. incomers.forEach((node) => {
  286. traverse(node, callback)
  287. })
  288. }
  289. else {
  290. callback(root)
  291. }
  292. }
  293. }
  294. traverse(currentNode, (node) => {
  295. rootNodes.push(node)
  296. })
  297. const length = rootNodes.length
  298. if (length)
  299. return uniqBy(rootNodes, 'id')
  300. return []
  301. }, [store])
  302. const getStartNodes = useCallback((nodes: Node[], currentNode?: Node) => {
  303. const { id, parentId } = currentNode || {}
  304. let startNodes: Node[] = []
  305. if (parentId) {
  306. const parentNode = nodes.find(node => node.id === parentId)
  307. if (!parentNode)
  308. throw new Error('Parent node not found')
  309. const startNode = nodes.find(node => node.id === (parentNode.data as (IterationNodeType | LoopNodeType)).start_node_id)
  310. if (startNode)
  311. startNodes = [startNode]
  312. }
  313. else {
  314. startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
  315. }
  316. if (!startNodes.length)
  317. startNodes = getRootNodesById(id || '')
  318. return startNodes
  319. }, [nodesMap, getRootNodesById])
  320. const checkNestedParallelLimit = useCallback((nodes: Node[], edges: Edge[], targetNode?: Node) => {
  321. const startNodes = getStartNodes(nodes, targetNode)
  322. for (let i = 0; i < startNodes.length; i++) {
  323. const {
  324. parallelList,
  325. hasAbnormalEdges,
  326. } = getParallelInfo(startNodes[i], nodes, edges)
  327. const { workflowConfig } = workflowStore.getState()
  328. if (hasAbnormalEdges)
  329. return false
  330. for (let i = 0; i < parallelList.length; i++) {
  331. const parallel = parallelList[i]
  332. if (parallel.depth > (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT)) {
  333. const { setShowTips } = workflowStore.getState()
  334. setShowTips(t('workflow.common.parallelTip.depthLimit', { num: (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT) }))
  335. return false
  336. }
  337. }
  338. }
  339. return true
  340. }, [t, workflowStore, getStartNodes])
  341. const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
  342. const {
  343. edges,
  344. getNodes,
  345. } = store.getState()
  346. const nodes = getNodes()
  347. const sourceNode: Node = nodes.find(node => node.id === source)!
  348. const targetNode: Node = nodes.find(node => node.id === target)!
  349. if (!checkParallelLimit(source!, sourceHandle || 'source'))
  350. return false
  351. if (sourceNode.type === CUSTOM_NOTE_NODE || targetNode.type === CUSTOM_NOTE_NODE)
  352. return false
  353. if (sourceNode.parentId !== targetNode.parentId)
  354. return false
  355. if (sourceNode && targetNode) {
  356. const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNode.data.type, !!sourceNode.parentId).availableNextBlocks
  357. const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNode.data.type, !!targetNode.parentId).availablePrevBlocks
  358. if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type))
  359. return false
  360. if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type))
  361. return false
  362. }
  363. const hasCycle = (node: Node, visited = new Set()) => {
  364. if (visited.has(node.id))
  365. return false
  366. visited.add(node.id)
  367. for (const outgoer of getOutgoers(node, nodes, edges)) {
  368. if (outgoer.id === source)
  369. return true
  370. if (hasCycle(outgoer, visited))
  371. return true
  372. }
  373. }
  374. return !hasCycle(targetNode)
  375. }, [store, checkParallelLimit, getAvailableBlocks])
  376. return {
  377. getNodeById,
  378. getTreeLeafNodes,
  379. getBeforeNodesInSameBranch,
  380. getBeforeNodesInSameBranchIncludeParent,
  381. getAfterNodesInSameBranch,
  382. handleOutVarRenameChange,
  383. isVarUsedInNodes,
  384. removeUsedVarInNodes,
  385. isNodeVarsUsedInNodes,
  386. checkParallelLimit,
  387. checkNestedParallelLimit,
  388. isValidConnection,
  389. getBeforeNodeById,
  390. getIterationNodeChildren,
  391. getLoopNodeChildren,
  392. getRootNodesById,
  393. getStartNodes,
  394. }
  395. }
  396. export const useFetchToolsData = () => {
  397. const workflowStore = useWorkflowStore()
  398. const handleFetchAllTools = useCallback(async (type: string) => {
  399. if (type === 'builtin') {
  400. const buildInTools = await fetchAllBuiltInTools()
  401. if (basePath) {
  402. buildInTools.forEach((item) => {
  403. if (typeof item.icon == 'string' && !item.icon.includes(basePath))
  404. item.icon = `${basePath}${item.icon}`
  405. })
  406. }
  407. workflowStore.setState({
  408. buildInTools: buildInTools || [],
  409. })
  410. }
  411. if (type === 'custom') {
  412. const customTools = await fetchAllCustomTools()
  413. workflowStore.setState({
  414. customTools: customTools || [],
  415. })
  416. }
  417. if (type === 'workflow') {
  418. const workflowTools = await fetchAllWorkflowTools()
  419. workflowStore.setState({
  420. workflowTools: workflowTools || [],
  421. })
  422. }
  423. if (type === 'mcp') {
  424. const mcpTools = await fetchAllMCPTools()
  425. workflowStore.setState({
  426. mcpTools: mcpTools || [],
  427. })
  428. }
  429. }, [workflowStore])
  430. return {
  431. handleFetchAllTools,
  432. }
  433. }
  434. export const useWorkflowReadOnly = () => {
  435. const workflowStore = useWorkflowStore()
  436. const workflowRunningData = useStore(s => s.workflowRunningData)
  437. const getWorkflowReadOnly = useCallback(() => {
  438. return workflowStore.getState().workflowRunningData?.result.status === WorkflowRunningStatus.Running
  439. }, [workflowStore])
  440. return {
  441. workflowReadOnly: workflowRunningData?.result.status === WorkflowRunningStatus.Running,
  442. getWorkflowReadOnly,
  443. }
  444. }
  445. export const useNodesReadOnly = () => {
  446. const workflowStore = useWorkflowStore()
  447. const workflowRunningData = useStore(s => s.workflowRunningData)
  448. const historyWorkflowData = useStore(s => s.historyWorkflowData)
  449. const isRestoring = useStore(s => s.isRestoring)
  450. const getNodesReadOnly = useCallback(() => {
  451. const {
  452. workflowRunningData,
  453. historyWorkflowData,
  454. isRestoring,
  455. } = workflowStore.getState()
  456. return workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring
  457. }, [workflowStore])
  458. return {
  459. nodesReadOnly: !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring),
  460. getNodesReadOnly,
  461. }
  462. }
  463. export const useIsNodeInIteration = (iterationId: string) => {
  464. const store = useStoreApi()
  465. const isNodeInIteration = useCallback((nodeId: string) => {
  466. const {
  467. getNodes,
  468. } = store.getState()
  469. const nodes = getNodes()
  470. const node = nodes.find(node => node.id === nodeId)
  471. if (!node)
  472. return false
  473. if (node.parentId === iterationId)
  474. return true
  475. return false
  476. }, [iterationId, store])
  477. return {
  478. isNodeInIteration,
  479. }
  480. }
  481. export const useIsNodeInLoop = (loopId: string) => {
  482. const store = useStoreApi()
  483. const isNodeInLoop = useCallback((nodeId: string) => {
  484. const {
  485. getNodes,
  486. } = store.getState()
  487. const nodes = getNodes()
  488. const node = nodes.find(node => node.id === nodeId)
  489. if (!node)
  490. return false
  491. if (node.parentId === loopId)
  492. return true
  493. return false
  494. }, [loopId, store])
  495. return {
  496. isNodeInLoop,
  497. }
  498. }