mirror of
https://github.com/langgenius/dify.git
synced 2026-08-29 03:45:08 +08:00
fix: allow publishing workflows with legacy Agent nodes (#41368)
This commit is contained in:
+7
-7
@@ -40,7 +40,7 @@ describe('useAvailableNodesMetaData', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should expose legacy Agent instead of Agent v2 in chat mode when Agent v2 is enabled', () => {
|
||||
it('should expose only legacy Agent in chat mode while retaining Agent v2 metadata', () => {
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
@@ -49,7 +49,7 @@ describe('useAvailableNodesMetaData', () => {
|
||||
expect(nodeTypes).toContain(BlockEnum.Agent)
|
||||
expect(nodeTypes).not.toContain(BlockEnum.AgentV2)
|
||||
expect(result.current.nodesMap?.[BlockEnum.Agent]).toBeDefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeUndefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include workflow-specific trigger and end nodes outside chat mode', () => {
|
||||
@@ -88,7 +88,7 @@ describe('useAvailableNodesMetaData', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should expose Agent v2 instead of legacy Agent when Agent v2 is enabled', () => {
|
||||
it('should hide legacy Agent from the node picker but retain its validator when Agent v2 is enabled', () => {
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
@@ -96,14 +96,14 @@ describe('useAvailableNodesMetaData', () => {
|
||||
|
||||
expect(nodeTypes).toContain(BlockEnum.AgentV2)
|
||||
expect(nodeTypes).not.toContain(BlockEnum.Agent)
|
||||
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeDefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.Agent]).toBeUndefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.AgentV2]?.checkValid).toEqual(expect.any(Function))
|
||||
expect(result.current.nodesMap?.[BlockEnum.Agent]?.checkValid).toEqual(expect.any(Function))
|
||||
expect(result.current.nodesMap?.[BlockEnum.AgentV2]?.metaData.helpLinkUri).toBe(
|
||||
'/docs/use-dify/nodes/agent#choose-an-agent',
|
||||
)
|
||||
})
|
||||
|
||||
it('should expose legacy Agent instead of Agent v2 when Agent v2 is disabled', () => {
|
||||
it('should expose only legacy Agent while retaining Agent v2 metadata when Agent v2 is disabled', () => {
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
mockIsAgentV2Enabled.mockReturnValue(false)
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('useAvailableNodesMetaData', () => {
|
||||
expect(nodeTypes).toContain(BlockEnum.Agent)
|
||||
expect(nodeTypes).not.toContain(BlockEnum.AgentV2)
|
||||
expect(result.current.nodesMap?.[BlockEnum.Agent]).toBeDefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeUndefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeDefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.Agent]?.metaData.helpLinkUri).toBe(
|
||||
'/docs/use-dify/nodes/agent',
|
||||
)
|
||||
|
||||
@@ -43,14 +43,8 @@ export const useAvailableNodesMetaData = () => {
|
||||
)
|
||||
|
||||
const mergedNodesMetaData = useMemo(() => {
|
||||
const commonNodes = WORKFLOW_COMMON_NODES.filter((node) =>
|
||||
shouldUseAgentV2
|
||||
? node.metaData.type !== BlockEnum.Agent
|
||||
: node.metaData.type !== BlockEnum.AgentV2,
|
||||
)
|
||||
|
||||
return [
|
||||
...commonNodes,
|
||||
...WORKFLOW_COMMON_NODES,
|
||||
startNodeMetaData,
|
||||
...(isChatMode
|
||||
? [AnswerDefault]
|
||||
@@ -62,9 +56,9 @@ export const useAvailableNodesMetaData = () => {
|
||||
TriggerPluginDefault,
|
||||
]),
|
||||
]
|
||||
}, [isChatMode, shouldUseAgentV2, startNodeMetaData])
|
||||
}, [isChatMode, startNodeMetaData])
|
||||
|
||||
const availableNodesMetaData = useMemo(
|
||||
const nodesMetaData = useMemo(
|
||||
() =>
|
||||
mergedNodesMetaData.map((node) => {
|
||||
const { metaData } = node
|
||||
@@ -93,25 +87,35 @@ export const useAvailableNodesMetaData = () => {
|
||||
[mergedNodesMetaData, t, docLink],
|
||||
)
|
||||
|
||||
const availableNodesMetaDataMap = useMemo(
|
||||
const availableNodesMetaData = useMemo(
|
||||
() =>
|
||||
availableNodesMetaData.reduce(
|
||||
nodesMetaData.filter((node) =>
|
||||
shouldUseAgentV2
|
||||
? node.metaData.type !== BlockEnum.Agent
|
||||
: node.metaData.type !== BlockEnum.AgentV2,
|
||||
),
|
||||
[nodesMetaData, shouldUseAgentV2],
|
||||
)
|
||||
|
||||
const nodesMetaDataMap = useMemo(
|
||||
() =>
|
||||
nodesMetaData.reduce(
|
||||
(acc, node) => {
|
||||
acc![node.metaData.type] = node
|
||||
return acc
|
||||
},
|
||||
{} as AvailableNodesMetaData['nodesMap'],
|
||||
),
|
||||
[availableNodesMetaData],
|
||||
[nodesMetaData],
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
nodes: availableNodesMetaData,
|
||||
nodesMap: {
|
||||
...availableNodesMetaDataMap,
|
||||
[BlockEnum.VariableAssigner]: availableNodesMetaDataMap?.[BlockEnum.VariableAggregator],
|
||||
...nodesMetaDataMap,
|
||||
[BlockEnum.VariableAssigner]: nodesMetaDataMap?.[BlockEnum.VariableAggregator],
|
||||
},
|
||||
}
|
||||
}, [availableNodesMetaData, availableNodesMetaDataMap])
|
||||
}, [availableNodesMetaData, nodesMetaDataMap])
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ChecklistItem } from '../use-checklist'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { zWorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps/zod.gen'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import { act, screen, waitFor } from '@testing-library/react'
|
||||
import { createElement, Fragment } from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
@@ -14,7 +14,7 @@ import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock
|
||||
import { renderWorkflowComponent, renderWorkflowHook } from '../../__tests__/workflow-test-env'
|
||||
import { useStore } from '../../store'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { useChecklist, useWorkflowRunValidation } from '../use-checklist'
|
||||
import { useChecklist, useChecklistBeforePublish, useWorkflowRunValidation } from '../use-checklist'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
@@ -113,7 +113,11 @@ vi.mock('../use-nodes-available-var-list', () => ({
|
||||
}
|
||||
return map
|
||||
},
|
||||
useGetNodesAvailableVarList: () => ({ getNodesAvailableVarList: vi.fn(() => ({})) }),
|
||||
useGetNodesAvailableVarList: () => ({
|
||||
getNodesAvailableVarList: vi.fn((nodes: Node[]) =>
|
||||
Object.fromEntries(nodes.map((node) => [node.id, { availableVars: [] }])),
|
||||
),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../nodes/_base/components/variable/utils', () => ({
|
||||
@@ -223,6 +227,24 @@ function buildConnectedGraph() {
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
function buildLegacyAgentGraph() {
|
||||
const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } })
|
||||
const agentNode = createNode({
|
||||
id: 'legacy-agent',
|
||||
data: {
|
||||
type: BlockEnum.Agent,
|
||||
title: 'Legacy Agent',
|
||||
agent_strategy_provider_name: 'provider',
|
||||
agent_strategy_name: 'strategy',
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
nodes: [startNode, agentNode],
|
||||
edges: [createEdge({ source: 'start', target: 'legacy-agent' })],
|
||||
}
|
||||
}
|
||||
|
||||
function buildInlineAgentGraph({
|
||||
difyTools = [],
|
||||
hasModel = true,
|
||||
@@ -399,6 +421,19 @@ describe('useChecklist', () => {
|
||||
expect(warning!.errorMessages).toContain('Model not configured')
|
||||
})
|
||||
|
||||
it('should validate legacy Agent nodes when their metadata is hidden by Agent v2', () => {
|
||||
const { nodes, edges } = buildLegacyAgentGraph()
|
||||
|
||||
const { result } = renderWorkflowHook(() => useChecklist(nodes, edges))
|
||||
|
||||
expect(result.current).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'legacy-agent',
|
||||
errorMessages: ['workflow.nodes.agent.checkList.strategyNotSelected'],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
errorMessage: 'agentV2.agentDetail.configure.files.missing',
|
||||
@@ -768,6 +803,27 @@ describe('useChecklist', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useChecklistBeforePublish
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('useChecklistBeforePublish', () => {
|
||||
it('should reject an invalid legacy Agent instead of throwing when its metadata is hidden', async () => {
|
||||
const { nodes, edges } = buildLegacyAgentGraph()
|
||||
rfState.nodes = nodes as unknown as typeof rfState.nodes
|
||||
rfState.edges = edges as unknown as typeof rfState.edges
|
||||
|
||||
const { result } = renderWorkflowHook(() => useChecklistBeforePublish())
|
||||
let isValid: boolean | undefined
|
||||
|
||||
await act(async () => {
|
||||
isValid = await result.current.handleCheckBeforePublish()
|
||||
})
|
||||
|
||||
expect(isValid).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useWorkflowRunValidation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -55,6 +55,7 @@ import { useDatasetsDetailStore } from '../datasets-detail-store/store'
|
||||
import { useHooksStore } from '../hooks-store/store'
|
||||
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
|
||||
import { hasValidInlineAgentBinding, isAgentV2NodeData } from '../nodes/agent-v2/types'
|
||||
import AgentDefault from '../nodes/agent/default'
|
||||
import { IndexMethodEnum } from '../nodes/knowledge-base/types'
|
||||
import {
|
||||
getLLMModelIssue,
|
||||
@@ -94,6 +95,9 @@ export type ChecklistItem = {
|
||||
}
|
||||
|
||||
type CheckValidExtraData = Record<string, unknown> | undefined
|
||||
type NodeValidator = NonNullable<
|
||||
ReturnType<typeof useNodesMetaData>['nodesMap']
|
||||
>[BlockEnum]['checkValid']
|
||||
|
||||
const EMPTY_ENVIRONMENT_VARIABLES: EnvironmentVariable[] = []
|
||||
|
||||
@@ -106,6 +110,17 @@ const withFlowType = (moreDataForCheckValid: CheckValidExtraData, flowType?: Flo
|
||||
}
|
||||
}
|
||||
|
||||
const resolveNodeValidator = (
|
||||
data: CommonNodeType,
|
||||
nodesExtraData: ReturnType<typeof useNodesMetaData>['nodesMap'],
|
||||
): NodeValidator | undefined => {
|
||||
const validator = nodesExtraData?.[getNodeCatalogType(data)]?.checkValid
|
||||
if (validator) return validator
|
||||
|
||||
if (data.type === BlockEnum.Agent && !isAgentV2NodeData(data))
|
||||
return AgentDefault.checkValid as NodeValidator
|
||||
}
|
||||
|
||||
const START_NODE_TYPES: BlockEnum[] = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
@@ -391,7 +406,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
|
||||
if (node!.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node!.data)
|
||||
const validator = nodesExtraData?.[getNodeCatalogType(node!.data)]?.checkValid
|
||||
const validator = resolveNodeValidator(node!.data, nodesExtraData)
|
||||
const isPluginMissing = isNodePluginMissing(node!.data, {
|
||||
builtInTools: buildInTools,
|
||||
customTools,
|
||||
@@ -785,15 +800,18 @@ export const useChecklistBeforePublish = () => {
|
||||
}
|
||||
|
||||
const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap)
|
||||
const { errorMessage } = nodesExtraData![getNodeCatalogType(node!.data)].checkValid(
|
||||
checkData,
|
||||
t,
|
||||
withFlowType(moreDataForCheckValid, flowType),
|
||||
)
|
||||
const validator = resolveNodeValidator(node!.data, nodesExtraData)
|
||||
if (validator) {
|
||||
const { errorMessage } = validator(
|
||||
checkData,
|
||||
t,
|
||||
withFlowType(moreDataForCheckValid, flowType),
|
||||
)
|
||||
|
||||
if (errorMessage) {
|
||||
toast.error(`[${node!.data.title}] ${errorMessage}`)
|
||||
return false
|
||||
if (errorMessage) {
|
||||
toast.error(`[${node!.data.title}] ${errorMessage}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateOutputMessages = duplicateEndOutputMessages.get(node!.id) || []
|
||||
|
||||
Reference in New Issue
Block a user