refactor: use bivariance to normalize node metadata types

This commit is contained in:
zhsama 2026-01-23 06:37:21 +08:00
parent ef8d0f497d
commit 4707a319e5
11 changed files with 157 additions and 67 deletions

View File

@ -1,4 +1,5 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { CommonNodeType, NodeDefault, NodeDefaultBase } from '@/app/components/workflow/types'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
@ -29,25 +30,47 @@ export const useAvailableNodesMetaData = () => {
'/use-dify/knowledge/knowledge-pipeline/knowledge-pipeline-orchestration',
), [docLink])
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
return {
...node,
metaData: {
const availableNodesMetaData = useMemo<NodeDefaultBase[]>(() => {
const toNodeDefaultBase = (
node: NodeDefault<CommonNodeType>,
metaData: NodeDefaultBase['metaData'],
defaultValue: Partial<CommonNodeType>,
): NodeDefaultBase => {
return {
...node,
metaData,
defaultValue,
checkValid: (payload: CommonNodeType, translator, moreDataForCheckValid) => {
// normalize validator signature for shared metadata storage.
return node.checkValid(payload, translator, moreDataForCheckValid)
},
getOutputVars: node.getOutputVars
? (payload: CommonNodeType, allPluginInfoList, ragVariables, utils) => {
// normalize output var signature for shared metadata storage.
return node.getOutputVars!(payload, allPluginInfoList, ragVariables, utils)
}
: undefined,
}
}
return mergedNodesMetaData.map((node) => {
// normalize per-node defaults into a shared metadata shape.
const typedNode = node as NodeDefault<CommonNodeType>
const { metaData } = typedNode
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
return toNodeDefaultBase(typedNode, {
...metaData,
title,
description,
helpLinkUri,
},
defaultValue: {
...node.defaultValue,
}, {
...typedNode.defaultValue,
type: metaData.type,
title,
},
}
}), [mergedNodesMetaData, t])
})
})
}, [mergedNodesMetaData, t, helpLinkUri])
const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => {
acc![node.metaData.type] = node

View File

@ -131,8 +131,7 @@ const SubGraphMain: FC<SubGraphMainProps> = (props) => {
nodes={nodes}
edges={edges}
viewport={viewport}
// eslint-disable-next-line ts/no-explicit-any -- TODO: remove after typing boundary
hooksStore={hooksStore as any}
hooksStore={hooksStore}
allowSelectionWhenReadOnly
canvasReadOnly
interactionMode={InteractionMode.Subgraph}

View File

@ -1,4 +1,5 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { CommonNodeType, NodeDefault, NodeDefaultBase } from '@/app/components/workflow/types'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
@ -7,24 +8,46 @@ import { BlockEnum } from '@/app/components/workflow/types'
export const useAvailableNodesMetaData = () => {
const { t } = useTranslation()
const availableNodesMetaData = useMemo(() => WORKFLOW_COMMON_NODES.map((node) => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
return {
...node,
metaData: {
const availableNodesMetaData = useMemo<NodeDefaultBase[]>(() => {
const toNodeDefaultBase = (
node: NodeDefault<CommonNodeType>,
metaData: NodeDefaultBase['metaData'],
defaultValue: Partial<CommonNodeType>,
): NodeDefaultBase => {
return {
...node,
metaData,
defaultValue,
checkValid: (payload: CommonNodeType, translator, moreDataForCheckValid) => {
// normalize validator signature for shared metadata storage.
return node.checkValid(payload, translator, moreDataForCheckValid)
},
getOutputVars: node.getOutputVars
? (payload: CommonNodeType, allPluginInfoList, ragVariables, utils) => {
// normalize output var signature for shared metadata storage.
return node.getOutputVars!(payload, allPluginInfoList, ragVariables, utils)
}
: undefined,
}
}
return WORKFLOW_COMMON_NODES.map((node) => {
// normalize per-node defaults into a shared metadata shape.
const typedNode = node as NodeDefault<CommonNodeType>
const { metaData } = typedNode
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
return toNodeDefaultBase(typedNode, {
...metaData,
title,
description,
},
defaultValue: {
...node.defaultValue,
}, {
...typedNode.defaultValue,
type: metaData.type,
title,
},
}
}), [t])
})
})
}, [t])
const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => {
acc![node.metaData.type] = node

View File

@ -1,4 +1,5 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { CommonNodeType, NodeDefault, NodeDefaultBase } from '@/app/components/workflow/types'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -41,26 +42,48 @@ export const useAvailableNodesMetaData = () => {
),
], [isChatMode, startNodeMetaData])
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang
return {
...node,
metaData: {
const availableNodesMetaData = useMemo<NodeDefaultBase[]>(() => {
const toNodeDefaultBase = (
node: NodeDefault<CommonNodeType>,
metaData: NodeDefaultBase['metaData'],
defaultValue: Partial<CommonNodeType>,
): NodeDefaultBase => {
return {
...node,
metaData,
defaultValue,
checkValid: (payload: CommonNodeType, translator, moreDataForCheckValid) => {
// normalize validator signature for shared metadata storage.
return node.checkValid(payload, translator, moreDataForCheckValid)
},
getOutputVars: node.getOutputVars
? (payload: CommonNodeType, allPluginInfoList, ragVariables, utils) => {
// normalize output var signature for shared metadata storage.
return node.getOutputVars!(payload, allPluginInfoList, ragVariables, utils)
}
: undefined,
}
}
return mergedNodesMetaData.map((node) => {
// normalize per-node defaults into a shared metadata shape.
const typedNode = node as NodeDefault<CommonNodeType>
const { metaData } = typedNode
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang
return toNodeDefaultBase(typedNode, {
...metaData,
title,
description,
helpLinkUri: docLink(helpLinkPath),
},
defaultValue: {
...node.defaultValue,
}, {
...typedNode.defaultValue,
type: metaData.type,
title,
},
}
}), [mergedNodesMetaData, t, docLink])
})
})
}, [mergedNodesMetaData, t, docLink])
const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => {
acc![node.metaData.type] = node

View File

@ -1,4 +1,4 @@
import type { NodeDefault } from '../types'
import type { NodeDefaultBase } from '../types'
import type { BlockClassificationEnum } from './types'
import { groupBy } from 'es-toolkit/compat'
import {
@ -19,7 +19,7 @@ type BlocksProps = {
searchText: string
onSelect: (type: BlockEnum) => void
availableBlocksTypes?: BlockEnum[]
blocks?: NodeDefault[]
blocks?: NodeDefaultBase[]
}
const Blocks = ({
searchText,
@ -44,7 +44,7 @@ const Blocks = ({
},
defaultValue: {},
checkValid: () => ({ isValid: true }),
}) as NodeDefault)
}) as NodeDefaultBase)
const groups = useMemo(() => {
return BLOCK_CLASSIFICATIONS.reduce((acc, classification) => {

View File

@ -8,7 +8,7 @@ import type {
} from 'react'
import type {
CommonNodeType,
NodeDefault,
NodeDefaultBase,
OnSelectBlock,
ToolWithProvider,
} from '../types'
@ -49,7 +49,7 @@ export type NodeSelectorProps = {
asChild?: boolean
availableBlocksTypes?: BlockEnum[]
disabled?: boolean
blocks?: NodeDefault[]
blocks?: NodeDefaultBase[]
dataSources?: ToolWithProvider[]
noBlocks?: boolean
noTools?: boolean

View File

@ -1,7 +1,7 @@
import type { Dispatch, FC, SetStateAction } from 'react'
import type {
BlockEnum,
NodeDefault,
NodeDefaultBase,
OnSelectBlock,
ToolWithProvider,
} from '../types'
@ -28,7 +28,7 @@ export type TabsProps = {
onTagsChange: Dispatch<SetStateAction<string[]>>
onSelect: OnSelectBlock
availableBlocksTypes?: BlockEnum[]
blocks: NodeDefault[]
blocks: NodeDefaultBase[]
dataSources?: ToolWithProvider[]
tabs: Array<{
key: TabsEnum

View File

@ -2,7 +2,7 @@ import type { FileUpload } from '../../base/features/types'
import type {
BlockEnum,
Node,
NodeDefault,
NodeDefaultBase,
ToolWithProvider,
ValueSelector,
} from '@/app/components/workflow/types'
@ -16,14 +16,17 @@ import {
useStore as useZustandStore,
} from 'zustand'
import { createStore } from 'zustand/vanilla'
import { InteractionMode } from '@/app/components/workflow'
import { HooksStoreContext } from './provider'
export type AvailableNodesMetaData = {
nodes: NodeDefault[]
nodesMap?: Record<BlockEnum, NodeDefault<any>>
nodes: NodeDefaultBase[]
nodesMap: Record<BlockEnum, NodeDefaultBase>
}
export type HooksStore = ReturnType<typeof createHooksStore>
export type CommonHooksFnMap = {
interactionMode?: 'default' | 'subgraph'
interactionMode?: InteractionMode
doSyncWorkflowDraft: (
notRefreshWhenSyncError?: boolean,
callback?: {
@ -78,7 +81,7 @@ export type Shape = {
} & CommonHooksFnMap
export const createHooksStore = ({
interactionMode = 'default',
interactionMode = InteractionMode.Default,
doSyncWorkflowDraft = async () => noop(),
syncWorkflowDraftWhenPageClose = noop,
handleRefreshWorkflowDraft = noop,
@ -97,6 +100,7 @@ export const createHooksStore = ({
subGraphSelectableNodeTypes,
availableNodesMetaData = {
nodes: [],
nodesMap: {} as Record<BlockEnum, NodeDefaultBase>,
},
getWorkflowRunAndTraceUrl = () => ({
runUrl: '',

View File

@ -24,7 +24,7 @@ const useResizablePanels = () => {
const resolvedCodePanelHeight = useMemo(() => {
if (!maxCodePanelHeight)
return codePanelHeight
// Reason: Clamp the panel height so the output area always has space.
// Clamp the panel height so the output area always has space.
return Math.min(codePanelHeight, maxCodePanelHeight)
}, [codePanelHeight, maxCodePanelHeight])

View File

@ -4,7 +4,7 @@ import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/typ
import type { CodeNodeType, OutputVar } from '@/app/components/workflow/nodes/code/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type {
CommonNodeType,
NodeDefaultBase,
PromptItem,
PromptTemplateItem,
Node as WorkflowNode,
@ -40,7 +40,7 @@ export const getDefaultOutputKey = (outputs?: OutputVar): string => {
const keys = Object.keys(outputs)
if (keys.length === 0)
return ''
// Reason: 'result' is the conventional default output key for code nodes
// 'result' is the conventional default output key for code nodes
if (keys.includes('result'))
return 'result'
return keys[0]
@ -132,10 +132,7 @@ const buildPromptTemplateWithText = (
return [...promptTemplate, defaultUserPrompt] as PromptTemplateItem[]
}
type NodesMetaDataMap = Record<BlockEnum, {
defaultValue?: Partial<LLMNodeType | CodeNodeType>
checkValid?: (data: CommonNodeType, t: (key: string, options?: Record<string, unknown>) => string, moreData?: unknown) => { isValid: boolean, errorMessage?: string }
}>
type NodesMetaDataMap = Record<BlockEnum, Pick<NodeDefaultBase, 'defaultValue' | 'checkValid'>>
type ConfigsMap = {
flowId?: string

View File

@ -340,7 +340,24 @@ export type NodeOutPutVar = {
isFlat?: boolean
}
export type NodeDefault<T = {}> = {
// allow node default validators with narrower payload types to be stored in shared collections.
type CheckValidFn<T> = {
bivarianceHack: (payload: T, t: any, moreDataForCheckValid?: any) => { isValid: boolean, errorMessage?: string }
}['bivarianceHack']
// allow node output var generators with narrower payload types to be stored in shared collections.
type GetOutputVarsFn<T> = {
bivarianceHack: (
payload: T,
allPluginInfoList: Record<string, ToolWithProvider[]>,
ragVariables?: Var[],
utils?: {
schemaTypeDefinitions?: SchemaTypeDefinition[]
},
) => Var[]
}['bivarianceHack']
export type NodeDefaultBase = {
metaData: {
classification: BlockClassificationEnum
sort: number
@ -355,12 +372,16 @@ export type NodeDefault<T = {}> = {
isSingleton?: boolean
isTypeFixed?: boolean
}
defaultValue: Partial<T>
defaultValue: Partial<CommonNodeType>
defaultRunInputData?: Record<string, any>
checkValid: (payload: T, t: any, moreDataForCheckValid?: any) => { isValid: boolean, errorMessage?: string }
getOutputVars?: (payload: T, allPluginInfoList: Record<string, ToolWithProvider[]>, ragVariables?: Var[], utils?: {
schemaTypeDefinitions?: SchemaTypeDefinition[]
}) => Var[]
checkValid: CheckValidFn<CommonNodeType>
getOutputVars?: GetOutputVarsFn<CommonNodeType>
}
export type NodeDefault<T extends CommonNodeType = CommonNodeType> = Omit<NodeDefaultBase, 'defaultValue' | 'checkValid' | 'getOutputVars'> & {
defaultValue: Partial<T>
checkValid: CheckValidFn<T>
getOutputVars?: GetOutputVarsFn<T>
}
export type OnSelectBlock = (type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => void