This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiFileCopyLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { debounce } from 'lodash-es'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
content: string
}
const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
const CopyFeedbackNew = ({ content }: Props) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
const onClickCopy = debounce(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
return (
<div className='inline-flex w-full pb-0.5' onClick={e => e.stopPropagation()} onMouseLeave={onMouseLeave}>
<Tooltip
popupContent={
(isCopied
? t(`${prefixEmbedded}.copied`)
: t(`${prefixEmbedded}.copy`)) || ''
}
>
<div
className='group/copy flex w-full items-center gap-0.5 '
onClick={onClickCopy}
>
<div
className='system-2xs-regular w-0 grow cursor-pointer truncate text-text-quaternary group-hover:text-text-tertiary'
>{content}</div>
<RiFileCopyLine className='h-3 w-3 shrink-0 text-text-tertiary opacity-0 group-hover/copy:opacity-100' />
</div>
</Tooltip>
</div>
)
}
export default CopyFeedbackNew

View File

@@ -0,0 +1,258 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { produce } from 'immer'
import { useTranslation } from 'react-i18next'
import type { ToolVarInputs } from '../types'
import { VarType as VarKindType } from '../types'
import cn from '@/utils/classnames'
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import { VarType } from '@/app/components/workflow/types'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import { noop } from 'lodash-es'
import type { Tool } from '@/app/components/tools/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema[]
value: ToolVarInputs
onChange: (value: ToolVarInputs) => void
onOpen?: (index: number) => void
isSupportConstantValue?: boolean
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
}
const InputVarList: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
onOpen = noop,
isSupportConstantValue,
filterVar,
currentTool,
currentProvider,
}) => {
const language = useLanguage()
const { t } = useTranslation()
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
},
})
const paramType = (type: string) => {
if (type === FormTypeEnum.textNumber)
return 'Number'
else if (type === FormTypeEnum.file || type === FormTypeEnum.files)
return 'Files'
else if (type === FormTypeEnum.appSelector)
return 'AppSelector'
else if (type === FormTypeEnum.modelSelector)
return 'ModelSelector'
else if (type === FormTypeEnum.toolSelector)
return 'ToolSelector'
else if (type === FormTypeEnum.dynamicSelect || type === FormTypeEnum.select)
return 'Select'
else
return 'String'
}
const handleNotMixedTypeChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string, varKindType: VarKindType) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable]
if (target) {
target.type = varKindType
target.value = varValue
}
else {
draft[variable] = {
type: varKindType,
value: varValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleMixedTypeChange = useCallback((variable: string) => {
return (itemValue: string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable]
if (target) {
target.value = itemValue
}
else {
draft[variable] = {
type: VarKindType.mixed,
value: itemValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleFileChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable] = {
type: VarKindType.variable,
value: varValue,
}
})
onChange(newValue)
}
}, [value, onChange])
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
inputs: Record<string, any>
files?: any[]
}) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable] = app as any
})
onChange(newValue)
}
}, [onChange, value])
const handleModelChange = useCallback((variable: string) => {
return (model: any) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable] = {
...draft[variable],
...model,
} as any
})
onChange(newValue)
}
}, [onChange, value])
const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
const handleInputFocus = useCallback((variable: string) => {
return (value: boolean) => {
setInputsIsFocus((prev) => {
return {
...prev,
[variable]: value,
}
})
}
}, [])
const handleOpen = useCallback((index: number) => {
return () => onOpen(index)
}, [onOpen])
return (
<div className='space-y-3'>
{
schema.map((schema, index) => {
const {
variable,
label,
type,
required,
tooltip,
scope,
} = schema
const varInput = value[variable]
const isNumber = type === FormTypeEnum.textNumber
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
// const isToolSelector = type === FormTypeEnum.toolSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
return (
<div key={variable} className='space-y-1'>
<div className='flex items-center space-x-2 leading-[18px]'>
<span className='code-sm-semibold text-text-secondary'>{label[language] || label.en_US}</span>
<span className='system-xs-regular text-text-tertiary'>{paramType(type)}</span>
{required && <span className='system-xs-regular text-util-colors-orange-dark-orange-dark-600'>Required</span>}
</div>
{isString && (
<Input
className={cn(inputsIsFocus[variable] ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'rounded-lg border px-3 py-[6px]')}
value={varInput?.value as string || ''}
onChange={handleMixedTypeChange(variable)}
readOnly={readOnly}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
onFocusChange={handleInputFocus(variable)}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)}
{(isNumber || isSelect) && (
<VarReferencePicker
readonly={readOnly}
isShowNodeName
nodeId={nodeId}
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)}
onOpen={handleOpen(index)}
defaultVarKindType={varInput?.type || ((isNumber || isDynamicSelect) ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue={isSupportConstantValue}
filterVar={isNumber ? filterVar : undefined}
availableVars={isSelect ? availableVars : undefined}
schema={schema}
currentTool={currentTool}
currentProvider={currentProvider}
/>
)}
{isFile && (
<VarReferencePicker
readonly={readOnly}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleFileChange(variable)}
onOpen={handleOpen(index)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
/>
)}
{isAppSelector && (
<AppSelector
disabled={readOnly}
scope={scope || 'all'}
value={varInput as any}
onSelect={handleAppChange(variable)}
/>
)}
{isModelSelector && (
<ModelParameterModal
popupClassName='!w-[387px]'
isAdvancedMode
isInWorkflow
value={varInput as any}
setModel={handleModelChange(variable)}
readonly={readOnly}
scope={scope}
/>
)}
{tooltip && <div className='body-xs-regular text-text-tertiary'>{tooltip[language] || tooltip.en_US}</div>}
</div>
)
})
}
</div>
)
}
export default React.memo(InputVarList)

