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:
Harry
2026-01-06 19:30:38 +08:00
parent caabca3f02
commit 1c7c475c43
22 changed files with 672 additions and 0 deletions

View 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

View 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)

View 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)

View File

@ -0,0 +1,6 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
export type CommandNodeType = CommonNodeType & {
working_directory: string
command: string
}

View 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