Merge main HEAD (segment 5) into sandboxed-agent-rebase

Resolve 83 conflicts: 10 backend, 62 frontend, 11 config/lock files.
Preserve sandbox/agent/collaboration features while adopting main's
UI refactorings (Dialog/AlertDialog/Popover), model provider updates,
and enterprise features.

Made-with: Cursor
This commit is contained in:
Novice
2026-03-23 14:20:06 +08:00
1671 changed files with 124822 additions and 22302 deletions

View File

@ -0,0 +1,194 @@
import type { ToolNodeType } from '../../types'
import { renderHook } from '@testing-library/react'
import { CollectionType } from '@/app/components/tools/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { VarType } from '../../types'
import useConfig from '../use-config'
const mockSetInputs = vi.hoisted(() => vi.fn())
const mockSetControlPromptEditorRerenderKey = vi.hoisted(() => vi.fn())
const mockUseCurrentToolCollection = vi.hoisted(() => vi.fn())
const mockGetConfiguredValue = vi.hoisted(() => vi.fn())
const mockToolParametersToFormSchemas = vi.hoisted(() => vi.fn())
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useLanguage: () => 'en_US',
}))
vi.mock('@/app/components/workflow/hooks', () => ({
useNodesReadOnly: () => ({ nodesReadOnly: false }),
}))
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
__esModule: true,
default: (_id: string, data: ToolNodeType) => ({
inputs: data,
setInputs: mockSetInputs,
}),
}))
vi.mock('@/app/components/tools/utils/to-form-schema', () => ({
getConfiguredValue: (...args: unknown[]) => mockGetConfiguredValue(...args),
toolParametersToFormSchemas: (...args: unknown[]) => mockToolParametersToFormSchemas(...args),
}))
vi.mock('@/service/tools', () => ({
updateBuiltInToolCredential: vi.fn(),
}))
vi.mock('@/service/use-tools', () => ({
useInvalidToolsByType: () => vi.fn(),
}))
vi.mock('@/app/components/workflow/store', () => ({
useWorkflowStore: () => ({
getState: () => ({
setControlPromptEditorRerenderKey: mockSetControlPromptEditorRerenderKey,
}),
}),
}))
vi.mock('../use-current-tool-collection', () => ({
__esModule: true,
default: (...args: unknown[]) => mockUseCurrentToolCollection(...args),
}))
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
title: 'Google Search',
desc: '',
type: BlockEnum.Tool,
provider_id: 'google_search',
provider_type: CollectionType.builtIn,
provider_name: 'Google Search',
tool_name: 'google_search',
tool_label: 'Google Search',
tool_parameters: {},
tool_configurations: {},
...overrides,
})
const currentTool = {
name: 'google_search',
parameters: [
{
variable: 'query',
form: 'llm',
label: { en_US: 'Query' },
type: 'string',
required: true,
default: 'default query',
},
{
variable: 'api_key',
form: 'credential',
label: { en_US: 'API Key' },
type: 'string',
required: true,
default: 'default secret',
},
],
}
const currentToolWithoutDefaults = {
name: 'google_search',
parameters: [
{
variable: 'query',
form: 'llm',
label: { en_US: 'Query' },
type: 'string',
required: true,
},
{
variable: 'api_key',
form: 'credential',
label: { en_US: 'API Key' },
type: 'string',
required: true,
},
],
}
const createToolVarInput = (value: string) => ({
type: VarType.mixed,
value,
})
describe('useConfig', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
mockUseCurrentToolCollection.mockReturnValue({
currCollection: {
name: 'google_search',
allow_delete: true,
is_team_authorization: false,
tools: [currentTool],
},
})
mockToolParametersToFormSchemas.mockImplementation(parameters => parameters)
mockGetConfiguredValue.mockImplementation((_value, schema: Array<{ variable: string, default?: string }>) => {
return schema.reduce<Record<string, ReturnType<typeof createToolVarInput>>>((acc, item) => {
acc[item.variable] = createToolVarInput(item.default || '')
return acc
}, {} as Record<string, ReturnType<typeof createToolVarInput>>)
})
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
})
describe('Default Value Sync', () => {
it('should apply default values only once when the current payload is initially empty', () => {
const emptyPayload = createNodeData()
const syncedPayload = createNodeData({
tool_parameters: { query: createToolVarInput('default query') },
tool_configurations: { api_key: createToolVarInput('default secret') },
})
const { rerender } = renderHook(
({ payload }) => useConfig('tool-node-1', payload),
{ initialProps: { payload: emptyPayload } },
)
expect(mockSetInputs).toHaveBeenCalledTimes(1)
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
tool_parameters: { query: createToolVarInput('default query') },
tool_configurations: { api_key: createToolVarInput('default secret') },
}))
rerender({ payload: syncedPayload })
expect(mockSetInputs).toHaveBeenCalledTimes(1)
})
it('should not update inputs when tool values are already populated on first render', () => {
renderHook(() => useConfig('tool-node-1', createNodeData({
tool_parameters: { query: createToolVarInput('existing query') },
tool_configurations: { api_key: createToolVarInput('existing secret') },
})))
expect(mockSetInputs).not.toHaveBeenCalled()
})
it('should not update inputs when empty schemas do not provide any default values', () => {
mockUseCurrentToolCollection.mockReturnValue({
currCollection: {
name: 'google_search',
allow_delete: true,
is_team_authorization: false,
tools: [currentToolWithoutDefaults],
},
})
mockGetConfiguredValue.mockReturnValue({})
renderHook(() => useConfig('tool-node-1', createNodeData()))
expect(mockSetInputs).not.toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,84 @@
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { renderHook } from '@testing-library/react'
import { CollectionType } from '@/app/components/tools/types'
import useCurrentToolCollection from '../use-current-tool-collection'
const mockUseAllBuiltInTools = vi.hoisted(() => vi.fn())
const mockUseAllCustomTools = vi.hoisted(() => vi.fn())
const mockUseAllWorkflowTools = vi.hoisted(() => vi.fn())
const mockUseAllMCPTools = vi.hoisted(() => vi.fn())
const mockCanFindTool = vi.hoisted(() => vi.fn())
vi.mock('@/service/use-tools', () => ({
useAllBuiltInTools: () => mockUseAllBuiltInTools(),
useAllCustomTools: () => mockUseAllCustomTools(),
useAllWorkflowTools: () => mockUseAllWorkflowTools(),
useAllMCPTools: () => mockUseAllMCPTools(),
}))
vi.mock('@/utils', () => ({
canFindTool: (...args: unknown[]) => mockCanFindTool(...args),
}))
const createToolCollection = (overrides: Partial<ToolWithProvider> = {}): ToolWithProvider => ({
id: 'builtin-search',
name: 'Google Search',
type: CollectionType.builtIn,
label: { en_US: 'Google Search' },
description: { en_US: 'Search provider' },
icon: '',
icon_dark: '',
background: '',
tags: [],
tools: [],
allow_delete: true,
is_team_authorization: false,
meta: {} as ToolWithProvider['meta'],
...overrides,
}) as ToolWithProvider
describe('useCurrentToolCollection', () => {
beforeEach(() => {
vi.clearAllMocks()
mockCanFindTool.mockImplementation((collectionId: string, providerId: string) => collectionId === providerId)
mockUseAllBuiltInTools.mockReturnValue({ data: [] })
mockUseAllCustomTools.mockReturnValue({ data: [] })
mockUseAllWorkflowTools.mockReturnValue({ data: [] })
mockUseAllMCPTools.mockReturnValue({ data: [] })
})
it('should return the built-in collection list and matched provider for built-in tools', () => {
const builtInCollection = createToolCollection({ id: 'builtin-search' })
mockUseAllBuiltInTools.mockReturnValue({ data: [builtInCollection] })
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.builtIn, 'builtin-search'))
expect(result.current.currentTools).toEqual([builtInCollection])
expect(result.current.currCollection).toBe(builtInCollection)
expect(mockCanFindTool).toHaveBeenCalledWith('builtin-search', 'builtin-search')
})
it('should select the custom tool collection when the provider type is custom', () => {
const customCollection = createToolCollection({
id: 'custom-search',
type: CollectionType.custom,
})
mockUseAllCustomTools.mockReturnValue({ data: [customCollection] })
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.custom, 'custom-search'))
expect(result.current.currentTools).toEqual([customCollection])
expect(result.current.currCollection).toBe(customCollection)
})
it('should return undefined when no collection matches the provider id', () => {
mockUseAllBuiltInTools.mockReturnValue({
data: [createToolCollection({ id: 'another-tool' })],
})
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.builtIn, 'builtin-search'))
expect(result.current.currentTools).toHaveLength(1)
expect(result.current.currCollection).toBeUndefined()
})
})