View File

@@ -0,0 +1,74 @@
import {
memo,
} from 'react'
import { useTranslation } from 'react-i18next'
import PromptEditor from '@/app/components/base/prompt-editor'
import Placeholder from './placeholder'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { useStore } from '@/app/components/workflow/store'
type MixedVariableTextInputProps = {
readOnly?: boolean
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
value?: string
onChange?: (text: string) => void
showManageInputField?: boolean
onManageInputField?: () => void
disableVariableInsertion?: boolean
}
const MixedVariableTextInput = ({
readOnly = false,
nodesOutputVars,
availableNodes = [],
value = '',
onChange,
showManageInputField,
onManageInputField,
disableVariableInsertion = false,
}: MixedVariableTextInputProps) => {
const { t } = useTranslation()
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
return (
<PromptEditor
key={controlPromptEditorRerenderKey}
wrapperClassName={cn(
'min-h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
)}
className='caret:text-text-accent'
editable={!readOnly}
value={value}
workflowVariableBlock={{
show: !disableVariableInsertion,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
showManageInputField,
onManageInputField,
}}
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} />}
onChange={onChange}
/>
)
}
export default memo(MixedVariableTextInput)

View File

@@ -0,0 +1,60 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { FOCUS_COMMAND } from 'lexical'
import { $insertNodes } from 'lexical'
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
import Badge from '@/app/components/base/badge'
type PlaceholderProps = {
disableVariableInsertion?: boolean
}
const Placeholder = ({ disableVariableInsertion = false }: PlaceholderProps) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const handleInsert = useCallback((text: string) => {
editor.update(() => {
const textNode = new CustomTextNode(text)
$insertNodes([textNode])
})
editor.dispatchCommand(FOCUS_COMMAND, undefined as any)
}, [editor])
return (
<div
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
onClick={(e) => {
e.stopPropagation()
handleInsert('')
}}
>
<div className='flex grow items-center'>
{t('workflow.nodes.tool.insertPlaceholder1')}
{(!disableVariableInsertion) && (
<>
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
<div
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
onMouseDown={((e) => {
e.preventDefault()
e.stopPropagation()
handleInsert('/')
})}
>
{t('workflow.nodes.tool.insertPlaceholder2')}
</div>
</>
)}
</div>
<Badge
className='shrink-0'
text='String'
uppercase={false}
/>
</div>
)
}
export default Placeholder

View File

