mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 17:08:03 +08:00
feat: add Command node support
- Introduced Command node type in workflow with associated UI components and translations. - Enhanced SandboxLayer to manage sandbox attachment for Command nodes during execution. - Updated various components and constants to integrate Command node functionality across the workflow.
This commit is contained in:
@ -45,6 +45,7 @@ const DEFAULT_ICON_MAP: Record<BlockEnum, React.ComponentType<{ className: strin
|
||||
[BlockEnum.Start]: Home,
|
||||
[BlockEnum.LLM]: Llm,
|
||||
[BlockEnum.Code]: Code,
|
||||
[BlockEnum.Command]: Code,
|
||||
[BlockEnum.End]: End,
|
||||
[BlockEnum.IfElse]: IfElse,
|
||||
[BlockEnum.HttpRequest]: Http,
|
||||
@ -84,6 +85,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
||||
[BlockEnum.Start]: 'bg-util-colors-blue-brand-blue-brand-500',
|
||||
[BlockEnum.LLM]: 'bg-util-colors-indigo-indigo-500',
|
||||
[BlockEnum.Code]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.Command]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.End]: 'bg-util-colors-warning-warning-500',
|
||||
[BlockEnum.IfElse]: 'bg-util-colors-cyan-cyan-500',
|
||||
[BlockEnum.Iteration]: 'bg-util-colors-cyan-cyan-500',
|
||||
|
||||
@ -147,6 +147,11 @@ export const BLOCKS = [
|
||||
type: BlockEnum.ListFilter,
|
||||
title: 'List Filter',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Utilities,
|
||||
type: BlockEnum.Command,
|
||||
title: 'Command',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.Agent,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import agentDefault from '@/app/components/workflow/nodes/agent/default'
|
||||
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
|
||||
import codeDefault from '@/app/components/workflow/nodes/code/default'
|
||||
import commandDefault from '@/app/components/workflow/nodes/command/default'
|
||||
|
||||
import documentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
||||
|
||||
@ -33,6 +34,7 @@ export const WORKFLOW_COMMON_NODES = [
|
||||
loopStartDefault,
|
||||
loopEndDefault,
|
||||
codeDefault,
|
||||
commandDefault,
|
||||
templateTransformDefault,
|
||||
variableAggregatorDefault,
|
||||
documentExtractorDefault,
|
||||
|
||||
@ -42,6 +42,7 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.LLM]: useLLMSingleRunFormParams,
|
||||
[BlockEnum.KnowledgeRetrieval]: useKnowledgeRetrievalSingleRunFormParams,
|
||||
[BlockEnum.Code]: useCodeSingleRunFormParams,
|
||||
[BlockEnum.Command]: undefined,
|
||||
[BlockEnum.TemplateTransform]: useTemplateTransformSingleRunFormParams,
|
||||
[BlockEnum.QuestionClassifier]: useQuestionClassifierSingleRunFormParams,
|
||||
[BlockEnum.HttpRequest]: useHttpRequestSingleRunFormParams,
|
||||
@ -81,6 +82,7 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.LLM]: undefined,
|
||||
[BlockEnum.KnowledgeRetrieval]: undefined,
|
||||
[BlockEnum.Code]: undefined,
|
||||
[BlockEnum.Command]: undefined,
|
||||
[BlockEnum.TemplateTransform]: undefined,
|
||||
[BlockEnum.QuestionClassifier]: undefined,
|
||||
[BlockEnum.HttpRequest]: undefined,
|
||||
|
||||
35
web/app/components/workflow/nodes/command/default.ts
Normal file
35
web/app/components/workflow/nodes/command/default.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { CommandNodeType } from './types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
|
||||
const i18nPrefix = 'errorMsg'
|
||||
|
||||
const metaData = genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Utilities,
|
||||
sort: 2,
|
||||
type: BlockEnum.Command,
|
||||
})
|
||||
|
||||
const nodeDefault: NodeDefault<CommandNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {
|
||||
working_directory: '',
|
||||
command: '',
|
||||
},
|
||||
checkValid(payload: CommandNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { command } = payload
|
||||
|
||||
if (!errorMessages && !command)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.command`, { ns: 'workflow' }) })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
13
web/app/components/workflow/nodes/command/node.tsx
Normal file
13
web/app/components/workflow/nodes/command/node.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import type { FC } from 'react'
|
||||
import type { CommandNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
|
||||
const Node: FC<NodeProps<CommandNodeType>> = () => {
|
||||
return (
|
||||
// No summary content - same as Code node
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
71
web/app/components/workflow/nodes/command/panel.tsx
Normal file
71
web/app/components/workflow/nodes/command/panel.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import type { FC } from 'react'
|
||||
import type { CommandNodeType } from './types'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import useConfig from './use-config'
|
||||
|
||||
const i18nPrefix = 'nodes.command'
|
||||
|
||||
const Panel: FC<NodePanelProps<CommandNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleWorkingDirectoryChange,
|
||||
handleCommandChange,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="space-y-4 px-4 pb-4">
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.workingDirectory`, { ns: 'workflow' })}
|
||||
>
|
||||
<Input
|
||||
instanceId="command-working-directory"
|
||||
className="w-full rounded-lg border border-components-input-border-active bg-components-input-bg-normal px-3 py-2 text-sm text-components-input-text-filled"
|
||||
placeholder={t(`${i18nPrefix}.workingDirectoryPlaceholder`, { ns: 'workflow' }) || ''}
|
||||
value={inputs.working_directory || ''}
|
||||
onChange={handleWorkingDirectoryChange}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
/>
|
||||
</Field>
|
||||
<Split />
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.command`, { ns: 'workflow' })}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
instanceId="command-command"
|
||||
className="min-h-[120px] w-full rounded-lg border border-components-input-border-active bg-components-input-bg-normal px-3 py-2 font-mono text-sm text-components-input-text-filled"
|
||||
placeholder={t(`${i18nPrefix}.commandPlaceholder`, { ns: 'workflow' }) || ''}
|
||||
promptMinHeightClassName="min-h-[120px]"
|
||||
value={inputs.command || ''}
|
||||
onChange={handleCommandChange}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
6
web/app/components/workflow/nodes/command/types.ts
Normal file
6
web/app/components/workflow/nodes/command/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
|
||||
export type CommandNodeType = CommonNodeType & {
|
||||
working_directory: string
|
||||
command: string
|
||||
}
|
||||
33
web/app/components/workflow/nodes/command/use-config.ts
Normal file
33
web/app/components/workflow/nodes/command/use-config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { CommandNodeType } from './types'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
|
||||
const useConfig = (id: string, payload: CommandNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<CommandNodeType>(id, payload)
|
||||
|
||||
const handleWorkingDirectoryChange = useCallback((value: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.working_directory = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleCommandChange = useCallback((value: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.command = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleWorkingDirectoryChange,
|
||||
handleCommandChange,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@ -8,6 +8,8 @@ import AssignerNode from './assigner/node'
|
||||
import AssignerPanel from './assigner/panel'
|
||||
import CodeNode from './code/node'
|
||||
import CodePanel from './code/panel'
|
||||
import CommandNode from './command/node'
|
||||
import CommandPanel from './command/panel'
|
||||
import DataSourceNode from './data-source/node'
|
||||
import DataSourcePanel from './data-source/panel'
|
||||
import DocExtractorNode from './document-extractor/node'
|
||||
@ -75,6 +77,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.TriggerSchedule]: TriggerScheduleNode,
|
||||
[BlockEnum.TriggerWebhook]: TriggerWebhookNode,
|
||||
[BlockEnum.TriggerPlugin]: TriggerPluginNode,
|
||||
[BlockEnum.Command]: CommandNode,
|
||||
}
|
||||
|
||||
export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
@ -103,4 +106,5 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.TriggerSchedule]: TriggerSchedulePanel,
|
||||
[BlockEnum.TriggerWebhook]: TriggerWebhookPanel,
|
||||
[BlockEnum.TriggerPlugin]: TriggerPluginPanel,
|
||||
[BlockEnum.Command]: CommandPanel,
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ export enum BlockEnum {
|
||||
TriggerSchedule = 'trigger-schedule',
|
||||
TriggerWebhook = 'trigger-webhook',
|
||||
TriggerPlugin = 'trigger-plugin',
|
||||
Command = 'command',
|
||||
}
|
||||
|
||||
export enum ControlMode {
|
||||
|
||||
@ -20,6 +20,7 @@ export const canRunBySingle = (nodeType: BlockEnum, isChildNode: boolean) => {
|
||||
return nodeType === BlockEnum.LLM
|
||||
|| nodeType === BlockEnum.KnowledgeRetrieval
|
||||
|| nodeType === BlockEnum.Code
|
||||
|| nodeType === BlockEnum.Command
|
||||
|| nodeType === BlockEnum.TemplateTransform
|
||||
|| nodeType === BlockEnum.QuestionClassifier
|
||||
|| nodeType === BlockEnum.HttpRequest
|
||||
|
||||
Reference in New Issue
Block a user