View File

@ -0,0 +1,49 @@
import type { ToolNodeType } from '../../types'
import { renderHook } from '@testing-library/react'
import { CollectionType } from '@/app/components/tools/types'
import { BlockEnum } from '@/app/components/workflow/types'
import useGetDataForCheckMore from '../use-get-data-for-check-more'
const mockUseConfig = vi.hoisted(() => vi.fn())
vi.mock('../use-config', () => ({
__esModule: true,
default: (...args: unknown[]) => mockUseConfig(...args),
}))
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
title: 'Google Search',
desc: '',
type: BlockEnum.Tool,
provider_id: 'google_search',
provider_type: CollectionType.builtIn,
provider_name: 'Google Search',
tool_name: 'google_search',
tool_label: 'Google Search',
tool_parameters: {},
tool_configurations: {},
...overrides,
})
describe('useGetDataForCheckMore', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should expose the config hook validator as getData', () => {
const getMoreDataForCheckValid = vi.fn(() => ({ provider: 'google' }))
const payload = createNodeData()
mockUseConfig.mockReturnValue({
getMoreDataForCheckValid,
})
const { result } = renderHook(() => useGetDataForCheckMore({
id: 'tool-node-1',
payload,
}))
expect(mockUseConfig).toHaveBeenCalledWith('tool-node-1', payload)
expect(result.current.getData).toBe(getMoreDataForCheckValid)
expect(result.current.getData()).toEqual({ provider: 'google' })
})
})