@@ -0,0 +1,61 @@
'use client'
import type { FC } from 'react'
import type { ToolVarInputs } from '../../types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import ToolFormItem from './item'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { Tool } from '@/app/components/tools/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema[]
value: ToolVarInputs
onChange: (value: ToolVarInputs) => void
onOpen?: (index: number) => void
inPanel?: boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
showManageInputField?: boolean
onManageInputField?: () => void
extraParams?: Record<string, any>
}
const ToolForm: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentTool,
currentProvider,
showManageInputField,
onManageInputField,
extraParams,
}) => {
return (
<div className='space-y-1'>
{
schema.map((schema, index) => (
<ToolFormItem
key={index}
readOnly={readOnly}
nodeId={nodeId}
schema={schema}
value={value}
onChange={onChange}
inPanel={inPanel}
currentTool={currentTool}
currentProvider={currentProvider}
showManageInputField={showManageInputField}
onManageInputField={onManageInputField}
extraParams={extraParams}
providerType='tool'
/>
))
}
</div>
)
}
export default ToolForm

View File

@@ -0,0 +1,117 @@
'use client'
import type { FC } from 'react'
import {
RiBracesLine,
} from '@remixicon/react'
import type { ToolVarInputs } from '../../types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item'
import { useBoolean } from 'ahooks'
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { Tool } from '@/app/components/tools/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema
value: ToolVarInputs
onChange: (value: ToolVarInputs) => void
inPanel?: boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
showManageInputField?: boolean
onManageInputField?: () => void
extraParams?: Record<string, any>
providerType?: 'tool' | 'trigger'
}
const ToolFormItem: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentTool,
currentProvider,
showManageInputField,
onManageInputField,
extraParams,
providerType = 'tool',
}) => {
const language = useLanguage()
const { name, label, type, required, tooltip, input_schema } = schema
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
const [isShowSchema, {
setTrue: showSchema,
setFalse: hideSchema,
}] = useBoolean(false)
return (
<div className='space-y-0.5 py-1'>
<div>
<div className='flex h-6 items-center'>
<div className='system-sm-medium text-text-secondary'>{label[language] || label.en_US}</div>
{required && (
<div className='system-xs-regular ml-1 text-text-destructive-secondary'>*</div>
)}
{!showDescription && tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
triggerClassName='ml-1 w-4 h-4'
asChild={false}
/>
)}
{showSchemaButton && (
<>
<div className='system-xs-regular ml-1 mr-0.5 text-text-quaternary'>·</div>
<Button
variant='ghost'
size='small'
onClick={showSchema}
className='system-xs-regular px-1 text-text-tertiary'
>
<RiBracesLine className='mr-1 size-3.5' />
<span>JSON Schema</span>
</Button>
</>
)}
</div>
{showDescription && tooltip && (
<div className='body-xs-regular pb-0.5 text-text-tertiary'>{tooltip[language] || tooltip.en_US}</div>
)}
</div>
<FormInputItem
readOnly={readOnly}
nodeId={nodeId}
schema={schema}
value={value}
onChange={onChange}
inPanel={inPanel}
currentTool={currentTool}
currentProvider={currentProvider}
showManageInputField={showManageInputField}
onManageInputField={onManageInputField}
extraParams={extraParams}
providerType={providerType}
/>
{isShowSchema && (
<SchemaModal
isShow
onClose={hideSchema}
rootName={name}
schema={input_schema!}
/>
)}
</div>
)
}
export default ToolFormItem

View File

