feat: Merge parent workflow nodes into subgraph variable scope.And some

performance improve.
This commit is contained in:
zhsama
2026-01-23 01:11:05 +08:00
parent e22996735f
commit ef8d0f497d
11 changed files with 131 additions and 72 deletions

View File

@ -6,7 +6,7 @@ import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/typ
import type { Edge, Node } from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import { useStoreApi } from 'reactflow'
import { WorkflowWithInnerContext } from '@/app/components/workflow'
import { InteractionMode, WorkflowWithInnerContext } from '@/app/components/workflow'
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
import { useInspectVarsCrudCommon } from '@/app/components/workflow/hooks/use-inspect-vars-crud-common'
import { BlockEnum } from '@/app/components/workflow/types'
@ -96,7 +96,7 @@ const SubGraphMain: FC<SubGraphMainProps> = (props) => {
}, [selectableNodeTypes, variant])
const hooksStore = useMemo(() => ({
interactionMode: 'subgraph',
interactionMode: InteractionMode.Subgraph,
subGraphSelectableNodeTypes: resolvedSelectableTypes,
availableNodesMetaData,
configsMap,
@ -135,7 +135,7 @@ const SubGraphMain: FC<SubGraphMainProps> = (props) => {
hooksStore={hooksStore as any}
allowSelectionWhenReadOnly
canvasReadOnly
interactionMode="subgraph"
interactionMode={InteractionMode.Subgraph}
>
{subGraphChildren}
</WorkflowWithInnerContext>

View File

@ -1,10 +1,12 @@
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { useCallback } from 'react'
import { useShallow } from 'zustand/react/shallow'
import {
useIsChatMode,
useWorkflow,
useWorkflowVariables,
} from '@/app/components/workflow/hooks'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
type Params = {
@ -15,6 +17,20 @@ type Params = {
passedInAvailableNodes?: Node[]
}
const mergeAvailableNodes = (baseNodes: Node[], extraNodes: Node[]) => {
if (!extraNodes.length)
return baseNodes
const merged = new Map<string, Node>()
baseNodes.forEach((node) => {
merged.set(node.id, node)
})
extraNodes.forEach((node) => {
if (!merged.has(node.id))
merged.set(node.id, node)
})
return Array.from(merged.values())
}
const getNodeInfo = (nodeId: string, nodes: Node[]) => {
const allNodes = nodes
const node = allNodes.find(n => n.id === nodeId)
@ -44,12 +60,14 @@ const useNodesAvailableVarList = (nodes: Node[], {
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode()
const parentAvailableNodes = useStore(useShallow(s => s.parentAvailableNodes)) || []
const nodeAvailabilityMap: { [key: string ]: { availableVars: NodeOutPutVar[], availableNodes: Node[] } } = {}
nodes.forEach((node) => {
const nodeId = node.id
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
const baseAvailableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
const availableNodes = mergeAvailableNodes(baseAvailableNodes, parentAvailableNodes)
if (node.data.type === BlockEnum.Loop)
availableNodes.push(node)
@ -79,6 +97,7 @@ export const useGetNodesAvailableVarList = () => {
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode()
const parentAvailableNodes = useStore(useShallow(s => s.parentAvailableNodes)) || []
const getNodesAvailableVarList = useCallback((nodes: Node[], {
onlyLeafNodeVar,
filterVar,
@ -93,7 +112,8 @@ export const useGetNodesAvailableVarList = () => {
nodes.forEach((node) => {
const nodeId = node.id
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
const baseAvailableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
const availableNodes = mergeAvailableNodes(baseAvailableNodes, parentAvailableNodes)
if (node.data.type === BlockEnum.Loop)
availableNodes.push(node)
@ -117,7 +137,7 @@ export const useGetNodesAvailableVarList = () => {
nodeAvailabilityMap[nodeId] = result
})
return nodeAvailabilityMap
}, [getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent, getNodeAvailableVars, isChatMode])
}, [getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent, getNodeAvailableVars, isChatMode, parentAvailableNodes])
return {
getNodesAvailableVarList,
}

View File

@ -134,6 +134,11 @@ const edgeTypes = {
[CUSTOM_EDGE]: CustomEdge,
}
export enum InteractionMode {
Default = 'default',
Subgraph = 'subgraph',
}
export type WorkflowProps = {
nodes: Node[]
edges: Edge[]
@ -142,7 +147,7 @@ export type WorkflowProps = {
onWorkflowDataUpdate?: (v: any) => void
allowSelectionWhenReadOnly?: boolean
canvasReadOnly?: boolean
interactionMode?: 'default' | 'subgraph'
interactionMode?: InteractionMode
}
export const Workflow: FC<WorkflowProps> = memo(({
nodes: originalNodes,
@ -223,7 +228,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
const store = useStoreApi()
eventEmitter?.useSubscription((v: any) => {
if (v.type === WORKFLOW_DATA_UPDATE) {
if (interactionMode === 'subgraph')
if (interactionMode === InteractionMode.Subgraph)
return
setNodes(v.payload.nodes)
store.getState().setNodes(v.payload.nodes)

View File

@ -270,8 +270,9 @@ const VarReferencePicker: FC<Props> = ({
}, [onChange, varKindType])
const handleVariableJump = useCallback((nodeId: string) => {
const currentNodeIndex = availableNodes.findIndex(node => node.id === nodeId)
const currentNode = availableNodes[currentNodeIndex]
const currentNode = nodes.find(node => node.id === nodeId)
if (!currentNode)
return
const workflowContainer = document.getElementById('workflow-container')
const {
@ -289,7 +290,7 @@ const VarReferencePicker: FC<Props> = ({
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
}, [availableNodes, reactflow, store])
}, [nodes, reactflow, store])
const type = getCurrentVariableType({
parentNode: (isInIteration ? iterationNode : loopNode) as any,

View File

@ -1,4 +1,6 @@
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import {
useIsChatMode,
useWorkflow,
@ -31,7 +33,23 @@ const useAvailableVarList = (nodeId: string, {
const { getTreeLeafNodes, getNodeById, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode()
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
const baseAvailableNodes = useMemo(() => {
return passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
}, [passedInAvailableNodes, onlyLeafNodeVar, nodeId, getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent])
const parentAvailableNodes = useWorkflowStore(useShallow(s => s.parentAvailableNodes)) || []
const availableNodes = useMemo(() => {
if (!parentAvailableNodes.length)
return baseAvailableNodes
const merged = new Map<string, Node>()
baseAvailableNodes.forEach((node) => {
merged.set(node.id, node)
})
parentAvailableNodes.forEach((node) => {
if (!merged.has(node.id))
merged.set(node.id, node)
})
return Array.from(merged.values())
}, [baseAvailableNodes, parentAvailableNodes])
const {
parentNode: iterationNode,
} = useNodeInfo(nodeId)
@ -71,10 +89,12 @@ const useAvailableVarList = (nodeId: string, {
hideEnv,
hideChatVar,
}), ...dataSourceRagVars]
const availableNodesWithParent = [
...availableNodes,
...(isDataSourceNode ? [currNode] : []),
]
const availableNodesWithParent = useMemo(() => {
return [
...availableNodes,
...(isDataSourceNode ? [currNode] : []),
]
}, [availableNodes, currNode, isDataSourceNode])
const llmNodeIds = new Set(
availableNodesWithParent
.filter(node => node?.data.type === BlockEnum.LLM)

View File

@ -1,4 +1,4 @@
import type { CommonNodeType, InputVar, TriggerNodeType, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
import type { CommonNodeType, InputVar, Node, TriggerNodeType, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
import type { FlowType } from '@/types/common'
import type { NodeRunResult, NodeTracing } from '@/types/workflow'
import { unionBy } from 'es-toolkit/compat'
@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
import {
useStoreApi,
} from 'reactflow'
import { useShallow } from 'zustand/react/shallow'
import { trackEvent } from '@/app/components/base/amplitude'
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
import Toast from '@/app/components/base/toast'
@ -150,9 +151,24 @@ const useOneStepRun = <T>({
const isIteration = data.type === BlockEnum.Iteration
const isLoop = data.type === BlockEnum.Loop
const isStartNode = data.type === BlockEnum.Start
const parentAvailableNodes = useStore(useShallow(s => s.parentAvailableNodes)) || []
const availableNodes = getBeforeNodesInSameBranch(id)
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
const mergeAvailableNodes = (baseNodes: Node[]) => {
if (!parentAvailableNodes.length)
return baseNodes
const merged = new Map<string, Node>()
baseNodes.forEach((node) => {
merged.set(node.id, node)
})
parentAvailableNodes.forEach((node) => {
if (!merged.has(node.id))
merged.set(node.id, node)
})
return Array.from(merged.values())
}
const availableNodes = mergeAvailableNodes(getBeforeNodesInSameBranch(id))
const availableNodesIncludeParent = mergeAvailableNodes(getBeforeNodesInSameBranchIncludeParent(id))
const workflowStore = useWorkflowStore()
const { schemaTypeDefinitions } = useMatchSchemaType()

View File

@ -1,10 +1,12 @@
import type { RefObject } from 'react'
import type { IterationNodeType } from './types'
import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types'
import type { InputVar, Node, ValueSelector, Variable } from '@/app/components/workflow/types'
import type { NodeTracing } from '@/types/workflow'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import formatTracing from '@/app/components/workflow/run/utils/format-log'
import { useStore } from '@/app/components/workflow/store'
import { InputVarType, VarType } from '@/app/components/workflow/types'
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
import { useIsNodeInIteration, useWorkflow } from '../../hooks'
@ -34,8 +36,22 @@ const useSingleRunFormParams = ({
const { isNodeInIteration } = useIsNodeInIteration(id)
const { getIterationNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
const parentAvailableNodes = useStore(useShallow(s => s.parentAvailableNodes)) || []
const iterationChildrenNodes = getIterationNodeChildren(id)
const beforeNodes = getBeforeNodesInSameBranch(id)
const beforeNodes = (() => {
const baseBeforeNodes = getBeforeNodesInSameBranch(id)
if (!parentAvailableNodes.length)
return baseBeforeNodes
const merged = new Map<string, Node>()
baseBeforeNodes.forEach((node) => {
merged.set(node.id, node)
})
parentAvailableNodes.forEach((node) => {
if (!merged.has(node.id))
merged.set(node.id, node)
})
return Array.from(merged.values())
})()
const canChooseVarNodes = [...beforeNodes, ...iterationChildrenNodes]
const iteratorInputKey = `${id}.input_selector`

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import type { ModelConfig, Node, NodeOutPutVar, PromptItem, PromptMessageContext, PromptTemplateItem, ValueSelector, Var, Variable } from '../../../types'
import type { ModelConfig, NodeOutPutVar, PromptItem, PromptMessageContext, PromptTemplateItem, ValueSelector, Var, Variable } from '../../../types'
import { produce } from 'immer'
import * as React from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
@ -18,7 +18,7 @@ import AddButton from '@/app/components/workflow/nodes/_base/components/add-butt
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { cn } from '@/utils/classnames'
import { useStore, useWorkflowStore } from '../../../store'
import { useWorkflowStore } from '../../../store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '../../../types'
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
import ConfigContextItem from './config-context-item'
@ -88,39 +88,9 @@ const ConfigPrompt: FC<Props> = ({
onlyLeafNodeVar: false,
filterVar,
})
const parentAvailableVars = useStore(state => state.parentAvailableVars) || []
const parentAvailableNodes = useStore(state => state.parentAvailableNodes) || []
const mergedAvailableVars = useMemo(() => {
if (!parentAvailableVars.length)
return availableVars
const merged = new Map<string, NodeOutPutVar>()
availableVars.forEach((item) => {
merged.set(item.nodeId, item)
})
parentAvailableVars.forEach((item) => {
if (!merged.has(item.nodeId))
merged.set(item.nodeId, item)
})
return Array.from(merged.values())
}, [availableVars, parentAvailableVars])
const mergedAvailableNodesWithParent = useMemo(() => {
if (!parentAvailableNodes.length)
return availableNodesWithParent
const merged = new Map<string, Node>()
availableNodesWithParent.forEach((node) => {
merged.set(node.id, node)
})
parentAvailableNodes.forEach((node) => {
if (!merged.has(node.id))
merged.set(node.id, node)
})
return Array.from(merged.values())
}, [availableNodesWithParent, parentAvailableNodes])
const contextVarOptions = useMemo<NodeOutPutVar[]>(() => {
return mergedAvailableNodesWithParent
return availableNodesWithParent
.filter(node => node.data.type === BlockEnum.Agent || node.data.type === BlockEnum.LLM)
.map(node => ({
nodeId: node.id,
@ -133,7 +103,7 @@ const ConfigPrompt: FC<Props> = ({
},
],
}))
}, [mergedAvailableNodesWithParent])
}, [availableNodesWithParent])
const handleChatModePromptChange = useCallback((index: number) => {
return (prompt: string) => {
@ -315,7 +285,7 @@ const ConfigPrompt: FC<Props> = ({
readOnly={readOnly}
payload={item}
contextVars={contextVarOptions}
availableNodes={mergedAvailableNodesWithParent}
availableNodes={availableNodesWithParent}
onChange={handleContextChange(index)}
onRemove={handleRemove(index)}
/>
@ -354,8 +324,8 @@ const ConfigPrompt: FC<Props> = ({
onRemove={handleRemove(index)}
isShowContext={isShowContext}
hasSetBlockStatus={hasSetBlockStatus}
availableVars={mergedAvailableVars}
availableNodes={mergedAvailableNodesWithParent}
availableVars={availableVars}
availableNodes={availableNodesWithParent}
varList={varList}
handleAddVariable={handleAddVariable}
modelConfig={modelConfig}
@ -425,8 +395,8 @@ const ConfigPrompt: FC<Props> = ({
isChatApp={isChatApp}
isShowContext={isShowContext}
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={mergedAvailableVars}
availableNodes={mergedAvailableNodesWithParent}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
isSupportPromptGenerator
isSupportJinja
editionType={(payload as PromptItem).edition_type}

View File

@ -1,9 +1,11 @@
import type { InputVar, ValueSelector, Variable } from '../../types'
import type { InputVar, Node, ValueSelector, Variable } from '../../types'
import type { CaseItem, Condition, LoopNodeType } from './types'
import type { NodeTracing } from '@/types/workflow'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import formatTracing from '@/app/components/workflow/run/utils/format-log'
import { useStore } from '@/app/components/workflow/store'
import { ValueType } from '@/app/components/workflow/types'
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
import { useIsNodeInLoop, useWorkflow } from '../../hooks'
@ -35,8 +37,22 @@ const useSingleRunFormParams = ({
const { isNodeInLoop } = useIsNodeInLoop(id)
const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
const parentAvailableNodes = useStore(useShallow(s => s.parentAvailableNodes)) || []
const loopChildrenNodes = getLoopNodeChildren(id)
const beforeNodes = getBeforeNodesInSameBranch(id)
const beforeNodes = (() => {
const baseBeforeNodes = getBeforeNodesInSameBranch(id)
if (!parentAvailableNodes.length)
return baseBeforeNodes
const merged = new Map<string, Node>()
baseBeforeNodes.forEach((node) => {
merged.set(node.id, node)
})
parentAvailableNodes.forEach((node) => {
if (!merged.has(node.id))
merged.set(node.id, node)
})
return Array.from(merged.values())
})()
const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes]
const { usedOutVars, allVarObject } = (() => {

View File

@ -1,6 +1,6 @@
import type { PointerEvent, RefObject } from 'react'
import type { ContextGenerateResponse } from '@/service/debug'
import { RiArrowDownSLine, RiCheckLine, RiCloseLine } from '@remixicon/react'
import { RiArrowDownSLine, RiCheckLine, RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
@ -178,6 +178,7 @@ const RightPanel = ({
onClick={onRun}
disabled={!canRun || isGenerating}
>
<RiPlayLargeLine className="mr-1 h-4 w-4" />
{t('nodes.tool.contextGenerate.run', { ns: 'workflow' })}
</Button>
)}

View File

@ -18,7 +18,7 @@ import { useIsChatMode, useNodesSyncDraft, useWorkflow, useWorkflowVariables } f
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types'
import { EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types'
import SubGraphCanvas from './sub-graph-canvas'
const SubGraphModal: FC<SubGraphModalProps> = (props) => {
@ -64,17 +64,11 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
return getBeforeNodesInSameBranch(toolNodeId, workflowNodes, workflowEdges)
}, [getBeforeNodesInSameBranch, isOpen, toolNodeId, workflowEdges, workflowNodes])
const parentContextNodes = useMemo(() => {
if (!parentBeforeNodes.length || !isAgentVariant)
return []
return parentBeforeNodes.filter(node => node.data.type === BlockEnum.Agent || node.data.type === BlockEnum.LLM)
}, [isAgentVariant, parentBeforeNodes])
const parentAvailableNodes = useMemo(() => {
if (!isOpen)
return []
return isAgentVariant ? parentContextNodes : parentBeforeNodes
}, [isAgentVariant, isOpen, parentBeforeNodes, parentContextNodes])
return parentBeforeNodes
}, [isOpen, parentBeforeNodes])
const parentAvailableVars = useMemo(() => {
if (!parentAvailableNodes.length)