View File

@ -0,0 +1,206 @@
import type { ToolNodeType, VarType } from '../../types'
import type { InputVar } from '@/app/components/workflow/types'
import type { NodeTracing } from '@/types/workflow'
import { act, renderHook } from '@testing-library/react'
import { CollectionType } from '@/app/components/tools/types'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
import useSingleRunFormParams from '../use-single-run-form-params'
const mockUseToolIcon = vi.hoisted(() => vi.fn())
const mockUseNodeCrud = vi.hoisted(() => vi.fn())
const mockFormatToTracingNodeList = vi.hoisted(() => vi.fn())
vi.mock('@/app/components/workflow/hooks', () => ({
useToolIcon: (...args: unknown[]) => mockUseToolIcon(...args),
}))
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
__esModule: true,
default: (...args: unknown[]) => mockUseNodeCrud(...args),
}))
vi.mock('@/app/components/workflow/run/utils/format-log', () => ({
__esModule: true,
default: (...args: unknown[]) => mockFormatToTracingNodeList(...args),
}))
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
title: 'Google Search',
desc: '',
type: BlockEnum.Tool,
provider_id: 'google_search',
provider_type: CollectionType.builtIn,
provider_name: 'Google Search',
tool_name: 'google_search',
tool_label: 'Google Search',
tool_parameters: {},
tool_configurations: {},
...overrides,
})
const createInputVar = (variable: InputVar['variable']): InputVar => ({
type: InputVarType.textInput,
label: typeof variable === 'string' ? variable : 'invalid',
variable,
required: false,
})
const createRunResult = (): NodeTracing => ({
id: 'trace-1',
index: 0,
predecessor_node_id: '',
node_id: 'tool-node-1',
node_type: BlockEnum.Tool,
title: 'Google Search',
inputs: {},
inputs_truncated: false,
process_data: {},
process_data_truncated: false,
outputs_truncated: false,
status: 'succeeded',
elapsed_time: 1,
metadata: {
iterator_length: 0,
iterator_index: 0,
loop_length: 0,
loop_index: 0,
},
created_at: 0,
created_by: {
id: 'user-1',
name: 'User',
email: 'user@example.com',
},
finished_at: 1,
})
describe('useSingleRunFormParams', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseToolIcon.mockReturnValue('tool-icon')
mockFormatToTracingNodeList.mockReturnValue([{ id: 'formatted-node' }])
mockUseNodeCrud.mockImplementation((_id: string, payload: ToolNodeType) => ({
inputs: payload,
}))
})
describe('Variable Extraction', () => {
it('should build form inputs from variable params and settings and expose dependent vars', () => {
const payload = createNodeData({
tool_parameters: {
query: { type: 'variable' as VarType, value: ['start', 'query'] },
legacy_query: { type: 'variable' as VarType, value: 'legacy.answer' },
constant_query: { type: 'constant' as VarType, value: 'fixed' },
},
tool_configurations: {
prompt: { type: 'mixed' as VarType, value: 'prefix {{#tool.result#}}' },
api_key: { type: 'constant' as VarType, value: 'secret' },
plainText: 'ignored',
},
})
const getInputVars = vi.fn(() => [
createInputVar('#start.query#'),
createInputVar(undefined as unknown as string),
createInputVar('#legacy.answer#'),
])
const { result } = renderHook(() => useSingleRunFormParams({
id: 'tool-node-1',
payload,
runInputData: {},
runInputDataRef: { current: {} },
getInputVars,
setRunInputData: vi.fn(),
toVarInputs: vi.fn(),
runResult: null as unknown as NodeTracing,
}))
expect(getInputVars).toHaveBeenCalledWith([
'{{#start.query#}}',
'{{#legacy.answer#}}',
'prefix {{#tool.result#}}',
])
expect(result.current.forms).toHaveLength(1)
expect(result.current.forms[0].inputs).toEqual([
createInputVar('#start.query#'),
createInputVar(undefined as unknown as string),
createInputVar('#legacy.answer#'),
])
expect(result.current.forms[0].values).toEqual({})
expect(result.current.toolIcon).toBe('tool-icon')
expect(result.current.getDependentVars()).toEqual([
['start', 'query'],
['legacy', 'answer'],
])
expect(result.current.nodeInfo).toBeNull()
})
})
describe('Form Updates', () => {
it('should update form values and forward run input data on change', () => {
const payload = createNodeData({
tool_parameters: {
nullable_constant: { type: 'constant' as VarType, value: null },
query: { type: 'variable' as VarType, value: ['start', 'query'] },
},
})
const getInputVars = vi.fn(() => [createInputVar('#start.query#')])
const setRunInputData = vi.fn()
const { result } = renderHook(() => useSingleRunFormParams({
id: 'tool-node-1',
payload,
runInputData: {},
runInputDataRef: { current: {} },
getInputVars,
setRunInputData,
toVarInputs: vi.fn(),
runResult: null as unknown as NodeTracing,
}))
act(() => {
result.current.forms[0].onChange({
query: 'weather',
tool_parameters: {
nullable_constant: 'temp-value',
},
})
})
expect(setRunInputData).toHaveBeenCalledWith({
query: 'weather',
tool_parameters: {
nullable_constant: 'temp-value',
},
})
expect(result.current.forms[0].values).toEqual({
query: 'weather',
tool_parameters: {
nullable_constant: 'temp-value',
},
nullable_constant: null,
})
})
})
describe('Tracing Data', () => {
it('should format the latest run result into node info when a run result exists', () => {
const payload = createNodeData()
const runResult = createRunResult()
const { result } = renderHook(() => useSingleRunFormParams({
id: 'tool-node-1',
payload,
runInputData: {},
runInputDataRef: { current: {} },
getInputVars: vi.fn(() => []),
setRunInputData: vi.fn(),
toVarInputs: vi.fn(),
runResult,
}))
expect(mockFormatToTracingNodeList).toHaveBeenCalledWith([runResult], expect.any(Function))
expect(result.current.nodeInfo).toEqual({ id: 'formatted-node' })
})
})
})