@@ -0,0 +1,130 @@
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import type { NodeDefault, ToolWithProvider } from '../../types'
import type { ToolNodeType } from './types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { TOOL_OUTPUT_STRUCT } from '../../constants'
import { CollectionType } from '@/app/components/tools/types'
import { canFindTool } from '@/utils'
import { getMatchedSchemaType } from '../_base/components/variable/use-match-schema-type'
const i18nPrefix = 'workflow.errorMsg'
const metaData = genNodeMetaData({
sort: -1,
type: BlockEnum.Tool,
helpLinkUri: 'tools',
})
const nodeDefault: NodeDefault<ToolNodeType> = {
metaData,
defaultValue: {
tool_parameters: {},
tool_configurations: {},
tool_node_version: '2',
},
checkValid(payload: ToolNodeType, t: any, moreDataForCheckValid: any) {
const { toolInputsSchema, toolSettingSchema, language, notAuthed } = moreDataForCheckValid
let errorMessages = ''
if (notAuthed)
errorMessages = t(`${i18nPrefix}.authRequired`)
if (!errorMessages) {
toolInputsSchema.filter((field: any) => {
return field.required
}).forEach((field: any) => {
const targetVar = payload.tool_parameters[field.variable]
if (!targetVar) {
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
return
}
const { type: variable_type, value } = targetVar
if (variable_type === VarKindType.variable) {
if (!errorMessages && (!value || value.length === 0))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
}
else {
if (!errorMessages && (value === undefined || value === null || value === ''))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
}
})
}
if (!errorMessages) {
toolSettingSchema.filter((field: any) => {
return field.required
}).forEach((field: any) => {
const value = payload.tool_configurations[field.variable]
if (!errorMessages && (value === undefined || value === null || value === ''))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] })
if (!errorMessages && typeof value === 'object' && !!value.type && (value.value === undefined || value.value === null || value.value === '' || (Array.isArray(value.value) && value.value.length === 0)))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] })
})
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
getOutputVars(payload: ToolNodeType, allPluginInfoList: Record<string, ToolWithProvider[]>, _ragVars: any, { schemaTypeDefinitions } = { schemaTypeDefinitions: [] }) {
const { provider_id, provider_type } = payload
let currentTools: ToolWithProvider[] = []
switch (provider_type) {
case CollectionType.builtIn:
currentTools = allPluginInfoList.buildInTools ?? []
break
case CollectionType.custom:
currentTools = allPluginInfoList.customTools ?? []
break
case CollectionType.workflow:
currentTools = allPluginInfoList.workflowTools ?? []
break
case CollectionType.mcp:
currentTools = allPluginInfoList.mcpTools ?? []
break
default:
currentTools = []
}
const currCollection = currentTools.find(item => canFindTool(item.id, provider_id))
const currTool = currCollection?.tools.find(tool => tool.name === payload.tool_name)
const output_schema = currTool?.output_schema
let res: any[] = []
if (!output_schema || !output_schema.properties) {
res = TOOL_OUTPUT_STRUCT
}
else {
const outputSchema: any[] = []
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
const dataType = output.type
const schemaType = getMatchedSchemaType(output, schemaTypeDefinitions)
let type = dataType === 'array'
? `Array[${output.items?.type ? output.items.type.slice(0, 1).toLocaleLowerCase() + output.items.type.slice(1) : 'Unknown'}]`
: `${output.type ? output.type.slice(0, 1).toLocaleLowerCase() + output.type.slice(1) : 'Unknown'}`
if (type === VarType.object && schemaType === 'file')
type = VarType.file
outputSchema.push({
variable: outputKey,
type,
description: output.description,
schemaType,
children: output.type === 'object' ? {
schema: {
type: 'object',
properties: output.properties,
},
} : undefined,
})
})
res = [
...TOOL_OUTPUT_STRUCT,
...outputSchema,
]
}
return res
},
}
export default nodeDefault

View File

