fix(web): sys variables not supported in snippet

This commit is contained in:
JzoNg
2026-05-27 21:13:19 +08:00
parent 83cd1a8d7a
commit fdfc9ab3d3
6 changed files with 183 additions and 19 deletions

View File

@ -3,6 +3,7 @@ import type { WorkflowProps } from '@/app/components/workflow'
import type { SnippetDetailPayload, SnippetInputField } from '@/models/snippet'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
import { BlockEnum } from '@/app/components/workflow/types'
import { PipelineInputVarType } from '@/models/pipeline'
import SnippetMain from '../snippet-main'
@ -20,6 +21,7 @@ const mockHandleStartWorkflowRun = vi.fn()
const mockHandleStopRun = vi.fn()
const mockHandleWorkflowStartRunInWorkflow = vi.fn()
const mockHandleCheckBeforePublish = vi.fn()
const mockUseAvailableNodesMetaData = vi.hoisted(() => vi.fn())
const mockInspectVarsCrud = {
hasNodeInspectVars: vi.fn(),
hasSetInspectVar: vi.fn(),
@ -75,6 +77,10 @@ vi.mock('@/app/components/workflow/hooks/use-checklist', () => ({
}),
}))
vi.mock('@/app/components/workflow-app/hooks', () => ({
useAvailableNodesMetaData: () => mockUseAvailableNodesMetaData(),
}))
vi.mock('@/app/components/snippets/hooks/use-inspect-vars-crud', () => ({
useInspectVarsCrud: () => mockInspectVarsCrud,
}))
@ -210,6 +216,14 @@ const renderSnippetMain = () => {
)
}
const createNodeMetadata = (type: BlockEnum) => ({
metaData: {
type,
},
defaultValue: {},
checkValid: vi.fn(),
})
describe('SnippetMain', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -222,6 +236,24 @@ describe('SnippetMain', () => {
},
refetch: vi.fn(),
})
const llmNodeMetadata = createNodeMetadata(BlockEnum.LLM)
const humanInputNodeMetadata = createNodeMetadata(BlockEnum.HumanInput)
const endNodeMetadata = createNodeMetadata(BlockEnum.End)
const knowledgeRetrievalNodeMetadata = createNodeMetadata(BlockEnum.KnowledgeRetrieval)
mockUseAvailableNodesMetaData.mockReturnValue({
nodes: [
llmNodeMetadata,
humanInputNodeMetadata,
endNodeMetadata,
knowledgeRetrievalNodeMetadata,
],
nodesMap: {
[BlockEnum.LLM]: llmNodeMetadata,
[BlockEnum.HumanInput]: humanInputNodeMetadata,
[BlockEnum.End]: endNodeMetadata,
[BlockEnum.KnowledgeRetrieval]: knowledgeRetrievalNodeMetadata,
},
})
mockHandleCheckBeforePublish.mockResolvedValue(true)
capturedHooksStore = undefined
snippetDetailStoreState = {
@ -320,6 +352,23 @@ describe('SnippetMain', () => {
})
})
describe('Block Selector', () => {
it('should filter unsupported snippet block types from available node metadata', () => {
renderSnippetMain()
const availableNodesMetaData = capturedHooksStore?.availableNodesMetaData as {
nodes: Array<{ metaData: { type: BlockEnum } }>
nodesMap: Partial<Record<BlockEnum, unknown>>
}
expect(availableNodesMetaData.nodes.map(node => node.metaData.type)).toEqual([BlockEnum.LLM])
expect(availableNodesMetaData.nodesMap[BlockEnum.LLM]).toBeDefined()
expect(availableNodesMetaData.nodesMap[BlockEnum.HumanInput]).toBeUndefined()
expect(availableNodesMetaData.nodesMap[BlockEnum.End]).toBeUndefined()
expect(availableNodesMetaData.nodesMap[BlockEnum.KnowledgeRetrieval]).toBeUndefined()
})
})
describe('Run Hooks', () => {
it('should pass snippet run handlers to WorkflowWithInnerContext', () => {
renderSnippetMain()

View File

@ -38,6 +38,12 @@ type SnippetMainContentProps = {
onCancel: () => void | Promise<void>
}
const unsupportedSnippetBlockTypes = new Set([
BlockEnum.HumanInput,
BlockEnum.End,
BlockEnum.KnowledgeRetrieval,
])
const SnippetMainContent = ({
snippetId,
fields,
@ -110,7 +116,7 @@ const SnippetMain = ({
} = publishedWorkflowQuery
const availableNodesMetaData = useMemo(() => {
const nodes = workflowAvailableNodesMetaData.nodes.filter(node =>
node.metaData.type !== BlockEnum.HumanInput && node.metaData.type !== BlockEnum.End)
!unsupportedSnippetBlockTypes.has(node.metaData.type))
if (!workflowAvailableNodesMetaData.nodesMap)
return { nodes }
@ -118,6 +124,7 @@ const SnippetMain = ({
const {
[BlockEnum.HumanInput]: _humanInput,
[BlockEnum.End]: _end,
[BlockEnum.KnowledgeRetrieval]: _knowledgeRetrieval,
...nodesMap
} = workflowAvailableNodesMetaData.nodesMap

View File

@ -9,7 +9,7 @@ import {
} from '@/app/components/workflow/hooks'
import {
appendSnippetInputFieldVars,
isSnippetCanvas,
filterSnippetSystemVars,
} from '@/app/components/workflow/nodes/_base/hooks/snippet-input-field-vars'
import { BlockEnum } from '@/app/components/workflow/types'
@ -36,18 +36,6 @@ const getNodeInfo = (nodeId: string, nodes: Node[]) => {
}
}
const filterSystemVarsForSnippet = (availableVars: NodeOutPutVar[]) => {
if (!isSnippetCanvas())
return availableVars
return availableVars
.map(nodeVar => ({
...nodeVar,
vars: nodeVar.vars.filter(variable => !variable.variable.startsWith('sys.')),
}))
.filter(nodeVar => nodeVar.vars.length > 0)
}
// TODO: loop type?
const useNodesAvailableVarList = (nodes: Node[], {
onlyLeafNodeVar,
@ -82,7 +70,7 @@ const useNodesAvailableVarList = (nodes: Node[], {
parentNode: iterationNode,
} = getNodeInfo(nodeId, nodes)
const availableVars = filterSystemVarsForSnippet([
const availableVars = filterSnippetSystemVars([
...snippetInputFieldAvailability.availableVars,
...getNodeAvailableVars({
parentNode: iterationNode,
@ -136,7 +124,7 @@ export const useGetNodesAvailableVarList = () => {
parentNode: iterationNode,
} = getNodeInfo(nodeId, nodes)
const availableVars = filterSystemVarsForSnippet([
const availableVars = filterSnippetSystemVars([
...snippetInputFieldAvailability.availableVars,
...getNodeAvailableVars({
parentNode: iterationNode,

View File

@ -0,0 +1,108 @@
import type { Node, NodeOutPutVar, Var } from '@/app/components/workflow/types'
import { renderHook } from '@testing-library/react'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import useAvailableVarList from '../use-available-var-list'
const mockGetTreeLeafNodes = vi.hoisted(() => vi.fn())
const mockGetBeforeNodesInSameBranchIncludeParent = vi.hoisted(() => vi.fn())
const mockGetNodeById = vi.hoisted(() => vi.fn())
const mockGetNodeAvailableVars = vi.hoisted(() => vi.fn())
vi.mock('@/app/components/snippets/store', () => ({
useSnippetDetailStore: (selector: (state: { fields: unknown[] }) => unknown) => selector({ fields: [] }),
}))
vi.mock('@/app/components/workflow/hooks', () => ({
useIsChatMode: () => true,
useWorkflow: () => ({
getTreeLeafNodes: mockGetTreeLeafNodes,
getBeforeNodesInSameBranchIncludeParent: mockGetBeforeNodesInSameBranchIncludeParent,
getNodeById: mockGetNodeById,
}),
useWorkflowVariables: () => ({
getNodeAvailableVars: mockGetNodeAvailableVars,
}),
}))
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: { ragPipelineVariables: unknown[] }) => unknown) => selector({ ragPipelineVariables: [] }),
}))
vi.mock('../use-node-info', () => ({
default: () => ({
parentNode: null,
}),
}))
const createNode = (overrides: Partial<Node> = {}): Node => ({
id: 'node-1',
type: 'custom',
position: { x: 0, y: 0 },
data: {
type: BlockEnum.LLM,
title: 'Node',
desc: '',
},
...overrides,
} as Node)
const outputVarsWithSystemVars: NodeOutPutVar[] = [
{
nodeId: 'vars-node',
title: 'Vars',
vars: [
{
variable: 'answer',
type: VarType.string,
},
{
variable: 'sys.files',
type: VarType.arrayFile,
},
] satisfies Var[],
},
{
nodeId: 'global',
title: 'SYSTEM',
vars: [{
variable: 'sys.user_id',
type: VarType.string,
}] satisfies Var[],
},
]
describe('useAvailableVarList', () => {
beforeEach(() => {
vi.clearAllMocks()
globalThis.history.pushState({}, '', '/')
mockGetBeforeNodesInSameBranchIncludeParent.mockReturnValue([createNode({ id: 'before-node' })])
mockGetTreeLeafNodes.mockReturnValue([createNode({ id: 'leaf-node' })])
mockGetNodeById.mockReturnValue(createNode({ id: 'node-1' }))
mockGetNodeAvailableVars.mockReturnValue(outputVarsWithSystemVars)
})
it('filters system variables on snippet canvases', () => {
globalThis.history.pushState({}, '', '/snippets/snippet-1/orchestrate')
const { result } = renderHook(() => useAvailableVarList('node-1', {
filterVar: () => true,
}))
expect(result.current.availableVars).toEqual([{
nodeId: 'vars-node',
title: 'Vars',
vars: [{
variable: 'answer',
type: VarType.string,
}],
}])
})
it('keeps system variables outside snippet canvases', () => {
const { result } = renderHook(() => useAvailableVarList('node-1', {
filterVar: () => true,
}))
expect(result.current.availableVars).toEqual(outputVarsWithSystemVars)
})
})

View File

@ -14,6 +14,18 @@ export const isSnippetCanvas = () => {
return /^\/snippets\/[^/]+\/orchestrate/.test(globalThis.location.pathname)
}
export const filterSnippetSystemVars = (availableVars: NodeOutPutVar[]) => {
if (!isSnippetCanvas())
return availableVars
return availableVars
.map(nodeVar => ({
...nodeVar,
vars: nodeVar.vars.filter(variable => !variable.variable.startsWith('sys.')),
}))
.filter(nodeVar => nodeVar.vars.length > 0)
}
const toWorkflowInputType = (type: SnippetInputField['type']) => type as unknown as InputVarType
export const buildSnippetInputFieldNode = (

View File

@ -9,7 +9,7 @@ import {
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { inputVarTypeToVarType } from '../../data-source/utils'
import { appendSnippetInputFieldVars } from './snippet-input-field-vars'
import { appendSnippetInputFieldVars, filterSnippetSystemVars } from './snippet-input-field-vars'
import useNodeInfo from './use-node-info'
type Params = {
@ -73,7 +73,7 @@ const useAvailableVarList = (nodeId: string, {
})
}
}
const availableVars = [
const availableVars = filterSnippetSystemVars([
...snippetInputFieldAvailability.availableVars,
...getNodeAvailableVars({
parentNode: iterationNode,
@ -84,7 +84,7 @@ const useAvailableVarList = (nodeId: string, {
hideChatVar,
}),
...dataSourceRagVars,
]
])
return {
availableVars,