View File

@ -0,0 +1,287 @@
import type { ToolNodeType, ToolVarInputs } from '../types'
import type { InputVar } from '@/app/components/workflow/types'
import { useBoolean } from 'ahooks'
import { capitalize } from 'es-toolkit/string'
import { produce } from 'immer'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { CollectionType } from '@/app/components/tools/types'
import {
getConfiguredValue,
toolParametersToFormSchemas,
} from '@/app/components/tools/utils/to-form-schema'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { updateBuiltInToolCredential } from '@/service/tools'
import {
useInvalidToolsByType,
} from '@/service/use-tools'
import { isToolAuthorizationRequired } from '../auth'
import { normalizeJsonSchemaType } from '../output-schema-utils'
import useCurrentToolCollection from './use-current-tool-collection'
const formatDisplayType = (output: Record<string, unknown>): string => {
const normalizedType = normalizeJsonSchemaType(output) || 'Unknown'
return capitalize(normalizedType)
}
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 { currCollection } = useCurrentToolCollection(provider_type, provider_id)
// Auth
const isShowAuthBtn = isToolAuthorizationRequired(provider_type, currCollection)
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('api.actionSuccess', { ns: 'common' }),
})
invalidToolsByType()
hideSetAuthModal()
},
[
currCollection?.name,
hideSetAuthModal,
t,
invalidToolsByType,
],
)
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 = useCallback(() => {
const inputsWithDefaultValue = produce(inputs, (draft) => {
if (
!draft.tool_configurations
|| Object.keys(draft.tool_configurations).length === 0
) {
const configuredToolSettings = getConfiguredValue(
tool_configurations,
toolSettingSchema,
) as ToolVarInputs
if (Object.keys(configuredToolSettings).length > 0)
draft.tool_configurations = configuredToolSettings
}
if (
!draft.tool_parameters
|| Object.keys(draft.tool_parameters).length === 0
) {
const configuredToolParameters = getConfiguredValue(
tool_parameters,
toolInputVarSchema,
) as ToolVarInputs
if (Object.keys(configuredToolParameters).length > 0)
draft.tool_parameters = configuredToolParameters
}
})
return inputsWithDefaultValue
}, [inputs, toolInputVarSchema, toolSettingSchema, tool_configurations, tool_parameters])
useEffect(() => {
if (!currTool)
return
const inputsWithDefaultValue = formattingParameters()
if (inputsWithDefaultValue === inputs)
return
const { setControlPromptEditorRerenderKey } = workflowStore.getState()
setInputs(inputsWithDefaultValue)
const rerenderTimeout = setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
return () => {
clearTimeout(rerenderTimeout)
}
}, [currTool, formattingParameters, inputs, setInputs, workflowStore])
// 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 {
const normalizedType = normalizeJsonSchemaType(output)
res.push({
name: outputKey,
type:
normalizedType === 'array'
? `Array[${output.items ? formatDisplayType(output.items) : 'Unknown'}]`
: formatDisplayType(output),
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,47 @@
import type { ToolNodeType } from '../types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { useMemo } from 'react'
import { CollectionType } from '@/app/components/tools/types'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllMCPTools,
useAllWorkflowTools,
} from '@/service/use-tools'
import { canFindTool } from '@/utils'
const useCurrentToolCollection = (
providerType: ToolNodeType['provider_type'],
providerId: string,
) => {
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const currentTools = useMemo<ToolWithProvider[]>(() => {
switch (providerType) {
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, providerType, workflowTools])
const currCollection = useMemo(() => {
return currentTools.find(item => canFindTool(item.id, providerId))
}, [currentTools, providerId])
return {
currentTools,
currCollection,
}
}
export default useCurrentToolCollection

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,171 @@
import type { RefObject } from 'react'
import type { ToolNodeType } from '../types'
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types'
import type { NodeTracing } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useToolIcon } from '@/app/components/workflow/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
import { AGENT_CONTEXT_VAR_PATTERN } from '@/app/components/workflow/utils/agent-context'
import { VarType } from '../types'
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
}
type NestedNodeParam = {
type?: VarType
value?: unknown
nested_node_config?: {
extractor_node_id?: string
output_selector?: unknown
}
}
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 => ![VarType.constant, VarType.nested_node].includes(inputs.tool_parameters[key].type))
.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, setInputVarValues] = useState<Record<string, any>>({})
const handleInputVarValuesChange = useCallback((value: Record<string, any>) => {
setInputVarValues(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: handleInputVarValuesChange,
}]
return forms
}, [handleInputVarValuesChange, inputVarValuesWithConstantValue, varInputs])
const nodeInfo = useMemo(() => {
if (!runResult)
return null
return formatToTracingNodeList([runResult], t)[0]
}, [runResult, t])
const toolIcon = useToolIcon(payload)
const resolveOutputSelector = (extractorNodeId: string, rawSelector?: unknown) => {
if (!Array.isArray(rawSelector))
return [] as string[]
if (rawSelector[0] === extractorNodeId)
return rawSelector.slice(1) as string[]
return rawSelector as string[]
}
const getDefaultNestedOutputSelector = (paramKey: string, value?: unknown) => {
if (typeof value === 'string') {
const matches = Array.from(value.matchAll(AGENT_CONTEXT_VAR_PATTERN))
if (matches.length > 0)
return ['structured_output', paramKey]
}
return ['result']
}
const collectNestedNodeSelectors = (params: Record<string, NestedNodeParam> = {}) => {
return Object.entries(params).flatMap(([paramKey, param]) => {
if (!param || param.type !== VarType.nested_node)
return [] as ValueSelector[]
const nestedConfig = param.nested_node_config
const extractorNodeId = nestedConfig?.extractor_node_id || `${id}_ext_${paramKey}`
const rawSelector = nestedConfig?.output_selector
const resolvedOutputSelector = resolveOutputSelector(extractorNodeId, rawSelector)
const outputSelector = resolvedOutputSelector.length > 0
? resolvedOutputSelector
: getDefaultNestedOutputSelector(paramKey, param.value)
return outputSelector.length > 0
? [[extractorNodeId, ...outputSelector]]
: []
})
}
const getDependentVars = () => {
const selectorList: ValueSelector[] = []
varInputs.forEach((item) => {
// Guard against null/undefined variable to prevent app crash
if (!item.variable || typeof item.variable !== 'string')
return
const selector = item.variable.slice(1, -1).split('.')
if (selector.length > 0)
selectorList.push(selector)
})
const nestedSelectors = [
...collectNestedNodeSelectors(inputs.tool_parameters as Record<string, NestedNodeParam>),
...collectNestedNodeSelectors(inputs.tool_configurations as Record<string, NestedNodeParam>),
]
selectorList.push(...nestedSelectors)
const seen = new Set<string>()
return selectorList.filter((selector) => {
const key = selector.join('.')
if (seen.has(key))
return false
seen.add(key)
return true
})
}
return {
forms,
nodeInfo,
toolIcon,
getDependentVars,
}
}
export default useSingleRunFormParams