@@ -0,0 +1,92 @@
import type { FC } from 'react'
import React, { useEffect } from 'react'
import type { NodeProps } from '@/app/components/workflow/types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks/use-node-data-update'
import type { ToolNodeType } from './types'
const Node: FC<NodeProps<ToolNodeType>> = ({
id,
data,
}) => {
const { tool_configurations, paramSchemas } = data
const toolConfigs = Object.keys(tool_configurations || {})
const {
isChecking,
isMissing,
uniqueIdentifier,
canInstall,
onInstallSuccess,
shouldDim,
} = useNodePluginInstallation(data)
const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier
const { handleNodeDataUpdate } = useNodeDataUpdate()
const shouldLock = !isChecking && isMissing && canInstall && Boolean(uniqueIdentifier)
useEffect(() => {
if (data._pluginInstallLocked === shouldLock && data._dimmed === shouldDim)
return
handleNodeDataUpdate({
id,
data: {
_pluginInstallLocked: shouldLock,
_dimmed: shouldDim,
},
})
}, [data._pluginInstallLocked, data._dimmed, handleNodeDataUpdate, id, shouldDim, shouldLock])
const hasConfigs = toolConfigs.length > 0
if (!showInstallButton && !hasConfigs)
return null
return (
<div className='relative mb-1 px-3 py-1'>
{showInstallButton && (
<div className='pointer-events-auto absolute right-3 top-[-32px] z-40'>
<InstallPluginButton
size='small'
className='!font-medium !text-text-accent'
extraIdentifiers={[
data.plugin_id,
data.provider_id,
data.provider_name,
].filter(Boolean) as string[]}
uniqueIdentifier={uniqueIdentifier!}
onSuccess={onInstallSuccess}
/>
</div>
)}
{hasConfigs && (
<div className='space-y-0.5' aria-disabled={shouldDim}>
{toolConfigs.map((key, index) => (
<div key={index} className='flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary'>
<div title={key} className='max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary'>
{key}
</div>
{typeof tool_configurations[key].value === 'string' && (
<div title={tool_configurations[key].value} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
{paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value}
</div>
)}
{typeof tool_configurations[key].value === 'number' && (
<div title={Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
{Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value}
</div>
)}
{typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && (
<div title={tool_configurations[key].model} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
{tool_configurations[key].model}
</div>
)}
</div>
))}
</div>
)}
</div>
)
}
export default React.memo(Node)

View File

@@ -0,0 +1,150 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Split from '../_base/components/split'
import type { ToolNodeType } from './types'
import useConfig from './use-config'
import ToolForm from './components/tool-form'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import type { NodePanelProps } from '@/app/components/workflow/types'
import Loading from '@/app/components/base/loading'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { useStore } from '@/app/components/workflow/store'
import { wrapStructuredVarItem } from '@/app/components/workflow/utils/tool'
import useMatchSchemaType, { getMatchedSchemaType } from '../_base/components/variable/use-match-schema-type'
const i18nPrefix = 'workflow.nodes.tool'
const Panel: FC<NodePanelProps<ToolNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
toolInputVarSchema,
setInputVar,
toolSettingSchema,
toolSettingValue,
setToolSettingValue,
currCollection,
isShowAuthBtn,
isLoading,
outputSchema,
hasObjectOutput,
currTool,
} = useConfig(id, data)
const [collapsed, setCollapsed] = React.useState(false)
const pipelineId = useStore(s => s.pipelineId)
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
const { schemaTypeDefinitions } = useMatchSchemaType()
if (isLoading) {
return (
<div className='flex h-[200px] items-center justify-center'>
<Loading />
</div>
)
}
return (
<div className='pt-2'>
{!isShowAuthBtn && (
<div className='relative'>
{toolInputVarSchema.length > 0 && (
<Field
className='px-4'
title={t(`${i18nPrefix}.inputVars`)}
>
<ToolForm
readOnly={readOnly}
nodeId={id}
schema={toolInputVarSchema as any}
value={inputs.tool_parameters}
onChange={setInputVar}
currentProvider={currCollection}
currentTool={currTool}
showManageInputField={!!pipelineId}
onManageInputField={() => setShowInputFieldPanel?.(true)}
/>
</Field>
)}
{toolInputVarSchema.length > 0 && toolSettingSchema.length > 0 && (
<Split className='mt-1' />
)}
{toolSettingSchema.length > 0 && (
<>
<OutputVars
title={t(`${i18nPrefix}.settings`)}
collapsed={collapsed}
onCollapse={setCollapsed}
>
<ToolForm
readOnly={readOnly}
nodeId={id}
schema={toolSettingSchema as any}
value={toolSettingValue}
onChange={setToolSettingValue}
/>
</OutputVars>
<Split />
</>
)}
</div>
)}
<div>
<OutputVars>
<>
<VarItem
name='text'
type='string'
description={t(`${i18nPrefix}.outputVars.text`)}
isIndent={hasObjectOutput}
/>
<VarItem
name='files'
type='array[file]'
description={t(`${i18nPrefix}.outputVars.files.title`)}
isIndent={hasObjectOutput}
/>
<VarItem
name='json'
type='array[object]'
description={t(`${i18nPrefix}.outputVars.json`)}
isIndent={hasObjectOutput}
/>
{outputSchema.map((outputItem) => {
const schemaType = getMatchedSchemaType(outputItem.value, schemaTypeDefinitions)
return (
<div key={outputItem.name}>
{outputItem.value?.type === 'object' ? (
<StructureOutputItem
rootClassName='code-sm-semibold text-text-secondary'
payload={wrapStructuredVarItem(outputItem, schemaType)}
/>
) : (
<VarItem
name={outputItem.name}
// eslint-disable-next-line sonarjs/no-nested-template-literals
type={`${outputItem.type.toLocaleLowerCase()}${schemaType ? ` (${schemaType})` : ''}`}
description={outputItem.description}
isIndent={hasObjectOutput}
/>
)}
</div>
)
})}
</>
</OutputVars>
</div>
</div>
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,26 @@
import type { Collection, CollectionType } from '@/app/components/tools/types'
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { ResourceVarInputs } from '../_base/types'
// Use base types directly
export { VarKindType as VarType } from '../_base/types'
export type ToolVarInputs = ResourceVarInputs
export type ToolNodeType = CommonNodeType & {
provider_id: string
provider_type: CollectionType
provider_name: string
tool_name: string
tool_label: string
tool_parameters: ToolVarInputs
tool_configurations: Record<string, any>
paramSchemas?: Record<string, any>[]
version?: string
tool_node_version?: string
tool_description?: string
is_team_authorization?: boolean
params?: Record<string, any>
plugin_id?: string
provider_icon?: Collection['icon']
plugin_unique_identifier?: string
}

View File

@@ -0,0 +1,298 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { produce } from 'immer'
import { useBoolean } from 'ahooks'
import { useWorkflowStore } from '../../store'
import type { ToolNodeType, ToolVarInputs } from './types'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { CollectionType } from '@/app/components/tools/types'
import { updateBuiltInToolCredential } from '@/service/tools'
import {
getConfiguredValue,
toolParametersToFormSchemas,
} from '@/app/components/tools/utils/to-form-schema'
import Toast from '@/app/components/base/toast'
import type { InputVar } from '@/app/components/workflow/types'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { canFindTool } from '@/utils'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllMCPTools,
useAllWorkflowTools,
useInvalidToolsByType,
} from '@/service/use-tools'
const useConfig = (id: string, payload: ToolNodeType) => {
const workflowStore = useWorkflowStore()
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { t } = useTranslation()
const language = useLanguage()
const { inputs, setInputs: doSetInputs } = useNodeCrud<ToolNodeType>(
id,
payload,
)
/*
* tool_configurations: tool setting, not dynamic setting (form type = form)
* tool_parameters: tool dynamic setting(form type = llm)
*/
const {
provider_id,
provider_type,
tool_name,
tool_configurations,
tool_parameters,
} = inputs
const isBuiltIn = provider_type === CollectionType.builtIn
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const currentTools = useMemo(() => {
switch (provider_type) {
case CollectionType.builtIn:
return buildInTools || []
case CollectionType.custom:
return customTools || []
case CollectionType.workflow:
return workflowTools || []
case CollectionType.mcp:
return mcpTools || []
default:
return []
}
}, [buildInTools, customTools, mcpTools, provider_type, workflowTools])
const currCollection = useMemo(() => {
return currentTools.find(item => canFindTool(item.id, provider_id))
}, [currentTools, provider_id])
// Auth
const needAuth = !!currCollection?.allow_delete
const isAuthed = !!currCollection?.is_team_authorization
const isShowAuthBtn = isBuiltIn && needAuth && !isAuthed
const [
showSetAuth,
{ setTrue: showSetAuthModal, setFalse: hideSetAuthModal },
] = useBoolean(false)
const invalidToolsByType = useInvalidToolsByType(provider_type)
const handleSaveAuth = useCallback(
async (value: any) => {
await updateBuiltInToolCredential(currCollection?.name as string, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
invalidToolsByType()
hideSetAuthModal()
},
[
currCollection?.name,
hideSetAuthModal,
t,
invalidToolsByType,
provider_type,
],
)
const currTool = useMemo(() => {
return currCollection?.tools.find(tool => tool.name === tool_name)
}, [currCollection, tool_name])
const formSchemas = useMemo(() => {
return currTool ? toolParametersToFormSchemas(currTool.parameters) : []
}, [currTool])
const toolInputVarSchema = useMemo(() => {
return formSchemas.filter((item: any) => item.form === 'llm')
}, [formSchemas])
// use setting
const toolSettingSchema = useMemo(() => {
return formSchemas.filter((item: any) => item.form !== 'llm')
}, [formSchemas])
const hasShouldTransferTypeSettingInput = toolSettingSchema.some(
item => item.type === 'boolean' || item.type === 'number-input',
)
const setInputs = useCallback(
(value: ToolNodeType) => {
if (!hasShouldTransferTypeSettingInput) {
doSetInputs(value)
return
}
const newInputs = produce(value, (draft) => {
const newConfig = { ...draft.tool_configurations }
Object.keys(draft.tool_configurations).forEach((key) => {
const schema = formSchemas.find(item => item.variable === key)
const value = newConfig[key]
if (schema?.type === 'boolean') {
if (typeof value === 'string')
newConfig[key] = value === 'true' || value === '1'
if (typeof value === 'number') newConfig[key] = value === 1
}
if (schema?.type === 'number-input') {
if (typeof value === 'string' && value !== '')
newConfig[key] = Number.parseFloat(value)
}
})
draft.tool_configurations = newConfig
})
doSetInputs(newInputs)
},
[doSetInputs, formSchemas, hasShouldTransferTypeSettingInput],
)
const [notSetDefaultValue, setNotSetDefaultValue] = useState(false)
const toolSettingValue = useMemo(() => {
if (notSetDefaultValue) return tool_configurations
return getConfiguredValue(tool_configurations, toolSettingSchema)
}, [notSetDefaultValue, toolSettingSchema, tool_configurations])
const setToolSettingValue = useCallback(
(value: Record<string, any>) => {
setNotSetDefaultValue(true)
setInputs({
...inputs,
tool_configurations: value,
})
},
[inputs, setInputs],
)
const formattingParameters = () => {
const inputsWithDefaultValue = produce(inputs, (draft) => {
if (
!draft.tool_configurations
|| Object.keys(draft.tool_configurations).length === 0
) {
draft.tool_configurations = getConfiguredValue(
tool_configurations,
toolSettingSchema,
)
}
if (
!draft.tool_parameters
|| Object.keys(draft.tool_parameters).length === 0
) {
draft.tool_parameters = getConfiguredValue(
tool_parameters,
toolInputVarSchema,
)
}
})
return inputsWithDefaultValue
}
useEffect(() => {
if (!currTool) return
const inputsWithDefaultValue = formattingParameters()
const { setControlPromptEditorRerenderKey } = workflowStore.getState()
setInputs(inputsWithDefaultValue)
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
}, [currTool])
// setting when call
const setInputVar = useCallback(
(value: ToolVarInputs) => {
setInputs({
...inputs,
tool_parameters: value,
})
},
[inputs, setInputs],
)
const isLoading = currTool && (isBuiltIn ? !currCollection : false)
const getMoreDataForCheckValid = () => {
return {
toolInputsSchema: (() => {
const formInputs: InputVar[] = []
toolInputVarSchema.forEach((item: any) => {
formInputs.push({
label: item.label[language] || item.label.en_US,
variable: item.variable,
type: item.type,
required: item.required,
})
})
return formInputs
})(),
notAuthed: isShowAuthBtn,
toolSettingSchema,
language,
}
}
const outputSchema = useMemo(() => {
const res: any[] = []
const output_schema = currTool?.output_schema
if (!output_schema || !output_schema.properties) return res
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
const type = output.type
if (type === 'object') {
res.push({
name: outputKey,
value: output,
})
}
else {
res.push({
name: outputKey,
type:
output.type === 'array'
? `Array[${output.items?.type
? output.items.type.slice(0, 1).toLocaleUpperCase()
+ output.items.type.slice(1)
: 'Unknown'
}]`
: `${output.type
? output.type.slice(0, 1).toLocaleUpperCase()
+ output.type.slice(1)
: 'Unknown'
}`,
description: output.description,
})
}
})
return res
}, [currTool])
const hasObjectOutput = useMemo(() => {
const output_schema = currTool?.output_schema
if (!output_schema || !output_schema.properties) return false
const properties = output_schema.properties
return Object.keys(properties).some(
key => properties[key].type === 'object',
)
}, [currTool])
return {
readOnly,
inputs,
currTool,
toolSettingSchema,
toolSettingValue,
setToolSettingValue,
toolInputVarSchema,
setInputVar,
currCollection,
isShowAuthBtn,
showSetAuth,
showSetAuthModal,
hideSetAuthModal,
handleSaveAuth,
isLoading,
outputSchema,
hasObjectOutput,
getMoreDataForCheckValid,
}
}
export default useConfig

