feat(web): unify node selector availability rules

This commit is contained in:
yyh
2026-02-13 12:16:24 +08:00
parent 49b115b1ea
commit 086f6a2bb3
10 changed files with 779 additions and 22 deletions

View File

@ -0,0 +1,142 @@
import { describe, expect, it } from 'vitest'
import { BlockEnum } from '@/app/components/workflow/types'
import {
buildNodeSelectorAvailabilityContext,
filterNodesForSelector,
isNodeAvailableInSelector,
NodeSelectorSandboxMode,
NodeSelectorScene,
} from './node-availability'
type MockNode = {
metaData: {
type: BlockEnum
}
}
describe('node-availability', () => {
it('should hide command and file-upload when sandbox is disabled', () => {
const mockNodes: MockNode[] = [
{ metaData: { type: BlockEnum.Start } },
{ metaData: { type: BlockEnum.Command } },
{ metaData: { type: BlockEnum.FileUpload } },
]
const result = filterNodesForSelector(mockNodes, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Disabled,
})
const nodeTypes = result.map(node => node.metaData.type)
expect(nodeTypes).toEqual([BlockEnum.Start])
})
it('should keep command and file-upload when sandbox is enabled', () => {
const mockNodes: MockNode[] = [
{ metaData: { type: BlockEnum.Start } },
{ metaData: { type: BlockEnum.Command } },
{ metaData: { type: BlockEnum.FileUpload } },
]
const result = filterNodesForSelector(mockNodes, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Enabled,
})
const nodeTypes = result.map(node => node.metaData.type)
expect(nodeTypes).toEqual([BlockEnum.Start, BlockEnum.Command, BlockEnum.FileUpload])
})
it('should return original reference when no filtering is needed', () => {
const mockNodes: MockNode[] = [
{ metaData: { type: BlockEnum.Start } },
{ metaData: { type: BlockEnum.End } },
]
const result = filterNodesForSelector(mockNodes, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Disabled,
})
expect(result).toBe(mockNodes)
})
it('should mark command and file-upload as sandbox-only', () => {
expect(isNodeAvailableInSelector(BlockEnum.Command, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Disabled,
})).toBe(false)
expect(isNodeAvailableInSelector(BlockEnum.FileUpload, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Disabled,
})).toBe(false)
expect(isNodeAvailableInSelector(BlockEnum.Start, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Disabled,
})).toBe(true)
})
it('should hide agent when sandbox is enabled', () => {
expect(isNodeAvailableInSelector(BlockEnum.Agent, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Enabled,
})).toBe(false)
expect(isNodeAvailableInSelector(BlockEnum.Agent, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Disabled,
})).toBe(true)
})
it('should hide human-input in rag pipeline flow', () => {
expect(isNodeAvailableInSelector(BlockEnum.HumanInput, {
scene: NodeSelectorScene.RagPipeline,
sandboxMode: NodeSelectorSandboxMode.Enabled,
})).toBe(false)
expect(isNodeAvailableInSelector(BlockEnum.HumanInput, {
scene: NodeSelectorScene.RagPipeline,
sandboxMode: NodeSelectorSandboxMode.Disabled,
})).toBe(false)
expect(isNodeAvailableInSelector(BlockEnum.HumanInput, {
scene: NodeSelectorScene.Workflow,
sandboxMode: NodeSelectorSandboxMode.Disabled,
})).toBe(true)
})
it('should build unsupported sandbox mode for scenes that do not support sandbox', () => {
const context = buildNodeSelectorAvailabilityContext({
scene: NodeSelectorScene.RagPipeline,
isSandboxRuntime: true,
isSandboxFeatureEnabled: true,
})
expect(context.scene).toBe(NodeSelectorScene.RagPipeline)
expect(context.sandboxMode).toBe(NodeSelectorSandboxMode.Unsupported)
})
it('should allow explicit scene sandbox support override', () => {
const context = buildNodeSelectorAvailabilityContext({
scene: NodeSelectorScene.RagPipeline,
supportsSandbox: true,
isSandboxRuntime: true,
isSandboxFeatureEnabled: false,
})
expect(context.sandboxMode).toBe(NodeSelectorSandboxMode.Enabled)
})
it('should build enabled sandbox mode when runtime or feature enables sandbox', () => {
const contextByRuntime = buildNodeSelectorAvailabilityContext({
scene: NodeSelectorScene.Workflow,
isSandboxRuntime: true,
isSandboxFeatureEnabled: false,
})
const contextByFeature = buildNodeSelectorAvailabilityContext({
scene: NodeSelectorScene.Workflow,
isSandboxRuntime: false,
isSandboxFeatureEnabled: true,
})
expect(contextByRuntime.sandboxMode).toBe(NodeSelectorSandboxMode.Enabled)
expect(contextByFeature.sandboxMode).toBe(NodeSelectorSandboxMode.Enabled)
})
})

