merge main

This commit is contained in:
JzoNg
2025-11-14 10:51:26 +08:00
1272 changed files with 74526 additions and 8746 deletions

View File

@ -12,7 +12,7 @@ import SearchInput from '@/app/components/base/search-input'
import Tools from '../../../block-selector/tools'
import { useTranslation } from 'react-i18next'
import { useStrategyProviders } from '@/service/use-strategy'
import { PluginType, type StrategyPluginDetail } from '@/app/components/plugins/types'
import { PluginCategoryEnum, type StrategyPluginDetail } from '@/app/components/plugins/types'
import type { ToolWithProvider } from '../../../types'
import { CollectionType } from '@/app/components/tools/types'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
@ -140,7 +140,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
if (query) {
fetchPlugins({
query,
category: PluginType.agent,
category: PluginCategoryEnum.agent,
})
}
}, [query])

View File

@ -22,6 +22,7 @@ import type { Node } from 'reactflow'
import type { PluginMeta } from '@/app/components/plugins/types'
import { noop } from 'lodash-es'
import { useDocLink } from '@/context/i18n'
import { AppModeEnum } from '@/types/app'
export type Strategy = {
agent_strategy_provider_name: string
@ -99,7 +100,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
modelConfig={
defaultModel.data
? {
mode: 'chat',
mode: AppModeEnum.CHAT,
name: defaultModel.data.model,
provider: defaultModel.data.provider.provider,
completion_params: {},

View File

@ -140,7 +140,7 @@ const FormItem: FC<Props> = ({
<Input
value={value || ''}
onChange={e => onChange(e.target.value)}
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
autoFocus={autoFocus}
/>
)
@ -152,7 +152,7 @@ const FormItem: FC<Props> = ({
type="number"
value={value || ''}
onChange={e => onChange(e.target.value)}
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
autoFocus={autoFocus}
/>
)
@ -163,7 +163,7 @@ const FormItem: FC<Props> = ({
<Textarea
value={value || ''}
onChange={e => onChange(e.target.value)}
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
autoFocus={autoFocus}
/>
)

View File

@ -6,7 +6,7 @@ import cn from 'classnames'
import type { CodeLanguage } from '../../code/types'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import { ActionButton } from '@/app/components/base/action-button'
import { AppType } from '@/types/app'
import { AppModeEnum } from '@/types/app'
import type { GenRes } from '@/service/debug'
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
import { useHooksStore } from '../../../hooks-store'
@ -42,7 +42,7 @@ const CodeGenerateBtn: FC<Props> = ({
</ActionButton>
{showAutomatic && (
<GetCodeGeneratorResModal
mode={AppType.chat}
mode={AppModeEnum.CHAT}
isShow={showAutomatic}
codeLanguages={codeLanguages}
onClose={showAutomaticFalse}

View File

@ -0,0 +1,40 @@
import type { FC, ReactNode } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export enum StartNodeTypeEnum {
Start = 'start',
Trigger = 'trigger',
}
type EntryNodeContainerProps = {
children: ReactNode
customLabel?: string
nodeType?: StartNodeTypeEnum
}
const EntryNodeContainer: FC<EntryNodeContainerProps> = ({
children,
customLabel,
nodeType = StartNodeTypeEnum.Trigger,
}) => {
const { t } = useTranslation()
const label = useMemo(() => {
const translationKey = nodeType === StartNodeTypeEnum.Start ? 'entryNodeStatus' : 'triggerStatus'
return customLabel || t(`workflow.${translationKey}.enabled`)
}, [customLabel, nodeType, t])
return (
<div className="w-fit min-w-[242px] rounded-2xl bg-workflow-block-wrapper-bg-1 px-0 pb-0 pt-0.5">
<div className="mb-0.5 flex items-center px-1.5 pt-0.5">
<span className="text-2xs font-semibold uppercase text-text-tertiary">
{label}
</span>
</div>
{children}
</div>
)
}
export default EntryNodeContainer

View File

@ -1,38 +1,50 @@
'use client'
import type { FC } from 'react'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useEffect, useMemo, useState } from 'react'
import { type ResourceVarInputs, VarKindType } from '../types'
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import type { Tool } from '@/app/components/tools/types'
import FormInputTypeSwitch from './form-input-type-switch'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import Input from '@/app/components/base/input'
import { SimpleSelect } from '@/app/components/base/select'
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
import FormInputBoolean from './form-input-boolean'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import cn from '@/utils/classnames'
import type { Tool } from '@/app/components/tools/types'
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { RiCheckLine, RiLoader4Line } from '@remixicon/react'
import type { Event } from '@/app/components/tools/types'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import CheckboxList from '@/app/components/base/checkbox-list'
import FormInputBoolean from './form-input-boolean'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema
value: ToolVarInputs
value: ResourceVarInputs
onChange: (value: any) => void
inPanel?: boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
currentTool?: Tool | Event
currentProvider?: ToolWithProvider | TriggerWithProvider
showManageInputField?: boolean
onManageInputField?: () => void
extraParams?: Record<string, any>
providerType?: string
disableVariableInsertion?: boolean
}
const FormInputItem: FC<Props> = ({
@ -46,15 +58,22 @@ const FormInputItem: FC<Props> = ({
currentProvider,
showManageInputField,
onManageInputField,
extraParams,
providerType,
disableVariableInsertion = false,
}) => {
const language = useLanguage()
const [toolsOptions, setToolsOptions] = useState<FormOption[] | null>(null)
const [isLoadingToolsOptions, setIsLoadingToolsOptions] = useState(false)
const {
placeholder,
variable,
type,
_type,
default: defaultValue,
options,
multiple,
scope,
} = schema as any
const varInput = value[variable]
@ -64,13 +83,16 @@ const FormInputItem: FC<Props> = ({
const isArray = type === FormTypeEnum.array
const isShowJSONEditor = isObject || isArray
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isBoolean = type === FormTypeEnum.boolean
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
const isBoolean = _type === FormTypeEnum.boolean
const isCheckbox = _type === FormTypeEnum.checkbox
const isSelect = type === FormTypeEnum.select
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
const showTypeSwitch = isNumber || isBoolean || isObject || isArray || isSelect
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
const isMultipleSelect = multiple && (isSelect || isDynamicSelect)
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
@ -123,12 +145,71 @@ const FormInputItem: FC<Props> = ({
const getVarKindType = () => {
if (isFile)
return VarKindType.variable
if (isSelect || isBoolean || isNumber || isArray || isObject)
if (isSelect || isDynamicSelect || isBoolean || isNumber || isArray || isObject)
return VarKindType.constant
if (isString)
return VarKindType.mixed
}
// Fetch dynamic options hook for tools
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '',
currentProvider?.name || '',
currentTool?.name || '',
variable || '',
providerType,
extraParams,
)
// Fetch dynamic options hook for triggers
const { data: triggerDynamicOptions, isLoading: isTriggerOptionsLoading } = useTriggerPluginDynamicOptions({
plugin_id: currentProvider?.plugin_id || '',
provider: currentProvider?.name || '',
action: currentTool?.name || '',
parameter: variable || '',
extra: extraParams,
credential_id: currentProvider?.credential_id || '',
}, isDynamicSelect && providerType === PluginCategoryEnum.trigger && !!currentTool && !!currentProvider)
// Computed values for dynamic options (unified for triggers and tools)
const triggerOptions = triggerDynamicOptions?.options
const dynamicOptions = providerType === PluginCategoryEnum.trigger
? triggerOptions ?? toolsOptions
: toolsOptions
const isLoadingOptions = providerType === PluginCategoryEnum.trigger
? (isTriggerOptionsLoading || isLoadingToolsOptions)
: isLoadingToolsOptions
// Fetch dynamic options for tools only (triggers use hook directly)
useEffect(() => {
const fetchPanelDynamicOptions = async () => {
if (isDynamicSelect && currentTool && currentProvider && (providerType === PluginCategoryEnum.tool || providerType === PluginCategoryEnum.trigger)) {
setIsLoadingToolsOptions(true)
try {
const data = await fetchDynamicOptions()
setToolsOptions(data?.options || [])
}
catch (error) {
console.error('Failed to fetch dynamic options:', error)
setToolsOptions([])
}
finally {
setIsLoadingToolsOptions(false)
}
}
}
fetchPanelDynamicOptions()
}, [
isDynamicSelect,
currentTool?.name,
currentProvider?.name,
variable,
extraParams,
providerType,
fetchDynamicOptions,
])
const handleTypeChange = (newType: string) => {
if (newType === VarKindType.variable) {
onChange({
@ -163,6 +244,24 @@ const FormInputItem: FC<Props> = ({
})
}
const getSelectedLabels = (selectedValues: any[]) => {
if (!selectedValues || selectedValues.length === 0)
return ''
const optionsList = isDynamicSelect ? (dynamicOptions || options || []) : (options || [])
const selectedOptions = optionsList.filter((opt: any) =>
selectedValues.includes(opt.value),
)
if (selectedOptions.length <= 2) {
return selectedOptions
.map((opt: any) => opt.label?.[language] || opt.label?.en_US || opt.value)
.join(', ')
}
return `${selectedOptions.length} selected`
}
const handleAppOrModelSelect = (newValue: any) => {
onChange({
...value,
@ -184,6 +283,45 @@ const FormInputItem: FC<Props> = ({
})
}
const availableCheckboxOptions = useMemo(() => (
(options || []).filter((option: { show_on?: Array<{ variable: string; value: any }> }) => {
if (option.show_on?.length)
return option.show_on.every(showOnItem => value[showOnItem.variable]?.value === showOnItem.value || value[showOnItem.variable] === showOnItem.value)
return true
})
), [options, value])
const checkboxListOptions = useMemo(() => (
availableCheckboxOptions.map((option: { value: string; label: Record<string, string> }) => ({
value: option.value,
label: option.label?.[language] || option.label?.en_US || option.value,
}))
), [availableCheckboxOptions, language])
const checkboxListValue = useMemo(() => {
let current: string[] = []
if (Array.isArray(varInput?.value))
current = varInput.value as string[]
else if (typeof varInput?.value === 'string')
current = [varInput.value as string]
else if (Array.isArray(defaultValue))
current = defaultValue as string[]
const allowedValues = new Set(availableCheckboxOptions.map((option: { value: string }) => option.value))
return current.filter(item => allowedValues.has(item))
}, [varInput?.value, defaultValue, availableCheckboxOptions])
const handleCheckboxListChange = (selected: string[]) => {
onChange({
...value,
[variable]: {
...varInput,
type: VarKindType.constant,
value: selected,
},
})
}
return (
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
{showTypeSwitch && (
@ -198,6 +336,7 @@ const FormInputItem: FC<Props> = ({
availableNodes={availableNodesWithParent}
showManageInputField={showManageInputField}
onManageInputField={onManageInputField}
disableVariableInsertion={disableVariableInsertion}
/>
)}
{isNumber && isConstant && (
@ -209,13 +348,23 @@ const FormInputItem: FC<Props> = ({
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)}
{isCheckbox && isConstant && (
<CheckboxList
title={schema.label?.[language] || schema.label?.en_US || variable}
value={checkboxListValue}
onChange={handleCheckboxListChange}
options={checkboxListOptions}
disabled={readOnly}
maxHeight='200px'
/>
)}
{isBoolean && isConstant && (
<FormInputBoolean
value={varInput?.value as boolean}
onChange={handleValueChange}
/>
)}
{isSelect && isConstant && (
{isSelect && isConstant && !isMultipleSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
disabled={readOnly}
@ -225,11 +374,175 @@ const FormInputItem: FC<Props> = ({
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => ({
value: option.value,
name: option.label[language] || option.label.en_US,
icon: option.icon,
}))}
onSelect={item => handleValueChange(item.value as string)}
placeholder={placeholder?.[language] || placeholder?.en_US}
renderOption={options.some((opt: any) => opt.icon) ? ({ item }) => (
<div className="flex items-center">
{item.icon && (
<img src={item.icon} alt="" className="mr-2 h-4 w-4" />
)}
<span>{item.name}</span>
</div>
) : undefined}
/>
)}
{isSelect && isConstant && isMultipleSelect && (
<Listbox
multiple
value={varInput?.value || []}
onChange={handleValueChange}
disabled={readOnly}
>
<div className="group/simple-select relative h-8 grow">
<ListboxButton className="flex h-full w-full cursor-pointer items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6">
<span className={cn('system-sm-regular block truncate text-left',
varInput?.value?.length > 0 ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder',
)}>
{getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
aria-hidden="true"
/>
</span>
</ListboxButton>
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm">
{options.filter((option: { show_on: any[] }) => {
if (option.show_on?.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
<ListboxOption
key={option.value}
value={option.value}
className={({ focus }) =>
cn('relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
focus && 'bg-state-base-hover',
)
}
>
{({ selected }) => (
<>
<div className="flex items-center">
{option.icon && (
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
)}
<span className={cn('block truncate', selected && 'font-normal')}>
{option.label[language] || option.label.en_US}
</span>
</div>
{selected && (
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-text-accent">
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
)}
{isDynamicSelect && !isMultipleSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
disabled={readOnly || isLoadingOptions}
defaultValue={varInput?.value}
items={(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
if (option.show_on?.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => ({
value: option.value,
name: option.label[language] || option.label.en_US,
icon: option.icon,
}))}
onSelect={item => handleValueChange(item.value as string)}
placeholder={isLoadingOptions ? 'Loading...' : (placeholder?.[language] || placeholder?.en_US)}
renderOption={({ item }) => (
<div className="flex items-center">
{item.icon && (
<img src={item.icon} alt="" className="mr-2 h-4 w-4" />
)}
<span>{item.name}</span>
</div>
)}
/>
)}
{isDynamicSelect && isMultipleSelect && (
<Listbox
multiple
value={varInput?.value || []}
onChange={handleValueChange}
disabled={readOnly || isLoadingOptions}
>
<div className="group/simple-select relative h-8 grow">
<ListboxButton className="flex h-full w-full cursor-pointer items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6">
<span className={cn('system-sm-regular block truncate text-left',
isLoadingOptions ? 'text-components-input-text-placeholder'
: varInput?.value?.length > 0 ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder',
)}>
{isLoadingOptions
? 'Loading...'
: getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
{isLoadingOptions ? (
<RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
) : (
<ChevronDownIcon
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
aria-hidden="true"
/>
)}
</span>
</ListboxButton>
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm">
{(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
if (option.show_on?.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
<ListboxOption
key={option.value}
value={option.value}
className={({ focus }) =>
cn('relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
focus && 'bg-state-base-hover',
)
}
>
{({ selected }) => (
<>
<div className="flex items-center">
{option.icon && (
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
)}
<span className={cn('block truncate', selected && 'font-normal')}>
{option.label[language] || option.label.en_US}
</span>
</div>
{selected && (
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-text-accent">
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
)}
{isShowJSONEditor && isConstant && (
<div className='mt-1 w-full'>
<CodeEditor

View File

@ -1,37 +1,96 @@
import Button from '@/app/components/base/button'
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
import type { ComponentProps, MouseEventHandler } from 'react'
import { useState } from 'react'
import classNames from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
import { TaskStatus } from '@/app/components/plugins/types'
import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins'
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
uniqueIdentifier: string
extraIdentifiers?: string[]
onSuccess?: () => void
}
export const InstallPluginButton = (props: InstallPluginButtonProps) => {
const { className, uniqueIdentifier, onSuccess, ...rest } = props
const {
className,
uniqueIdentifier,
extraIdentifiers = [],
onSuccess,
...rest
} = props
const { t } = useTranslation()
const identifiers = Array.from(new Set(
[uniqueIdentifier, ...extraIdentifiers].filter((item): item is string => Boolean(item)),
))
const manifest = useCheckInstalled({
pluginIds: [uniqueIdentifier],
enabled: !!uniqueIdentifier,
pluginIds: identifiers,
enabled: identifiers.length > 0,
})
const install = useInstallPackageFromMarketPlace()
const isLoading = manifest.isLoading || install.isPending
// await for refetch to get the new installed plugin, when manifest refetch, this component will unmount
|| install.isSuccess
const [isTracking, setIsTracking] = useState(false)
const isLoading = manifest.isLoading || install.isPending || isTracking
const handleInstall: MouseEventHandler = (e) => {
e.stopPropagation()
if (isLoading)
return
setIsTracking(true)
install.mutate(uniqueIdentifier, {
onSuccess: async () => {
await manifest.refetch()
onSuccess?.()
onSuccess: async (response) => {
const finish = async () => {
await manifest.refetch()
onSuccess?.()
setIsTracking(false)
install.reset()
}
if (!response) {
await finish()
return
}
if (response.all_installed) {
await finish()
return
}
const { check } = checkTaskStatus()
try {
const { status } = await check({
taskId: response.task_id,
pluginUniqueIdentifier: uniqueIdentifier,
})
if (status === TaskStatus.failed) {
setIsTracking(false)
install.reset()
return
}
await finish()
}
catch {
setIsTracking(false)
install.reset()
}
},
onError: () => {
setIsTracking(false)
install.reset()
},
})
}
if (!manifest.data) return null
if (manifest.data.plugins.some(plugin => plugin.id === uniqueIdentifier)) return null
const identifierSet = new Set(identifiers)
const isInstalled = manifest.data.plugins.some(plugin => (
identifierSet.has(plugin.id)
|| (plugin.plugin_unique_identifier && identifierSet.has(plugin.plugin_unique_identifier))
|| (plugin.plugin_id && identifierSet.has(plugin.plugin_id))
))
if (isInstalled) return null
return <Button
variant={'secondary'}
disabled={isLoading}

View File

@ -0,0 +1,62 @@
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'
type MixedVariableTextInputProps = {
readOnly?: boolean
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
value?: string
onChange?: (text: string) => void
}
const MixedVariableTextInput = ({
readOnly = false,
nodesOutputVars,
availableNodes = [],
value = '',
onChange,
}: MixedVariableTextInputProps) => {
const { t } = useTranslation()
return (
<PromptEditor
wrapperClassName={cn(
'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: true,
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),
}}
placeholder={<Placeholder />}
onChange={onChange}
/>
)
}
export default memo(MixedVariableTextInput)

View File

@ -0,0 +1,52 @@
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'
const Placeholder = () => {
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')}
<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

@ -39,11 +39,11 @@ const Add = ({
const { nodesReadOnly } = useNodesReadOnly()
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
handleNodeAdd(
{
nodeType: type,
toolDefaultValue,
pluginDefaultValue,
},
{
prevNodeId: nodeId,

View File

@ -38,8 +38,8 @@ const ChangeItem = ({
availableNextBlocks,
} = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
handleNodeChange(nodeId, type, sourceHandle, pluginDefaultValue)
}, [nodeId, sourceHandle, handleNodeChange])
const renderTrigger = useCallback(() => {

View File

@ -9,7 +9,6 @@ import {
RiPlayLargeLine,
} from '@remixicon/react'
import {
useNodeDataUpdate,
useNodesInteractions,
} from '../../../hooks'
import { type Node, NodeRunningStatus } from '../../../types'
@ -19,6 +18,9 @@ import {
Stop,
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import Tooltip from '@/app/components/base/tooltip'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useWorkflowRunValidation } from '@/app/components/workflow/hooks/use-checklist'
import Toast from '@/app/components/base/toast'
type NodeControlProps = Pick<Node, 'id' | 'data'>
const NodeControl: FC<NodeControlProps> = ({
@ -27,9 +29,11 @@ const NodeControl: FC<NodeControlProps> = ({
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const { handleNodeDataUpdate } = useNodeDataUpdate()
const { handleNodeSelect } = useNodesInteractions()
const workflowStore = useWorkflowStore()
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
const { warningNodes } = useWorkflowRunValidation()
const warningForNode = warningNodes.find(item => item.id === id)
const handleOpenChange = useCallback((newOpen: boolean) => {
setOpen(newOpen)
}, [])
@ -38,7 +42,8 @@ const NodeControl: FC<NodeControlProps> = ({
return (
<div
className={`
absolute -top-7 right-0 hidden h-7 pb-1 group-hover:flex
absolute -top-7 right-0 hidden h-7 pb-1
${!data._pluginInstallLocked && 'group-hover:flex'}
${data.selected && '!flex'}
${open && '!flex'}
`}
@ -50,17 +55,20 @@ const NodeControl: FC<NodeControlProps> = ({
{
canRunBySingle(data.type, isChildNode) && (
<div
className='flex h-5 w-5 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
className={`flex h-5 w-5 items-center justify-center rounded-md ${isSingleRunning ? 'cursor-pointer hover:bg-state-base-hover' : warningForNode ? 'cursor-not-allowed text-text-disabled' : 'cursor-pointer hover:bg-state-base-hover'}`}
onClick={() => {
const nextData: Record<string, any> = {
_isSingleRun: !isSingleRunning,
const action = isSingleRunning ? 'stop' : 'run'
if (!isSingleRunning && warningForNode) {
const message = warningForNode.errorMessage || t('workflow.panel.checklistTip')
Toast.notify({ type: 'error', message })
return
}
if(isSingleRunning)
nextData._singleRunningStatus = undefined
handleNodeDataUpdate({
id,
data: nextData,
const store = workflowStore.getState()
store.setInitShowLastRunTab(true)
store.setPendingSingleRun({
nodeId: id,
action,
})
handleNodeSelect(id)
}}
@ -70,7 +78,7 @@ const NodeControl: FC<NodeControlProps> = ({
? <Stop className='h-3 w-3' />
: (
<Tooltip
popupContent={t('workflow.panel.runThisStep')}
popupContent={warningForNode ? warningForNode.errorMessage || t('workflow.panel.checklistTip') : t('workflow.panel.runThisStep')}
asChild={false}
>
<RiPlayLargeLine className='h-3 w-3' />

View File

@ -16,7 +16,7 @@ import {
} from '../../../types'
import type { Node } from '../../../types'
import BlockSelector from '../../../block-selector'
import type { DataSourceDefaultValue, ToolDefaultValue } from '../../../block-selector/types'
import type { PluginDefaultValue } from '../../../block-selector/types'
import {
useAvailableBlocks,
useIsChatMode,
@ -25,6 +25,7 @@ import {
} from '../../../hooks'
import {
useStore,
useWorkflowStore,
} from '../../../store'
import cn from '@/utils/classnames'
@ -57,11 +58,11 @@ export const NodeTargetHandle = memo(({
if (!connected)
setOpen(v => !v)
}, [connected])
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue) => {
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
handleNodeAdd(
{
nodeType: type,
toolDefaultValue,
pluginDefaultValue,
},
{
nextNodeId: id,
@ -84,7 +85,10 @@ export const NodeTargetHandle = memo(({
data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
!connected && 'after:opacity-0',
data.type === BlockEnum.Start && 'opacity-0',
(data.type === BlockEnum.Start
|| data.type === BlockEnum.TriggerWebhook
|| data.type === BlockEnum.TriggerSchedule
|| data.type === BlockEnum.TriggerPlugin) && 'opacity-0',
handleClassName,
)}
isConnectable={isConnectable}
@ -124,7 +128,10 @@ export const NodeSourceHandle = memo(({
showExceptionStatus,
}: NodeHandleProps) => {
const { t } = useTranslation()
const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
const shouldAutoOpenStartNodeSelector = useStore(s => s.shouldAutoOpenStartNodeSelector)
const setShouldAutoOpenStartNodeSelector = useStore(s => s.setShouldAutoOpenStartNodeSelector)
const setHasSelectedStartNode = useStore(s => s.setHasSelectedStartNode)
const workflowStoreApi = useWorkflowStore()
const [open, setOpen] = useState(false)
const { handleNodeAdd } = useNodesInteractions()
const { getNodesReadOnly } = useNodesReadOnly()
@ -140,11 +147,11 @@ export const NodeSourceHandle = memo(({
e.stopPropagation()
setOpen(v => !v)
}, [])
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue) => {
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
handleNodeAdd(
{
nodeType: type,
toolDefaultValue,
pluginDefaultValue,
},
{
prevNodeId: id,
@ -154,9 +161,27 @@ export const NodeSourceHandle = memo(({
}, [handleNodeAdd, id, handleId])
useEffect(() => {
if (notInitialWorkflow && data.type === BlockEnum.Start && !isChatMode)
if (!shouldAutoOpenStartNodeSelector)
return
if (isChatMode) {
setShouldAutoOpenStartNodeSelector?.(false)
return
}
if (data.type === BlockEnum.Start || data.type === BlockEnum.TriggerSchedule || data.type === BlockEnum.TriggerWebhook || data.type === BlockEnum.TriggerPlugin) {
setOpen(true)
}, [notInitialWorkflow, data.type, isChatMode])
if (setShouldAutoOpenStartNodeSelector)
setShouldAutoOpenStartNodeSelector(false)
else
workflowStoreApi?.setState?.({ shouldAutoOpenStartNodeSelector: false })
if (setHasSelectedStartNode)
setHasSelectedStartNode(false)
else
workflowStoreApi?.setState?.({ hasSelectedStartNode: false })
}
}, [shouldAutoOpenStartNodeSelector, data.type, isChatMode, setShouldAutoOpenStartNodeSelector, setHasSelectedStartNode, workflowStoreApi])
return (
<Handle

View File

@ -1,63 +0,0 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { RiCrosshairLine } from '@remixicon/react'
import { useReactFlow, useStore } from 'reactflow'
import TooltipPlus from '@/app/components/base/tooltip'
import { useNodesSyncDraft } from '@/app/components/workflow-app/hooks'
type NodePositionProps = {
nodeId: string
}
const NodePosition = ({
nodeId,
}: NodePositionProps) => {
const { t } = useTranslation()
const reactflow = useReactFlow()
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const {
nodePosition,
nodeWidth,
nodeHeight,
} = useStore(useShallow((s) => {
const nodes = s.getNodes()
const currentNode = nodes.find(node => node.id === nodeId)!
return {
nodePosition: currentNode.position,
nodeWidth: currentNode.width,
nodeHeight: currentNode.height,
}
}))
const transform = useStore(s => s.transform)
if (!nodePosition || !nodeWidth || !nodeHeight) return null
const workflowContainer = document.getElementById('workflow-container')
const zoom = transform[2]
const { clientWidth, clientHeight } = workflowContainer!
const { setViewport } = reactflow
return (
<TooltipPlus
popupContent={t('workflow.panel.moveToThisNode')}
>
<div
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
onClick={() => {
setViewport({
x: (clientWidth - 400 - nodeWidth * zoom) / 2 - nodePosition.x * zoom,
y: (clientHeight - nodeHeight * zoom) / 2 - nodePosition.y * zoom,
zoom: transform[2],
})
doSyncWorkflowDraft()
}}
>
<RiCrosshairLine className='h-4 w-4 text-text-tertiary' />
</div>
</TooltipPlus>
)
}
export default memo(NodePosition)

View File

@ -8,12 +8,17 @@ import { intersection } from 'lodash-es'
import BlockSelector from '@/app/components/workflow/block-selector'
import {
useAvailableBlocks,
useIsChatMode,
useNodesInteractions,
} from '@/app/components/workflow/hooks'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import type {
Node,
OnSelectBlock,
} from '@/app/components/workflow/types'
import { BlockEnum, isTriggerNode } from '@/app/components/workflow/types'
import { FlowType } from '@/types/common'
type ChangeBlockProps = {
nodeId: string
@ -31,6 +36,14 @@ const ChangeBlock = ({
availablePrevBlocks,
availableNextBlocks,
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
const isChatMode = useIsChatMode()
const flowType = useHooksStore(s => s.configsMap?.flowType)
const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode
const ignoreNodeIds = useMemo(() => {
if (isTriggerNode(nodeData.type as BlockEnum))
return [nodeId]
return undefined
}, [nodeData.type, nodeId])
const availableNodes = useMemo(() => {
if (availablePrevBlocks.length && availableNextBlocks.length)
@ -41,8 +54,8 @@ const ChangeBlock = ({
return availableNextBlocks
}, [availablePrevBlocks, availableNextBlocks])
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
handleNodeChange(nodeId, type, sourceHandle, pluginDefaultValue)
}, [handleNodeChange, nodeId, sourceHandle])
const renderTrigger = useCallback(() => {
@ -64,6 +77,9 @@ const ChangeBlock = ({
trigger={renderTrigger}
popupClassName='min-w-[240px]'
availableBlocksTypes={availableNodes}
showStartTab={showStartTab}
ignoreNodeIds={ignoreNodeIds}
forceEnableStartTab={nodeData.type === BlockEnum.Start}
/>
)
}

View File

@ -76,9 +76,11 @@ const RetryOnPanel = ({
/>
<Input
type='number'
wrapperClassName='w-[80px]'
wrapperClassName='w-[100px]'
value={retry_config?.max_retries || 3}
onChange={e => handleMaxRetriesChange(e.target.value as any)}
onChange={e =>
handleMaxRetriesChange(Number.parseInt(e.currentTarget.value, 10) || 3)
}
min={1}
max={10}
unit={t('workflow.nodes.common.retry.times') || ''}
@ -96,9 +98,11 @@ const RetryOnPanel = ({
/>
<Input
type='number'
wrapperClassName='w-[80px]'
wrapperClassName='w-[100px]'
value={retry_config?.retry_interval || 1000}
onChange={e => handleRetryIntervalChange(e.target.value as any)}
onChange={e =>
handleRetryIntervalChange(Number.parseInt(e.currentTarget.value, 10) || 1000)
}
min={100}
max={5000}
unit={t('workflow.nodes.common.retry.ms') || ''}

View File

@ -8,7 +8,7 @@ import type {
VarType,
} from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isExceptionVariable } from '@/app/components/workflow/utils'
import {
VariableLabelInSelect,
@ -39,7 +39,8 @@ const VariableTag = ({
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar
const isGlobal = isGlobalVar(valueSelector)
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar || isGlobal
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
const isException = isExceptionVariable(variableName, node?.data.type)

View File

@ -1,14 +1,14 @@
'use client'
import cn from '@/utils/classnames'
import { RiArrowDropDownLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { Field as FieldType } from '../../../../../llm/types'
import { Type } from '../../../../../llm/types'
import { getFieldType } from '../../../../../llm/utils'
import type { Field as FieldType } from '../../../../../llm/types'
import cn from '@/utils/classnames'
import TreeIndentLine from '../tree-indent-line'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { RiArrowDropDownLine } from '@remixicon/react'
type Props = {
name: string,
@ -28,6 +28,7 @@ const Field: FC<Props> = ({
const { t } = useTranslation()
const isRoot = depth === 1
const hasChildren = payload.type === Type.object && payload.properties
const hasEnum = payload.enum && payload.enum.length > 0
const [fold, {
toggle: toggleFold,
}] = useBoolean(false)
@ -44,7 +45,10 @@ const Field: FC<Props> = ({
/>
)}
<div className={cn('system-sm-medium ml-[7px] h-6 truncate leading-6 text-text-secondary', isRoot && rootClassName)}>{name}</div>
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}</div>
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>
{getFieldType(payload)}
{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}
</div>
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
</div>
{payload.description && (
@ -52,6 +56,18 @@ const Field: FC<Props> = ({
<div className='system-xs-regular w-0 grow truncate text-text-tertiary'>{payload.description}</div>
</div>
)}
{hasEnum && (
<div className='ml-[7px] flex'>
<div className='system-xs-regular w-0 grow text-text-quaternary'>
{payload.enum!.map((value, index) => (
<span key={index}>
{typeof value === 'string' ? `"${value}"` : value}
{index < payload.enum!.length - 1 && ' | '}
</span>
))}
</div>
</div>
)}
</div>
</div>

View File

@ -39,6 +39,9 @@ import type {
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
import type { RAGPipelineVariable } from '@/models/pipeline'
import type { WebhookTriggerNodeType } from '@/app/components/workflow/nodes/trigger-webhook/types'
import type { PluginTriggerNodeType } from '@/app/components/workflow/nodes/trigger-plugin/types'
import PluginTriggerNodeDefault from '@/app/components/workflow/nodes/trigger-plugin/default'
import {
AGENT_OUTPUT_STRUCT,
@ -51,6 +54,7 @@ import {
SUPPORT_OUTPUT_VARS_NODE,
TEMPLATE_TRANSFORM_OUTPUT_STRUCT,
TOOL_OUTPUT_STRUCT,
getGlobalVars,
} from '@/app/components/workflow/constants'
import ToolNodeDefault from '@/app/components/workflow/nodes/tool/default'
import DataSourceNodeDefault from '@/app/components/workflow/nodes/data-source/default'
@ -59,11 +63,21 @@ import type { PromptItem } from '@/models/debug'
import { VAR_REGEX } from '@/config'
import type { AgentNodeType } from '../../../agent/types'
import type { SchemaTypeDefinition } from '@/service/use-common'
import { AppModeEnum } from '@/types/app'
export const isSystemVar = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'sys' || valueSelector[1] === 'sys'
}
export const isGlobalVar = (valueSelector: ValueSelector) => {
if(!isSystemVar(valueSelector)) return false
const second = valueSelector[1]
if(['query', 'files'].includes(second))
return false
return true
}
export const isENV = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'env'
}
@ -348,34 +362,29 @@ const formatItem = (
variable: 'sys.query',
type: VarType.string,
})
res.vars.push({
variable: 'sys.dialogue_count',
type: VarType.number,
})
res.vars.push({
variable: 'sys.conversation_id',
type: VarType.string,
})
}
res.vars.push({
variable: 'sys.user_id',
type: VarType.string,
})
res.vars.push({
variable: 'sys.files',
type: VarType.arrayFile,
})
res.vars.push({
variable: 'sys.app_id',
type: VarType.string,
})
res.vars.push({
variable: 'sys.workflow_id',
type: VarType.string,
})
res.vars.push({
variable: 'sys.workflow_run_id',
type: VarType.string,
break
}
case BlockEnum.TriggerWebhook: {
const {
variables = [],
} = data as WebhookTriggerNodeType
res.vars = variables.map((v) => {
const type = v.value_type || VarType.string
const varRes: Var = {
variable: v.variable,
type,
isParagraph: false,
isSelect: false,
options: v.options,
required: v.required,
}
return varRes
})
break
@ -612,6 +621,17 @@ const formatItem = (
break
}
case BlockEnum.TriggerPlugin: {
const outputSchema = PluginTriggerNodeDefault.getOutputVars?.(
data as PluginTriggerNodeType,
allPluginInfoList,
[],
{ schemaTypeDefinitions },
) || []
res.vars = outputSchema
break
}
case 'env': {
res.vars = data.envList.map((env: EnvironmentVariable) => {
return {
@ -634,6 +654,11 @@ const formatItem = (
break
}
case 'global': {
res.vars = data.globalVarList
break
}
case 'rag': {
res.vars = data.ragVariables.map((ragVar: RAGPipelineVariable) => {
return {
@ -774,6 +799,15 @@ export const toNodeOutputVars = (
chatVarList: conversationVariables,
},
}
// GLOBAL_VAR_NODE data format
const GLOBAL_VAR_NODE = {
id: 'global',
data: {
title: 'SYSTEM',
type: 'global',
globalVarList: getGlobalVars(isChatMode),
},
}
// RAG_PIPELINE_NODE data format
const RAG_PIPELINE_NODE = {
id: 'rag',
@ -793,6 +827,8 @@ export const toNodeOutputVars = (
if (b.data.type === 'env') return -1
if (a.data.type === 'conversation') return 1
if (b.data.type === 'conversation') return -1
if (a.data.type === 'global') return 1
if (b.data.type === 'global') return -1
// sort nodes by x position
return (b.position?.x || 0) - (a.position?.x || 0)
})
@ -803,6 +839,7 @@ export const toNodeOutputVars = (
),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...(isChatMode && conversationVariables.length > 0 ? [CHAT_VAR_NODE] : []),
GLOBAL_VAR_NODE,
...(RAG_PIPELINE_NODE.data.ragVariables.length > 0
? [RAG_PIPELINE_NODE]
: []),
@ -1026,7 +1063,8 @@ export const getVarType = ({
if (valueSelector[1] === 'index') return VarType.number
}
const isSystem = isSystemVar(valueSelector)
const isGlobal = isGlobalVar(valueSelector)
const isInStartNodeSysVar = isSystemVar(valueSelector) && !isGlobal
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const isSharedRagVariable
@ -1039,7 +1077,8 @@ export const getVarType = ({
})
const targetVarNodeId = (() => {
if (isSystem) return startNode?.id
if (isInStartNodeSysVar) return startNode?.id
if (isGlobal) return 'global'
if (isInNodeRagVariable) return valueSelector[1]
return valueSelector[0]
})()
@ -1052,7 +1091,7 @@ export const getVarType = ({
let type: VarType = VarType.string
let curr: any = targetVar.vars
if (isSystem || isEnv || isChatVar || isSharedRagVariable) {
if (isInStartNodeSysVar || isEnv || isChatVar || isSharedRagVariable || isGlobal) {
return curr.find(
(v: any) => v.variable === (valueSelector as ValueSelector).join('.'),
)?.type
@ -1242,7 +1281,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
}
case BlockEnum.LLM: {
const payload = data as LLMNodeType
const isChatModel = payload.model?.mode === 'chat'
const isChatModel = payload.model?.mode === AppModeEnum.CHAT
let prompts: string[] = []
if (isChatModel) {
prompts
@ -1545,7 +1584,7 @@ export const updateNodeVars = (
}
case BlockEnum.LLM: {
const payload = data as LLMNodeType
const isChatModel = payload.model?.mode === 'chat'
const isChatModel = payload.model?.mode === AppModeEnum.CHAT
if (isChatModel) {
payload.prompt_template = (
payload.prompt_template as PromptItem[]

View File

@ -18,10 +18,11 @@ import {
import RemoveButton from '../remove-button'
import useAvailableVarList from '../../hooks/use-available-var-list'
import VarReferencePopup from './var-reference-popup'
import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar, removeFileVars, varTypeToStructType } from './utils'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, removeFileVars, varTypeToStructType } from './utils'
import ConstantField from './constant-field'
import cn from '@/utils/classnames'
import type { CommonNodeType, Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { BlockEnum } from '@/app/components/workflow/types'
@ -38,6 +39,7 @@ import {
useWorkflowVariables,
} from '@/app/components/workflow/hooks'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
// import type { BaseResource, BaseResourceProvider } from '@/app/components/workflow/nodes/_base/types'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import AddButton from '@/app/components/base/button/add-button'
import Badge from '@/app/components/base/badge'
@ -45,9 +47,10 @@ import Tooltip from '@/app/components/base/tooltip'
import { isExceptionVariable } from '@/app/components/workflow/utils'
import VarFullPathPanel from './var-full-path-panel'
import { noop } from 'lodash-es'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import type { Tool } from '@/app/components/tools/types'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
import { VAR_SHOW_NAME_MAP } from '@/app/components/workflow/constants'
const TRIGGER_DEFAULT_WIDTH = 227
@ -79,7 +82,7 @@ type Props = {
popupFor?: 'assigned' | 'toAssigned'
zIndex?: number
currentTool?: Tool
currentProvider?: ToolWithProvider
currentProvider?: ToolWithProvider | TriggerWithProvider
preferSchemaType?: boolean
}
@ -205,6 +208,9 @@ const VarReferencePicker: FC<Props> = ({
const varName = useMemo(() => {
if (!hasValue)
return ''
const showName = VAR_SHOW_NAME_MAP[(value as ValueSelector).join('.')]
if(showName)
return showName
const isSystem = isSystemVar(value as ValueSelector)
const varName = Array.isArray(value) ? value[(value as ValueSelector).length - 1] : ''
@ -293,15 +299,17 @@ const VarReferencePicker: FC<Props> = ({
preferSchemaType,
})
const { isEnv, isChatVar, isRagVar, isValidVar, isException } = useMemo(() => {
const { isEnv, isChatVar, isGlobal, isRagVar, isValidVar, isException } = useMemo(() => {
const isEnv = isENV(value as ValueSelector)
const isChatVar = isConversationVar(value as ValueSelector)
const isGlobal = isGlobalVar(value as ValueSelector)
const isRagVar = isRagVariableVar(value as ValueSelector)
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isRagVar
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isGlobal || isRagVar
const isException = isExceptionVariable(varName, outputVarNode?.type)
return {
isEnv,
isChatVar,
isGlobal,
isRagVar,
isValidVar,
isException,
@ -394,10 +402,11 @@ const VarReferencePicker: FC<Props> = ({
const variableCategory = useMemo(() => {
if (isEnv) return 'environment'
if (isChatVar) return 'conversation'
if (isGlobal) return 'global'
if (isLoopVar) return 'loop'
if (isRagVar) return 'rag'
return 'system'
}, [isEnv, isChatVar, isLoopVar, isRagVar])
}, [isEnv, isChatVar, isGlobal, isLoopVar, isRagVar])
return (
<div className={cn(className, !readonly && 'cursor-pointer')}>
@ -477,7 +486,7 @@ const VarReferencePicker: FC<Props> = ({
{hasValue
? (
<>
{isShowNodeName && !isEnv && !isChatVar && !isRagVar && (
{isShowNodeName && !isEnv && !isChatVar && !isGlobal && !isRagVar && (
<div className='flex items-center' onClick={(e) => {
if (e.metaKey || e.ctrlKey) {
e.stopPropagation()
@ -505,10 +514,11 @@ const VarReferencePicker: FC<Props> = ({
<div className='flex items-center text-text-accent'>
{isLoading && <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />}
<VariableIconWithColor
variables={value as ValueSelector}
variableCategory={variableCategory}
isExceptionVariable={isException}
/>
<div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{
<div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning', isGlobal && 'text-util-colors-orange-orange-600')} title={varName} style={{
maxWidth: maxVarNameWidth,
}}>{varName}</div>
</div>
@ -584,4 +594,4 @@ const VarReferencePicker: FC<Props> = ({
</div >
)
}
export default React.memo(VarReferencePicker)
export default React.memo(VarReferencePicker)

View File

@ -23,6 +23,7 @@ import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender
import ManageInputField from './manage-input-field'
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { VAR_SHOW_NAME_MAP } from '@/app/components/workflow/constants'
type ItemProps = {
nodeId: string
@ -82,10 +83,14 @@ const Item: FC<ItemProps> = ({
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
const varName = useMemo(() => {
if(VAR_SHOW_NAME_MAP[itemData.variable])
return VAR_SHOW_NAME_MAP[itemData.variable]
if (!isFlat)
return itemData.variable
if (itemData.variable === 'current')
return isInCodeGeneratorInstructionEditor ? 'current_code' : 'current_prompt'
return itemData.variable
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
@ -187,6 +192,7 @@ const Item: FC<ItemProps> = ({
>
<div className='flex w-0 grow items-center'>
{!isFlat && <VariableIconWithColor
variables={itemData.variable.split('.')}
variableCategory={variableCategory}
isExceptionVariable={isException}
/>}

View File

@ -11,6 +11,7 @@ import VariableIcon from './variable-icon'
import VariableName from './variable-name'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
import { isConversationVar, isENV, isGlobalVar, isRagVariableVar } from '../../utils'
const VariableLabel = ({
nodeType,
@ -26,6 +27,7 @@ const VariableLabel = ({
rightSlot,
}: VariablePayload) => {
const varColorClassName = useVarColor(variables, isExceptionVariable)
const isHideNodeLabel = !(isENV(variables) || isConversationVar(variables) || isGlobalVar(variables) || isRagVariableVar(variables))
return (
<div
className={cn(
@ -35,10 +37,12 @@ const VariableLabel = ({
onClick={onClick}
ref={ref}
>
<VariableNodeLabel
nodeType={nodeType}
nodeTitle={nodeTitle}
/>
{ isHideNodeLabel && (
<VariableNodeLabel
nodeType={nodeType}
nodeTitle={nodeTitle}
/>
)}
{
notShowFullPath && (
<>

View File

@ -1,15 +1,17 @@
import { useMemo } from 'react'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { BubbleX, Env, GlobalVariable } from '@/app/components/base/icons/src/vender/line/others'
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
import {
isConversationVar,
isENV,
isGlobalVar,
isRagVariableVar,
isSystemVar,
} from '../utils'
import { VarInInspectType } from '@/types/workflow'
import { VAR_SHOW_NAME_MAP } from '@/app/components/workflow/constants'
export const useVarIcon = (variables: string[], variableCategory?: VarInInspectType | string) => {
if (variableCategory === 'loop')
@ -24,6 +26,9 @@ export const useVarIcon = (variables: string[], variableCategory?: VarInInspectT
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
return BubbleX
if (isGlobalVar(variables) || variableCategory === VarInInspectType.system)
return GlobalVariable
return Variable02
}
@ -41,13 +46,22 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation')
return 'text-util-colors-teal-teal-700'
if (isGlobalVar(variables) || variableCategory === VarInInspectType.system)
return 'text-util-colors-orange-orange-600'
return 'text-text-accent'
}, [variables, isExceptionVariable, variableCategory])
}
export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
const showName = VAR_SHOW_NAME_MAP[variables.join('.')]
let variableFullPathName = variables.slice(1).join('.')
if (isRagVariableVar(variables))
variableFullPathName = variables.slice(2).join('.')
const varName = useMemo(() => {
let variableFullPathName = variables.slice(1).join('.')
variableFullPathName = variables.slice(1).join('.')
if (isRagVariableVar(variables))
variableFullPathName = variables.slice(2).join('.')
@ -58,6 +72,8 @@ export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
return `${isSystem ? 'sys.' : ''}${varName}`
}, [variables, notShowFullPath])
if (showName)
return showName
return varName
}

View File

@ -1,36 +1,18 @@
import type {
FC,
ReactNode,
} from 'react'
import React, {
cloneElement,
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import Tooltip from '@/app/components/base/tooltip'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import {
RiCloseLine,
RiPlayLargeLine,
} from '@remixicon/react'
import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
import NextStep from '../next-step'
import PanelOperator from '../panel-operator'
import NodePosition from '@/app/components/workflow/nodes/_base/components/node-position'
import HelpLink from '../help-link'
import {
DescriptionInput,
TitleInput,
} from '../title-description-input'
import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel'
import RetryOnPanel from '../retry/retry-on-panel'
import { useResizePanel } from '../../hooks/use-resize-panel'
import cn from '@/utils/classnames'
AuthCategory,
AuthorizedInDataSourceNode,
AuthorizedInNode,
PluginAuth,
PluginAuthInDataSourceNode,
} from '@/app/components/plugins/plugin-auth'
import { usePluginStore } from '@/app/components/plugins/plugin-detail-panel/store'
import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
import BlockIcon from '@/app/components/workflow/block-icon'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import {
WorkflowHistoryEvent,
useAvailableBlocks,
@ -41,41 +23,59 @@ import {
useToolIcon,
useWorkflowHistory,
} from '@/app/components/workflow/hooks'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form'
import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types'
import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types'
import { useLogs } from '@/app/components/workflow/run/hooks'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
import {
canRunBySingle,
hasErrorHandleNode,
hasRetryNode,
isSupportCustomRunForm,
} from '@/app/components/workflow/utils'
import Tooltip from '@/app/components/base/tooltip'
import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useStore } from '@/app/components/workflow/store'
import Tab, { TabType } from './tab'
import { useModalContext } from '@/context/modal-context'
import { useAllBuiltInTools } from '@/service/use-tools'
import { useAllTriggerPlugins } from '@/service/use-triggers'
import { FlowType } from '@/types/common'
import { canFindTool } from '@/utils'
import cn from '@/utils/classnames'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import {
RiCloseLine,
RiPlayLargeLine,
} from '@remixicon/react'
import { debounce } from 'lodash-es'
import type { FC, ReactNode } from 'react'
import React, {
cloneElement,
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { useResizePanel } from '../../hooks/use-resize-panel'
import BeforeRunForm from '../before-run-form'
import PanelWrap from '../before-run-form/panel-wrap'
import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel'
import HelpLink from '../help-link'
import NextStep from '../next-step'
import PanelOperator from '../panel-operator'
import RetryOnPanel from '../retry/retry-on-panel'
import { DescriptionInput, TitleInput } from '../title-description-input'
import LastRun from './last-run'
import useLastRun from './last-run/use-last-run'
import BeforeRunForm from '../before-run-form'
import { debounce } from 'lodash-es'
import { useLogs } from '@/app/components/workflow/run/hooks'
import PanelWrap from '../before-run-form/panel-wrap'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { FlowType } from '@/types/common'
import {
AuthorizedInDataSourceNode,
AuthorizedInNode,
PluginAuth,
PluginAuthInDataSourceNode,
} from '@/app/components/plugins/plugin-auth'
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
import { canFindTool } from '@/utils'
import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types'
import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types'
import { useModalContext } from '@/context/modal-context'
import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { useAllBuiltInTools } from '@/service/use-tools'
import Tab, { TabType } from './tab'
import { TriggerSubscription } from './trigger-subscription'
const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
const nodeType = params.payload.type
@ -86,6 +86,7 @@ const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
return <div>Custom Run Form: {nodeType} not found</div>
}
}
type BasePanelProps = {
children: ReactNode
id: Node['id']
@ -98,6 +99,7 @@ const BasePanel: FC<BasePanelProps> = ({
children,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const { showMessageLogModal } = useAppStore(useShallow(state => ({
showMessageLogModal: state.showMessageLogModal,
})))
@ -108,6 +110,13 @@ const BasePanel: FC<BasePanelProps> = ({
const nodePanelWidth = useStore(s => s.nodePanelWidth)
const otherPanelWidth = useStore(s => s.otherPanelWidth)
const setNodePanelWidth = useStore(s => s.setNodePanelWidth)
const {
pendingSingleRun,
setPendingSingleRun,
} = useStore(s => ({
pendingSingleRun: s.pendingSingleRun,
setPendingSingleRun: s.setPendingSingleRun,
}))
const reservedCanvasWidth = 400 // Reserve the minimum visible width for the canvas
@ -212,6 +221,7 @@ const BasePanel: FC<BasePanelProps> = ({
useEffect(() => {
hasClickRunning.current = false
}, [id])
const {
nodesMap,
} = useNodesMetaData()
@ -235,6 +245,7 @@ const BasePanel: FC<BasePanelProps> = ({
singleRunParams,
nodeInfo,
setRunInputData,
handleStop,
handleSingleRun,
handleRunWithParams,
getExistVarValuesInForms,
@ -252,26 +263,65 @@ const BasePanel: FC<BasePanelProps> = ({
setIsPaused(false)
}, [tabType])
useEffect(() => {
if (!pendingSingleRun || pendingSingleRun.nodeId !== id)
return
if (pendingSingleRun.action === 'run')
handleSingleRun()
else
handleStop()
setPendingSingleRun(undefined)
}, [pendingSingleRun, id, handleSingleRun, handleStop, setPendingSingleRun])
const logParams = useLogs()
const passedLogParams = (() => {
if ([BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type))
return logParams
return {}
})()
const passedLogParams = useMemo(() => [BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type) ? logParams : {}, [data.type, logParams])
const storeBuildInTools = useStore(s => s.buildInTools)
const { data: buildInTools } = useAllBuiltInTools()
const currCollection = useMemo(() => {
return buildInTools?.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, data.provider_id])
const showPluginAuth = useMemo(() => {
return data.type === BlockEnum.Tool && currCollection?.allow_delete
}, [currCollection, data.type])
const currToolCollection = useMemo(() => {
const candidates = buildInTools ?? storeBuildInTools
return candidates?.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, storeBuildInTools, data.provider_id])
const needsToolAuth = useMemo(() => {
return data.type === BlockEnum.Tool && currToolCollection?.allow_delete
}, [data.type, currToolCollection?.allow_delete])
// only fetch trigger plugins when the node is a trigger plugin
const { data: triggerPlugins = [] } = useAllTriggerPlugins(data.type === BlockEnum.TriggerPlugin)
const currentTriggerPlugin = useMemo(() => {
if (data.type !== BlockEnum.TriggerPlugin || !data.plugin_id || !triggerPlugins?.length)
return undefined
return triggerPlugins?.find(p => p.plugin_id === data.plugin_id)
}, [data.type, data.plugin_id, triggerPlugins])
const { setDetail } = usePluginStore()
useEffect(() => {
if (currentTriggerPlugin?.subscription_constructor) {
setDetail({
name: currentTriggerPlugin.label[language],
plugin_id: currentTriggerPlugin.plugin_id || '',
plugin_unique_identifier: currentTriggerPlugin.plugin_unique_identifier || '',
id: currentTriggerPlugin.id,
provider: currentTriggerPlugin.name,
declaration: {
trigger: {
subscription_schema: currentTriggerPlugin.subscription_schema || [],
subscription_constructor: currentTriggerPlugin.subscription_constructor,
},
},
})
}
}, [currentTriggerPlugin, language, setDetail])
const dataSourceList = useStore(s => s.dataSourceList)
const currentDataSource = useMemo(() => {
if (data.type === BlockEnum.DataSource && data.provider_type !== DataSourceClassification.localFile)
return dataSourceList?.find(item => item.plugin_id === data.plugin_id)
}, [dataSourceList, data.plugin_id, data.type, data.provider_type])
}, [dataSourceList, data.provider_id, data.type, data.provider_type])
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
handleNodeDataUpdateWithSyncDraft({
id,
@ -280,15 +330,46 @@ const BasePanel: FC<BasePanelProps> = ({
},
})
}, [handleNodeDataUpdateWithSyncDraft, id])
const { setShowAccountSettingModal } = useModalContext()
const handleJumpToDataSourcePage = useCallback(() => {
setShowAccountSettingModal({ payload: 'data-source' })
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE })
}, [setShowAccountSettingModal])
const {
appendNodeInspectVars,
} = useInspectVarsCrud()
const handleSubscriptionChange = useCallback((v: SimpleSubscription, callback?: () => void) => {
handleNodeDataUpdateWithSyncDraft(
{ id, data: { subscription_id: v.id } },
{
sync: true,
callback: { onSettled: callback },
},
)
}, [handleNodeDataUpdateWithSyncDraft, id])
const readmeEntranceComponent = useMemo(() => {
let pluginDetail
switch (data.type) {
case BlockEnum.Tool:
pluginDetail = currToolCollection
break
case BlockEnum.DataSource:
pluginDetail = currentDataSource
break
case BlockEnum.TriggerPlugin:
pluginDetail = currentTriggerPlugin
break
default:
break
}
return !pluginDetail ? null : <ReadmeEntrance pluginDetail={pluginDetail as any} className='mt-auto' />
}, [data.type, currToolCollection, currentDataSource, currentTriggerPlugin])
if (logParams.showSpecialResultPanel) {
return (
<div className={cn(
@ -405,18 +486,10 @@ const BasePanel: FC<BasePanelProps> = ({
<div
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
onClick={() => {
if (isSingleRunning) {
handleNodeDataUpdate({
id,
data: {
_isSingleRun: false,
_singleRunningStatus: undefined,
},
})
}
else {
if (isSingleRunning)
handleStop()
else
handleSingleRun()
}
}}
>
{
@ -427,7 +500,6 @@ const BasePanel: FC<BasePanelProps> = ({
</Tooltip>
)
}
<NodePosition nodeId={id}></NodePosition>
<HelpLink nodeType={data.type} />
<PanelOperator id={id} data={data} showHelpLink={false} />
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular' />
@ -446,13 +518,14 @@ const BasePanel: FC<BasePanelProps> = ({
/>
</div>
{
showPluginAuth && (
needsToolAuth && (
<PluginAuth
className='px-4 pb-2'
pluginPayload={{
provider: currCollection?.name || '',
providerType: currCollection?.type || '',
provider: currToolCollection?.name || '',
providerType: currToolCollection?.type || '',
category: AuthCategory.tool,
detail: currToolCollection as any,
}}
>
<div className='flex items-center justify-between pl-4 pr-3'>
@ -462,9 +535,10 @@ const BasePanel: FC<BasePanelProps> = ({
/>
<AuthorizedInNode
pluginPayload={{
provider: currCollection?.name || '',
providerType: currCollection?.type || '',
provider: currToolCollection?.name || '',
providerType: currToolCollection?.type || '',
category: AuthCategory.tool,
detail: currToolCollection as any,
}}
onAuthorizationItemClick={handleAuthorizationItemClick}
credentialId={data.credential_id}
@ -493,7 +567,20 @@ const BasePanel: FC<BasePanelProps> = ({
)
}
{
!showPluginAuth && !currentDataSource && (
currentTriggerPlugin && (
<TriggerSubscription
subscriptionIdSelected={data.subscription_id}
onSubscriptionChange={handleSubscriptionChange}
>
<Tab
value={tabType}
onChange={setTabType}
/>
</TriggerSubscription>
)
}
{
!needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}
@ -505,7 +592,7 @@ const BasePanel: FC<BasePanelProps> = ({
<Split />
</div>
{tabType === TabType.settings && (
<div className='flex-1 overflow-y-auto'>
<div className='flex flex-1 flex-col overflow-y-auto'>
<div>
{cloneElement(children as any, {
id,
@ -550,6 +637,7 @@ const BasePanel: FC<BasePanelProps> = ({
</div>
)
}
{readmeEntranceComponent}
</div>
)}
@ -568,6 +656,7 @@ const BasePanel: FC<BasePanelProps> = ({
{...passedLogParams}
/>
)}
</div>
</div>
)

View File

@ -60,6 +60,19 @@ const LastRun: FC<Props> = ({
const noLastRun = (error as any)?.status === 404
const runResult = (canRunLastRun ? lastRunResult : singleRunResult) || lastRunResult || {}
const resolvedStatus = useMemo(() => {
if (isPaused)
return NodeRunningStatus.Stopped
if (oneStepRunRunningStatus === NodeRunningStatus.Stopped)
return NodeRunningStatus.Stopped
if (oneStepRunRunningStatus === NodeRunningStatus.Listening)
return NodeRunningStatus.Listening
return (runResult as any).status || otherResultPanelProps.status
}, [isPaused, oneStepRunRunningStatus, runResult, otherResultPanelProps.status])
const resetHidePageStatus = useCallback(() => {
setPageHasHide(false)
setPageShowed(false)
@ -104,18 +117,18 @@ const LastRun: FC<Props> = ({
if (isRunning)
return <ResultPanel status='running' showSteps={false} />
if (!isPaused && (noLastRun || !runResult)) {
return (
<NoData canSingleRun={canSingleRun} onSingleRun={onSingleRunClicked} />
)
}
return (
<div>
<ResultPanel
{...runResult as any}
{...otherResultPanelProps}
status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)}
status={resolvedStatus}
total_tokens={(runResult as any)?.execution_metadata?.total_tokens || otherResultPanelProps?.total_tokens}
created_by={(runResult as any)?.created_by_account?.created_by || otherResultPanelProps?.created_by}
nodeInfo={runResult as NodeTracing}

View File

@ -23,6 +23,7 @@ import useHumanInputSingleRunFormParams from '@/app/components/workflow/nodes/hu
import useKnowledgeBaseSingleRunFormParams from '@/app/components/workflow/nodes/knowledge-base/use-single-run-form-params'
import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more'
import useTriggerPluginGetDataForCheckMore from '@/app/components/workflow/nodes/trigger-plugin/use-check-params'
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
// import
@ -31,10 +32,12 @@ import { BlockEnum } from '@/app/components/workflow/types'
import {
useNodesSyncDraft,
} from '@/app/components/workflow/hooks'
import { useWorkflowRunValidation } from '@/app/components/workflow/hooks/use-checklist'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { useInvalidLastRun } from '@/service/use-workflow'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { isSupportCustomRunForm } from '@/app/components/workflow/utils'
import Toast from '@/app/components/base/toast'
const singleRunFormParamsHooks: Record<BlockEnum, any> = {
[BlockEnum.LLM]: useLLMSingleRunFormParams,
@ -64,6 +67,9 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
[BlockEnum.HumanInput]: useHumanInputSingleRunFormParams,
[BlockEnum.DataSource]: undefined,
[BlockEnum.DataSourceEmpty]: undefined,
[BlockEnum.TriggerWebhook]: undefined,
[BlockEnum.TriggerSchedule]: undefined,
[BlockEnum.TriggerPlugin]: undefined,
}
const useSingleRunFormParamsHooks = (nodeType: BlockEnum) => {
@ -100,6 +106,9 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
[BlockEnum.DataSource]: undefined,
[BlockEnum.DataSourceEmpty]: undefined,
[BlockEnum.KnowledgeBase]: undefined,
[BlockEnum.TriggerWebhook]: undefined,
[BlockEnum.TriggerSchedule]: undefined,
[BlockEnum.TriggerPlugin]: useTriggerPluginGetDataForCheckMore,
}
const useGetDataForCheckMoreHooks = <T>(nodeType: BlockEnum) => {
@ -142,6 +151,17 @@ const useLastRun = <T>({
isRunAfterSingleRun,
})
const { warningNodes } = useWorkflowRunValidation()
const blockIfChecklistFailed = useCallback(() => {
const warningForNode = warningNodes.find(item => item.id === id)
if (!warningForNode)
return false
const message = warningForNode.errorMessage || 'This node has unresolved checklist issues'
Toast.notify({ type: 'error', message })
return true
}, [warningNodes, id])
const {
hideSingleRun,
handleRun: doCallRunApi,
@ -202,7 +222,7 @@ const useLastRun = <T>({
})
}
const workflowStore = useWorkflowStore()
const { setInitShowLastRunTab } = workflowStore.getState()
const { setInitShowLastRunTab, setShowVariableInspectPanel } = workflowStore.getState()
const initShowLastRunTab = useStore(s => s.initShowLastRunTab)
const [tabType, setTabType] = useState<TabType>(initShowLastRunTab ? TabType.lastRun : TabType.settings)
useEffect(() => {
@ -214,6 +234,8 @@ const useLastRun = <T>({
const invalidLastRun = useInvalidLastRun(flowType, flowId, id)
const handleRunWithParams = async (data: Record<string, any>) => {
if (blockIfChecklistFailed())
return
const { isValid } = checkValid()
if (!isValid)
return
@ -312,9 +334,13 @@ const useLastRun = <T>({
}
const handleSingleRun = () => {
if (blockIfChecklistFailed())
return
const { isValid } = checkValid()
if (!isValid)
return
if (blockType === BlockEnum.TriggerWebhook || blockType === BlockEnum.TriggerPlugin || blockType === BlockEnum.TriggerSchedule)
setShowVariableInspectPanel(true)
if (isCustomRunNode) {
showSingleRun()
return

View File

@ -0,0 +1,26 @@
import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
import { CreateButtonType, CreateSubscriptionButton } from '@/app/components/plugins/plugin-detail-panel/subscription-list/create'
import { SubscriptionSelectorEntry } from '@/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry'
import { useSubscriptionList } from '@/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list'
import cn from '@/utils/classnames'
import type { FC } from 'react'
type TriggerSubscriptionProps = {
subscriptionIdSelected?: string
onSubscriptionChange: (v: SimpleSubscription, callback?: () => void) => void
children: React.ReactNode
}
export const TriggerSubscription: FC<TriggerSubscriptionProps> = ({ subscriptionIdSelected, onSubscriptionChange, children }) => {
const { subscriptions } = useSubscriptionList()
const subscriptionCount = subscriptions?.length || 0
return <div className={cn('px-4', subscriptionCount > 0 && 'flex items-center justify-between pr-3')}>
{!subscriptionCount && <CreateSubscriptionButton buttonType={CreateButtonType.FULL_BUTTON} />}
{children}
{subscriptionCount > 0 && <SubscriptionSelectorEntry
selectedId={subscriptionIdSelected}
onSelect={onSubscriptionChange}
/>}
</div>
}

View File

@ -10,7 +10,15 @@ import {
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
import {
BlockEnum,
InputVarType,
NodeRunningStatus,
VarType,
WorkflowRunningStatus,
} from '@/app/components/workflow/types'
import type { TriggerNodeType } from '@/app/components/workflow/types'
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { fetchNodeInspectVars, getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
import Toast from '@/app/components/base/toast'
@ -29,7 +37,7 @@ import IterationDefault from '@/app/components/workflow/nodes/iteration/default'
import DocumentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
import LoopDefault from '@/app/components/workflow/nodes/loop/default'
import HumanInputDefault from '@/app/components/workflow/nodes/human-input/default'
import { ssePost } from '@/service/base'
import { post, ssePost } from '@/service/base'
import { noop } from 'lodash-es'
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
import type { NodeRunResult, NodeTracing } from '@/types/workflow'
@ -52,9 +60,10 @@ import {
useStoreApi,
} from 'reactflow'
import { useInvalidLastRun } from '@/service/use-workflow'
import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import type { FlowType } from '@/types/common'
import useMatchSchemaType from '../components/variable/use-match-schema-type'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import {
useAllBuiltInTools,
useAllCustomTools,
@ -63,7 +72,7 @@ import {
} from '@/service/use-tools'
// eslint-disable-next-line ts/no-unsafe-function-type
const checkValidFns: Record<BlockEnum, Function> = {
const checkValidFns: Partial<Record<BlockEnum, Function>> = {
[BlockEnum.LLM]: checkLLMValid,
[BlockEnum.KnowledgeRetrieval]: checkKnowledgeRetrievalValid,
[BlockEnum.IfElse]: checkIfElseValid,
@ -79,7 +88,12 @@ const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.DocExtractor]: checkDocumentExtractorValid,
[BlockEnum.Loop]: checkLoopValid,
[BlockEnum.HumanInput]: checkHumanInputValid,
} as any
}
type RequestError = {
message: string
status: string
}
export type Params<T> = {
id: string
@ -201,7 +215,52 @@ const useOneStepRun = <T>({
const store = useStoreApi()
const {
setShowSingleRunPanel,
setIsListening,
setListeningTriggerType,
setListeningTriggerNodeId,
setListeningTriggerNodeIds,
setListeningTriggerIsAll,
setShowVariableInspectPanel,
} = workflowStore.getState()
const updateNodeInspectRunningState = useCallback((nodeId: string, isRunning: boolean) => {
const {
nodesWithInspectVars,
setNodesWithInspectVars,
} = workflowStore.getState()
let hasChanges = false
const nodes = produce(nodesWithInspectVars, (draft) => {
const index = draft.findIndex(node => node.nodeId === nodeId)
if (index !== -1) {
const targetNode = draft[index]
if (targetNode.isSingRunRunning !== isRunning) {
targetNode.isSingRunRunning = isRunning
if (isRunning)
targetNode.isValueFetched = false
hasChanges = true
}
}
else if (isRunning) {
const { getNodes } = store.getState()
const target = getNodes().find(node => node.id === nodeId)
if (target) {
draft.unshift({
nodeId,
nodeType: target.data.type,
title: target.data.title,
vars: [],
nodePayload: target.data,
isSingRunRunning: true,
isValueFetched: false,
})
hasChanges = true
}
}
})
if (hasChanges)
setNodesWithInspectVars(nodes)
}, [workflowStore, store])
const invalidLastRun = useInvalidLastRun(flowType, flowId!, id)
const [runResult, doSetRunResult] = useState<NodeRunResult | null>(null)
const {
@ -210,10 +269,26 @@ const useOneStepRun = <T>({
invalidateConversationVarValues,
} = useInspectVarsCrud()
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
const webhookSingleRunActiveRef = useRef(false)
const webhookSingleRunAbortRef = useRef<AbortController | null>(null)
const webhookSingleRunTimeoutRef = useRef<number | undefined>(undefined)
const webhookSingleRunTokenRef = useRef(0)
const webhookSingleRunDelayResolveRef = useRef<(() => void) | null>(null)
const pluginSingleRunActiveRef = useRef(false)
const pluginSingleRunAbortRef = useRef<AbortController | null>(null)
const pluginSingleRunTimeoutRef = useRef<number | undefined>(undefined)
const pluginSingleRunTokenRef = useRef(0)
const pluginSingleRunDelayResolveRef = useRef<(() => void) | null>(null)
const isPausedRef = useRef(isPaused)
useEffect(() => {
isPausedRef.current = isPaused
}, [isPaused])
const { eventEmitter } = useEventEmitterContextContext()
const isScheduleTriggerNode = data.type === BlockEnum.TriggerSchedule
const isWebhookTriggerNode = data.type === BlockEnum.TriggerWebhook
const isPluginTriggerNode = data.type === BlockEnum.TriggerPlugin
const isTriggerNode = isWebhookTriggerNode || isPluginTriggerNode || isScheduleTriggerNode
const setRunResult = useCallback(async (data: NodeRunResult | null) => {
const isPaused = isPausedRef.current
@ -233,13 +308,27 @@ const useOneStepRun = <T>({
const { getNodes } = store.getState()
const nodes = getNodes()
appendNodeInspectVars(id, vars, nodes)
updateNodeInspectRunningState(id, false)
if (data?.status === NodeRunningStatus.Succeeded) {
invalidLastRun()
if (isStartNode)
if (isStartNode || isTriggerNode)
invalidateSysVarValues()
invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh.
}
}, [isRunAfterSingleRun, runningStatus, flowId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues])
}, [
isRunAfterSingleRun,
runningStatus,
flowId,
id,
store,
appendNodeInspectVars,
updateNodeInspectRunningState,
invalidLastRun,
isStartNode,
isTriggerNode,
invalidateSysVarValues,
invalidateConversationVarValues,
])
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const setNodeRunning = () => {
@ -251,6 +340,299 @@ const useOneStepRun = <T>({
},
})
}
const cancelWebhookSingleRun = useCallback(() => {
webhookSingleRunActiveRef.current = false
webhookSingleRunTokenRef.current += 1
if (webhookSingleRunAbortRef.current)
webhookSingleRunAbortRef.current.abort()
webhookSingleRunAbortRef.current = null
if (webhookSingleRunTimeoutRef.current !== undefined) {
window.clearTimeout(webhookSingleRunTimeoutRef.current)
webhookSingleRunTimeoutRef.current = undefined
}
if (webhookSingleRunDelayResolveRef.current) {
webhookSingleRunDelayResolveRef.current()
webhookSingleRunDelayResolveRef.current = null
}
}, [])
const cancelPluginSingleRun = useCallback(() => {
pluginSingleRunActiveRef.current = false
pluginSingleRunTokenRef.current += 1
if (pluginSingleRunAbortRef.current)
pluginSingleRunAbortRef.current.abort()
pluginSingleRunAbortRef.current = null
if (pluginSingleRunTimeoutRef.current !== undefined) {
window.clearTimeout(pluginSingleRunTimeoutRef.current)
pluginSingleRunTimeoutRef.current = undefined
}
if (pluginSingleRunDelayResolveRef.current) {
pluginSingleRunDelayResolveRef.current()
pluginSingleRunDelayResolveRef.current = null
}
}, [])
const startTriggerListening = useCallback(() => {
if (!isTriggerNode)
return
setIsListening(true)
setShowVariableInspectPanel(true)
setListeningTriggerType(data.type as TriggerNodeType)
setListeningTriggerNodeId(id)
setListeningTriggerNodeIds([id])
setListeningTriggerIsAll(false)
}, [
isTriggerNode,
setIsListening,
setShowVariableInspectPanel,
setListeningTriggerType,
data.type,
setListeningTriggerNodeId,
id,
setListeningTriggerNodeIds,
setListeningTriggerIsAll,
])
const stopTriggerListening = useCallback(() => {
if (!isTriggerNode)
return
setIsListening(false)
setListeningTriggerType(null)
setListeningTriggerNodeId(null)
setListeningTriggerNodeIds([])
setListeningTriggerIsAll(false)
}, [
isTriggerNode,
setIsListening,
setListeningTriggerType,
setListeningTriggerNodeId,
setListeningTriggerNodeIds,
setListeningTriggerIsAll,
])
const runScheduleSingleRun = useCallback(async (): Promise<NodeRunResult | null> => {
const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run`
try {
const response: any = await post(urlPath, {
body: JSON.stringify({}),
})
if (!response) {
const message = 'Schedule trigger run failed'
Toast.notify({ type: 'error', message })
throw new Error(message)
}
if (response?.status === 'error') {
const message = response?.message || 'Schedule trigger run failed'
Toast.notify({ type: 'error', message })
throw new Error(message)
}
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Succeeded,
},
})
return response as NodeRunResult
}
catch (error) {
console.error('handleRun: schedule trigger single run error', error)
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Failed,
},
})
Toast.notify({ type: 'error', message: 'Schedule trigger run failed' })
throw error
}
}, [flowId, id, handleNodeDataUpdate, data])
const runWebhookSingleRun = useCallback(async (): Promise<any | null> => {
const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run`
webhookSingleRunActiveRef.current = true
const token = ++webhookSingleRunTokenRef.current
while (webhookSingleRunActiveRef.current && token === webhookSingleRunTokenRef.current) {
const controller = new AbortController()
webhookSingleRunAbortRef.current = controller
try {
const response: any = await post(urlPath, {
body: JSON.stringify({}),
signal: controller.signal,
})
if (!webhookSingleRunActiveRef.current || token !== webhookSingleRunTokenRef.current)
return null
if (!response) {
const message = response?.message || 'Webhook debug failed'
Toast.notify({ type: 'error', message })
cancelWebhookSingleRun()
throw new Error(message)
}
if (response?.status === 'waiting') {
const delay = Number(response.retry_in) || 2000
webhookSingleRunAbortRef.current = null
if (!webhookSingleRunActiveRef.current || token !== webhookSingleRunTokenRef.current)
return null
await new Promise<void>((resolve) => {
const timeoutId = window.setTimeout(resolve, delay)
webhookSingleRunTimeoutRef.current = timeoutId
webhookSingleRunDelayResolveRef.current = resolve
controller.signal.addEventListener('abort', () => {
window.clearTimeout(timeoutId)
resolve()
}, { once: true })
})
webhookSingleRunTimeoutRef.current = undefined
webhookSingleRunDelayResolveRef.current = null
continue
}
if (response?.status === 'error') {
const message = response.message || 'Webhook debug failed'
Toast.notify({ type: 'error', message })
cancelWebhookSingleRun()
throw new Error(message)
}
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Listening,
},
})
cancelWebhookSingleRun()
return response
}
catch (error) {
if (controller.signal.aborted && (!webhookSingleRunActiveRef.current || token !== webhookSingleRunTokenRef.current))
return null
if (controller.signal.aborted)
return null
Toast.notify({ type: 'error', message: 'Webhook debug request failed' })
cancelWebhookSingleRun()
if (error instanceof Error)
throw error
throw new Error(String(error))
}
finally {
webhookSingleRunAbortRef.current = null
}
}
return null
}, [flowId, id, data, handleNodeDataUpdate, cancelWebhookSingleRun])
const runPluginSingleRun = useCallback(async (): Promise<any | null> => {
const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run`
pluginSingleRunActiveRef.current = true
const token = ++pluginSingleRunTokenRef.current
while (pluginSingleRunActiveRef.current && token === pluginSingleRunTokenRef.current) {
const controller = new AbortController()
pluginSingleRunAbortRef.current = controller
let requestError: RequestError | undefined
const response: any = await post(urlPath, {
body: JSON.stringify({}),
signal: controller.signal,
}).catch(async (error: Response) => {
const data = await error.clone().json() as Record<string, any>
const { error: respError, status } = data || {}
requestError = {
message: respError,
status,
}
return null
}).finally(() => {
pluginSingleRunAbortRef.current = null
})
if (!pluginSingleRunActiveRef.current || token !== pluginSingleRunTokenRef.current)
return null
if (requestError) {
if (controller.signal.aborted)
return null
Toast.notify({ type: 'error', message: requestError.message })
cancelPluginSingleRun()
throw requestError
}
if (!response) {
const message = 'Plugin debug failed'
Toast.notify({ type: 'error', message })
cancelPluginSingleRun()
throw new Error(message)
}
if (response?.status === 'waiting') {
const delay = Number(response.retry_in) || 2000
if (!pluginSingleRunActiveRef.current || token !== pluginSingleRunTokenRef.current)
return null
await new Promise<void>((resolve) => {
const timeoutId = window.setTimeout(resolve, delay)
pluginSingleRunTimeoutRef.current = timeoutId
pluginSingleRunDelayResolveRef.current = resolve
controller.signal.addEventListener('abort', () => {
window.clearTimeout(timeoutId)
resolve()
}, { once: true })
})
pluginSingleRunTimeoutRef.current = undefined
pluginSingleRunDelayResolveRef.current = null
continue
}
if (response?.status === 'error') {
const message = response.message || 'Plugin debug failed'
Toast.notify({ type: 'error', message })
cancelPluginSingleRun()
throw new Error(message)
}
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Listening,
},
})
cancelPluginSingleRun()
return response
}
return null
}, [flowId, id, data, handleNodeDataUpdate, cancelPluginSingleRun])
const checkValidWrap = () => {
if (!checkValid)
return { isValid: true, errorMessage: '' }
@ -265,7 +647,7 @@ const useOneStepRun = <T>({
})
Toast.notify({
type: 'error',
message: res.errorMessage,
message: res.errorMessage || '',
})
}
return res
@ -312,33 +694,84 @@ const useOneStepRun = <T>({
const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
const handleRun = async (submitData: Record<string, any>) => {
if (isWebhookTriggerNode)
cancelWebhookSingleRun()
if (isPluginTriggerNode)
cancelPluginSingleRun()
updateNodeInspectRunningState(id, true)
if (isTriggerNode)
startTriggerListening()
else
stopTriggerListening()
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Running,
_singleRunningStatus: isTriggerNode
? NodeRunningStatus.Listening
: NodeRunningStatus.Running,
},
})
let res: any
let hasError = false
try {
if (!isIteration && !isLoop) {
const isStartNode = data.type === BlockEnum.Start
const postData: Record<string, any> = {}
if (isStartNode) {
const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData
if (isChatMode)
postData.conversation_id = ''
postData.inputs = inputs
postData.query = query
postData.files = files || []
if (isScheduleTriggerNode) {
res = await runScheduleSingleRun()
}
else if (isWebhookTriggerNode) {
res = await runWebhookSingleRun()
if (!res) {
if (webhookSingleRunActiveRef.current) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Stopped,
},
})
}
return false
}
}
else if (isPluginTriggerNode) {
res = await runPluginSingleRun()
if (!res) {
if (pluginSingleRunActiveRef.current) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Stopped,
},
})
}
return false
}
}
else {
postData.inputs = submitData
const isStartNode = data.type === BlockEnum.Start
const postData: Record<string, any> = {}
if (isStartNode) {
const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData
if (isChatMode)
postData.conversation_id = ''
postData.inputs = inputs
postData.query = query
postData.files = files || []
}
else {
postData.inputs = submitData
}
res = await singleNodeRun(flowType, flowId!, id, postData) as any
}
res = await singleNodeRun(flowType, flowId!, id, postData) as any
}
else if (isIteration) {
setIterationRunResult([])
@ -569,6 +1002,14 @@ const useOneStepRun = <T>({
}
}
finally {
if (isWebhookTriggerNode)
cancelWebhookSingleRun()
if (isPluginTriggerNode)
cancelPluginSingleRun()
if (isTriggerNode)
stopTriggerListening()
if (!isIteration && !isLoop)
updateNodeInspectRunningState(id, false)
if (!isPausedRef.current && !isIteration && !isLoop && res) {
setRunResult({
...res,
@ -594,15 +1035,55 @@ const useOneStepRun = <T>({
}
}
const handleStop = () => {
const handleStop = useCallback(() => {
if (isTriggerNode) {
const isTriggerActive = runningStatus === NodeRunningStatus.Listening
|| webhookSingleRunActiveRef.current
|| pluginSingleRunActiveRef.current
if (!isTriggerActive)
return
}
else if (runningStatus !== NodeRunningStatus.Running) {
return
}
cancelWebhookSingleRun()
cancelPluginSingleRun()
handleNodeDataUpdate({
id,
data: {
...data,
_singleRunningStatus: NodeRunningStatus.NotStart,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Stopped,
},
})
}
stopTriggerListening()
updateNodeInspectRunningState(id, false)
const {
workflowRunningData,
setWorkflowRunningData,
nodesWithInspectVars,
deleteNodeInspectVars,
} = workflowStore.getState()
if (workflowRunningData) {
setWorkflowRunningData(produce(workflowRunningData, (draft) => {
draft.result.status = WorkflowRunningStatus.Stopped
}))
}
const inspectNode = nodesWithInspectVars.find(node => node.nodeId === id)
if (inspectNode && !inspectNode.isValueFetched && (!inspectNode.vars || inspectNode.vars.length === 0))
deleteNodeInspectVars(id)
}, [
isTriggerNode,
runningStatus,
cancelWebhookSingleRun,
cancelPluginSingleRun,
handleNodeDataUpdate,
id,
stopTriggerListening,
updateNodeInspectRunningState,
workflowStore,
])
const toVarInputs = (variables: Variable[]): InputVar[] => {
if (!variables)
@ -665,6 +1146,11 @@ const useOneStepRun = <T>({
})
}
eventEmitter?.useSubscription((v: any) => {
if (v.type === EVENT_WORKFLOW_STOP)
handleStop()
})
return {
isShowSingleRun,
hideSingleRun,

View File

@ -16,23 +16,18 @@ import {
RiLoader2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { NodeProps } from '../../types'
import type { NodeProps } from '@/app/components/workflow/types'
import {
BlockEnum,
NodeRunningStatus,
} from '../../types'
import {
useNodesReadOnly,
useToolIcon,
} from '../../hooks'
import {
hasErrorHandleNode,
hasRetryNode,
} from '../../utils'
import { useNodeIterationInteractions } from '../iteration/use-interactions'
import { useNodeLoopInteractions } from '../loop/use-interactions'
import type { IterationNodeType } from '../iteration/types'
import CopyID from '../tool/components/copy-id'
isTriggerNode,
} from '@/app/components/workflow/types'
import { useNodesReadOnly, useToolIcon } from '@/app/components/workflow/hooks'
import { hasErrorHandleNode, hasRetryNode } from '@/app/components/workflow/utils'
import { useNodeIterationInteractions } from '@/app/components/workflow/nodes/iteration/use-interactions'
import { useNodeLoopInteractions } from '@/app/components/workflow/nodes/loop/use-interactions'
import type { IterationNodeType } from '@/app/components/workflow/nodes/iteration/types'
import CopyID from '@/app/components/workflow/nodes/tool/components/copy-id'
import {
NodeSourceHandle,
NodeTargetHandle,
@ -42,11 +37,12 @@ import NodeControl from './components/node-control'
import ErrorHandleOnNode from './components/error-handle/error-handle-on-node'
import RetryOnNode from './components/retry/retry-on-node'
import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
import EntryNodeContainer, { StartNodeTypeEnum } from './components/entry-node-container'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
import Tooltip from '@/app/components/base/tooltip'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
import { ToolTypeEnum } from '../../block-selector/types'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { ToolTypeEnum } from '@/app/components/workflow/block-selector/types'
type NodeChildProps = {
id: string
@ -67,6 +63,7 @@ const BaseNode: FC<BaseNodeProps> = ({
const { t } = useTranslation()
const nodeRef = useRef<HTMLDivElement>(null)
const { nodesReadOnly } = useNodesReadOnly()
const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions()
const { handleNodeLoopChildSizeChange } = useNodeLoopInteractions()
const toolIcon = useToolIcon(data)
@ -141,13 +138,13 @@ const BaseNode: FC<BaseNodeProps> = ({
return null
}, [data._loopIndex, data._runningStatus, t])
return (
const nodeContent = (
<div
className={cn(
'relative flex rounded-2xl border',
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
data._waitingRun && 'opacity-70',
data._dimmed && 'opacity-30',
data._pluginInstallLocked && 'cursor-not-allowed',
)}
ref={nodeRef}
style={{
@ -155,6 +152,17 @@ const BaseNode: FC<BaseNodeProps> = ({
height: (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) ? data.height : 'auto',
}}
>
{(data._dimmed || data._pluginInstallLocked) && (
<div
className={cn(
'absolute inset-0 rounded-2xl transition-opacity',
data._pluginInstallLocked
? 'pointer-events-auto z-30 bg-workflow-block-parma-bg opacity-80 backdrop-blur-[2px]'
: 'pointer-events-none z-20 bg-workflow-block-parma-bg opacity-50',
)}
data-testid='workflow-node-install-overlay'
/>
)}
{
data.type === BlockEnum.DataSource && (
<div className='absolute inset-[-2px] top-[-22px] z-[-1] rounded-[18px] bg-node-data-source-bg p-0.5 backdrop-blur-[6px]'>
@ -297,13 +305,13 @@ const BaseNode: FC<BaseNodeProps> = ({
</div>
{
data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop && (
cloneElement(children, { id, data })
cloneElement(children, { id, data } as any)
)
}
{
(data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) && (
<div className='grow pb-1 pl-1 pr-1'>
{cloneElement(children, { id, data })}
{cloneElement(children, { id, data } as any)}
</div>
)
}
@ -338,6 +346,17 @@ const BaseNode: FC<BaseNodeProps> = ({
</div>
</div>
)
const isStartNode = data.type === BlockEnum.Start
const isEntryNode = isTriggerNode(data.type as any) || isStartNode
return isEntryNode ? (
<EntryNodeContainer
nodeType={isStartNode ? StartNodeTypeEnum.Start : StartNodeTypeEnum.Trigger}
>
{nodeContent}
</EntryNodeContainer>
) : nodeContent
}
export default memo(BaseNode)

View File

@ -0,0 +1,27 @@
import type { ValueSelector } from '@/app/components/workflow/types'
// Generic variable types for all resource forms
export enum VarKindType {
variable = 'variable',
constant = 'constant',
mixed = 'mixed',
}
// Generic resource variable inputs
export type ResourceVarInputs = Record<string, {
type: VarKindType
value?: string | ValueSelector | any
}>
// Base resource interface
export type BaseResource = {
name: string
[key: string]: any
}
// Base resource provider interface
export type BaseResourceProvider = {
plugin_id?: string
name: string
[key: string]: any
}