View File

@@ -0,0 +1,20 @@
import type { ToolNodeType } from './types'
import useConfig from './use-config'
type Params = {
id: string
payload: ToolNodeType,
}
const useGetDataForCheckMore = ({
id,
payload,
}: Params) => {
const { getMoreDataForCheckValid } = useConfig(id, payload)
return {
getData: getMoreDataForCheckValid,
}
}
export default useGetDataForCheckMore

View File

@@ -0,0 +1,108 @@
import type { RefObject } from 'react'
import type { InputVar, Variable } from '@/app/components/workflow/types'
import { useCallback, useMemo, useState } from 'react'
import useNodeCrud from '../_base/hooks/use-node-crud'
import { type ToolNodeType, VarType } from './types'
import type { ValueSelector } from '@/app/components/workflow/types'
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
import { produce } from 'immer'
import type { NodeTracing } from '@/types/workflow'
import { useTranslation } from 'react-i18next'
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
import { useToolIcon } from '../../hooks'
type Params = {
id: string,
payload: ToolNodeType,
runInputData: Record<string, any>
runInputDataRef: RefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
runResult: NodeTracing
}
const useSingleRunFormParams = ({
id,
payload,
getInputVars,
setRunInputData,
runResult,
}: Params) => {
const { t } = useTranslation()
const { inputs } = useNodeCrud<ToolNodeType>(id, payload)
const hadVarParams = Object.keys(inputs.tool_parameters)
.filter(key => inputs.tool_parameters[key].type !== VarType.constant)
.map(k => inputs.tool_parameters[k])
const hadVarSettings = Object.keys(inputs.tool_configurations)
.filter(key => typeof inputs.tool_configurations[key] === 'object' && inputs.tool_configurations[key].type && inputs.tool_configurations[key].type !== VarType.constant)
.map(k => inputs.tool_configurations[k])
const varInputs = getInputVars([...hadVarParams, ...hadVarSettings].map((p) => {
if (p.type === VarType.variable) {
// handle the old wrong value not crash the page
if (!(p.value as any).join)
return `{{#${p.value}#}}`
return `{{#${(p.value as ValueSelector).join('.')}#}}`
}
return p.value as string
}))
const [inputVarValues, doSetInputVarValues] = useState<Record<string, any>>({})
const setInputVarValues = useCallback((value: Record<string, any>) => {
doSetInputVarValues(value)
setRunInputData(value)
}, [setRunInputData])
const inputVarValuesWithConstantValue = useCallback(() => {
const res = produce(inputVarValues, (draft) => {
Object.keys(inputs.tool_parameters).forEach((key: string) => {
const { type, value } = inputs.tool_parameters[key]
if (type === VarType.constant && (value === undefined || value === null)) {
if(!draft.tool_parameters || !draft.tool_parameters[key])
return
draft[key] = value
}
})
})
return res
}, [inputs.tool_parameters, inputVarValues])
const forms = useMemo(() => {
const forms: FormProps[] = [{
inputs: varInputs,
values: inputVarValuesWithConstantValue(),
onChange: setInputVarValues,
}]
return forms
}, [inputVarValuesWithConstantValue, setInputVarValues, varInputs])
const nodeInfo = useMemo(() => {
if (!runResult)
return null
return formatToTracingNodeList([runResult], t)[0]
}, [runResult, t])
const toolIcon = useToolIcon(payload)
const getDependentVars = () => {
return varInputs.map((item) => {
// Guard against null/undefined variable to prevent app crash
if (!item.variable || typeof item.variable !== 'string')
return []
return item.variable.slice(1, -1).split('.')
}).filter(arr => arr.length > 0)
}
return {
forms,
nodeInfo,
toolIcon,
getDependentVars,
}
}
export default useSingleRunFormParams

View File

@@ -0,0 +1,5 @@
import type { ToolNodeType } from './types'
export const checkNodeValid = (_payload: ToolNodeType) => {
return true
}