View File

@ -0,0 +1,115 @@
import { BlockEnum } from '@/app/components/workflow/types'
export enum NodeSelectorScene {
Workflow = 'workflow',
Chatflow = 'chatflow',
RagPipeline = 'rag-pipeline',
Subgraph = 'subgraph',
}
export enum NodeSelectorSandboxMode {
Enabled = 'enabled',
Disabled = 'disabled',
Unsupported = 'unsupported',
}
export const NODE_SELECTOR_SCENE_SUPPORTS_SANDBOX: Record<NodeSelectorScene, boolean> = {
[NodeSelectorScene.Workflow]: true,
[NodeSelectorScene.Chatflow]: true,
[NodeSelectorScene.RagPipeline]: false,
[NodeSelectorScene.Subgraph]: true,
}
type NodeAvailabilityRule = {
sandboxOnly?: boolean
hiddenWhenSandboxEnabled?: boolean
hiddenInScenes?: NodeSelectorScene[]
}
export const NODE_SELECTOR_AVAILABILITY_RULES: Partial<Record<BlockEnum, NodeAvailabilityRule>> = {
[BlockEnum.Command]: { sandboxOnly: true },
[BlockEnum.FileUpload]: { sandboxOnly: true },
[BlockEnum.Agent]: { hiddenWhenSandboxEnabled: true },
[BlockEnum.HumanInput]: { hiddenInScenes: [NodeSelectorScene.RagPipeline] },
}
export type NodeSelectorAvailabilityContext = {
scene: NodeSelectorScene
sandboxMode: NodeSelectorSandboxMode
}
type BuildNodeSelectorAvailabilityContextProps = {
scene: NodeSelectorScene
isSandboxRuntime?: boolean
isSandboxFeatureEnabled?: boolean
supportsSandbox?: boolean
}
const resolveSandboxMode = ({
scene,
isSandboxRuntime = false,
isSandboxFeatureEnabled = false,
supportsSandbox = NODE_SELECTOR_SCENE_SUPPORTS_SANDBOX[scene],
}: BuildNodeSelectorAvailabilityContextProps): NodeSelectorSandboxMode => {
if (!supportsSandbox)
return NodeSelectorSandboxMode.Unsupported
return (isSandboxRuntime || isSandboxFeatureEnabled)
? NodeSelectorSandboxMode.Enabled
: NodeSelectorSandboxMode.Disabled
}
export const buildNodeSelectorAvailabilityContext = ({
scene,
isSandboxRuntime,
isSandboxFeatureEnabled,
supportsSandbox,
}: BuildNodeSelectorAvailabilityContextProps): NodeSelectorAvailabilityContext => {
return {
scene,
sandboxMode: resolveSandboxMode({
scene,
isSandboxRuntime,
isSandboxFeatureEnabled,
supportsSandbox,
}),
}
}
export const isNodeAvailableInSelector = (
nodeType: BlockEnum,
{ scene, sandboxMode }: NodeSelectorAvailabilityContext,
) => {
const rule = NODE_SELECTOR_AVAILABILITY_RULES[nodeType]
if (!rule)
return true
const sandboxEnabled = sandboxMode === NodeSelectorSandboxMode.Enabled
if (rule.sandboxOnly && !sandboxEnabled)
return false
if (rule.hiddenWhenSandboxEnabled && sandboxEnabled)
return false
if (rule.hiddenInScenes?.includes(scene))
return false
return true
}
type NodeLike = {
metaData: {
type: BlockEnum
}
}
export const filterNodesForSelector = <T extends NodeLike>(
nodes: T[],
context: NodeSelectorAvailabilityContext,
) => {
const filteredNodes = nodes.filter(node => isNodeAvailableInSelector(node.metaData.type, context))
if (filteredNodes.length === nodes.length)
return nodes
return filteredNodes
}