浏览代码

feat: add DYNAMIC_SELECT parameter type for dynamic options in parameter entities (#21425)

tags/1.5.1
Yeuoly 4 个月前
父节点
当前提交
cea6522122
没有帐户链接到提交者的电子邮件

+ 39
- 0
api/controllers/console/workspace/plugin.py 查看文件

from core.plugin.impl.exc import PluginDaemonClientSideError from core.plugin.impl.exc import PluginDaemonClientSideError
from libs.login import login_required from libs.login import login_required
from models.account import TenantPluginPermission from models.account import TenantPluginPermission
from services.plugin.plugin_parameter_service import PluginParameterService
from services.plugin.plugin_permission_service import PluginPermissionService from services.plugin.plugin_permission_service import PluginPermissionService
from services.plugin.plugin_service import PluginService from services.plugin.plugin_service import PluginService


) )




class PluginFetchDynamicSelectOptionsApi(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self):
# check if the user is admin or owner
if not current_user.is_admin_or_owner:
raise Forbidden()

tenant_id = current_user.current_tenant_id
user_id = current_user.id

parser = reqparse.RequestParser()
parser.add_argument("plugin_id", type=str, required=True, location="args")
parser.add_argument("provider", type=str, required=True, location="args")
parser.add_argument("action", type=str, required=True, location="args")
parser.add_argument("parameter", type=str, required=True, location="args")
parser.add_argument("provider_type", type=str, required=True, location="args")
args = parser.parse_args()

try:
options = PluginParameterService.get_dynamic_select_options(
tenant_id,
user_id,
args["plugin_id"],
args["provider"],
args["action"],
args["parameter"],
args["provider_type"],
)
except PluginDaemonClientSideError as e:
raise ValueError(e)

return jsonable_encoder({"options": options})


api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key")
api.add_resource(PluginListApi, "/workspaces/current/plugin/list") api.add_resource(PluginListApi, "/workspaces/current/plugin/list")
api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions")


api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change") api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change")
api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch")

api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options")

+ 5
- 0
api/core/entities/parameter_entities.py 查看文件

MODEL_SELECTOR = "model-selector" MODEL_SELECTOR = "model-selector"
TOOLS_SELECTOR = "array[tools]" TOOLS_SELECTOR = "array[tools]"


# Dynamic select parameter
# Once you are not sure about the available options until authorization is done
# eg: Select a Slack channel from a Slack workspace
DYNAMIC_SELECT = "dynamic-select"

# TOOL_SELECTOR = "tool-selector" # TOOL_SELECTOR = "tool-selector"





+ 1
- 0
api/core/plugin/entities/parameters.py 查看文件

APP_SELECTOR = CommonParameterType.APP_SELECTOR.value APP_SELECTOR = CommonParameterType.APP_SELECTOR.value
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value
DYNAMIC_SELECT = CommonParameterType.DYNAMIC_SELECT.value


# deprecated, should not use. # deprecated, should not use.
SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value

+ 6
- 1
api/core/plugin/entities/plugin_daemon.py 查看文件

from collections.abc import Mapping
from collections.abc import Mapping, Sequence
from datetime import datetime from datetime import datetime
from enum import StrEnum from enum import StrEnum
from typing import Any, Generic, Optional, TypeVar from typing import Any, Generic, Optional, TypeVar
from core.model_runtime.entities.model_entities import AIModelEntity from core.model_runtime.entities.model_entities import AIModelEntity
from core.model_runtime.entities.provider_entities import ProviderEntity from core.model_runtime.entities.provider_entities import ProviderEntity
from core.plugin.entities.base import BasePluginEntity from core.plugin.entities.base import BasePluginEntity
from core.plugin.entities.parameters import PluginParameterOption
from core.plugin.entities.plugin import PluginDeclaration, PluginEntity from core.plugin.entities.plugin import PluginDeclaration, PluginEntity
from core.tools.entities.common_entities import I18nObject from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin
class PluginListResponse(BaseModel): class PluginListResponse(BaseModel):
list: list[PluginEntity] list: list[PluginEntity]
total: int total: int


class PluginDynamicSelectOptionsResponse(BaseModel):
options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.")

+ 45
- 0
api/core/plugin/impl/dynamic_select.py 查看文件

from collections.abc import Mapping
from typing import Any

from core.plugin.entities.plugin import GenericProviderID
from core.plugin.entities.plugin_daemon import PluginDynamicSelectOptionsResponse
from core.plugin.impl.base import BasePluginClient


class DynamicSelectClient(BasePluginClient):
def fetch_dynamic_select_options(
self,
tenant_id: str,
user_id: str,
plugin_id: str,
provider: str,
action: str,
credentials: Mapping[str, Any],
parameter: str,
) -> PluginDynamicSelectOptionsResponse:
"""
Fetch dynamic select options for a plugin parameter.
"""
response = self._request_with_plugin_daemon_response_stream(
"POST",
f"plugin/{tenant_id}/dispatch/dynamic_select/fetch_parameter_options",
PluginDynamicSelectOptionsResponse,
data={
"user_id": user_id,
"data": {
"provider": GenericProviderID(provider).provider_name,
"credentials": credentials,
"provider_action": action,
"parameter": parameter,
},
},
headers={
"X-Plugin-ID": plugin_id,
"Content-Type": "application/json",
},
)

for options in response:
return options

raise ValueError("Plugin service returned no options")

+ 1
- 0
api/core/tools/entities/tool_entities.py 查看文件

FILES = PluginParameterType.FILES.value FILES = PluginParameterType.FILES.value
APP_SELECTOR = PluginParameterType.APP_SELECTOR.value APP_SELECTOR = PluginParameterType.APP_SELECTOR.value
MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value
DYNAMIC_SELECT = PluginParameterType.DYNAMIC_SELECT.value


# deprecated, should not use. # deprecated, should not use.
SYSTEM_FILES = PluginParameterType.SYSTEM_FILES.value SYSTEM_FILES = PluginParameterType.SYSTEM_FILES.value

+ 1
- 0
api/core/tools/utils/configuration.py 查看文件

cached_credentials = cache.get() cached_credentials = cache.get()
if cached_credentials: if cached_credentials:
return cached_credentials return cached_credentials

data = self._deep_copy(data) data = self._deep_copy(data)
# get fields need to be decrypted # get fields need to be decrypted
fields = dict[str, BasicProviderConfig]() fields = dict[str, BasicProviderConfig]()

+ 74
- 0
api/services/plugin/plugin_parameter_service.py 查看文件

from collections.abc import Mapping, Sequence
from typing import Any, Literal

from sqlalchemy.orm import Session

from core.plugin.entities.parameters import PluginParameterOption
from core.plugin.impl.dynamic_select import DynamicSelectClient
from core.tools.tool_manager import ToolManager
from core.tools.utils.configuration import ProviderConfigEncrypter
from extensions.ext_database import db
from models.tools import BuiltinToolProvider


class PluginParameterService:
@staticmethod
def get_dynamic_select_options(
tenant_id: str,
user_id: str,
plugin_id: str,
provider: str,
action: str,
parameter: str,
provider_type: Literal["tool"],
) -> Sequence[PluginParameterOption]:
"""
Get dynamic select options for a plugin parameter.

Args:
tenant_id: The tenant ID.
plugin_id: The plugin ID.
provider: The provider name.
action: The action name.
parameter: The parameter name.
"""
credentials: Mapping[str, Any] = {}

match provider_type:
case "tool":
provider_controller = ToolManager.get_builtin_provider(provider, tenant_id)
# init tool configuration
tool_configuration = ProviderConfigEncrypter(
tenant_id=tenant_id,
config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()],
provider_type=provider_controller.provider_type.value,
provider_identity=provider_controller.entity.identity.name,
)

# check if credentials are required
if not provider_controller.need_credentials:
credentials = {}
else:
# fetch credentials from db
with Session(db.engine) as session:
db_record = (
session.query(BuiltinToolProvider)
.filter(
BuiltinToolProvider.tenant_id == tenant_id,
BuiltinToolProvider.provider == provider,
)
.first()
)

if db_record is None:
raise ValueError(f"Builtin provider {provider} not found when fetching credentials")

credentials = tool_configuration.decrypt(db_record.credentials)
case _:
raise ValueError(f"Invalid provider type: {provider_type}")

return (
DynamicSelectClient()
.fetch_dynamic_select_options(tenant_id, user_id, plugin_id, provider, action, credentials, parameter)
.options
)

+ 20
- 6
web/app/components/base/select/index.tsx 查看文件

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react' import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid' import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid'
import Badge from '../badge/index' import Badge from '../badge/index'
import { RiCheckLine } from '@remixicon/react'
import { RiCheckLine, RiLoader4Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { import {
item: Item item: Item
selected: boolean selected: boolean
}) => React.ReactNode }) => React.ReactNode
isLoading?: boolean
onOpenChange?: (open: boolean) => void
} }
const Select: FC<ISelectProps> = ({ const Select: FC<ISelectProps> = ({
className, className,
defaultValue = 1, defaultValue = 1,
disabled = false, disabled = false,
onSelect, onSelect,
onOpenChange,
placeholder, placeholder,
optionWrapClassName, optionWrapClassName,
optionClassName, optionClassName,
hideChecked, hideChecked,
notClearable, notClearable,
renderOption, renderOption,
isLoading = false,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const localPlaceholder = placeholder || t('common.placeholder.select') const localPlaceholder = placeholder || t('common.placeholder.select')


const [selectedItem, setSelectedItem] = useState<Item | null>(null) const [selectedItem, setSelectedItem] = useState<Item | null>(null)

useEffect(() => { useEffect(() => {
let defaultSelect = null let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue) const existed = items.find((item: Item) => item.value === defaultValue)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValue]) }, [defaultValue])


const listboxRef = useRef<HTMLDivElement>(null)

return ( return (
<Listbox
<Listbox ref={listboxRef}
value={selectedItem} value={selectedItem}
onChange={(value: Item) => { onChange={(value: Item) => {
if (!disabled) { if (!disabled) {
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}> <div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
{renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>} {renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>}
{!renderTrigger && ( {!renderTrigger && (
<ListboxButton className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
<ListboxButton onClick={() => {
// get data-open, use setTimeout to ensure the attribute is set
setTimeout(() => {
if (listboxRef.current)
onOpenChange?.(listboxRef.current.getAttribute('data-open') !== null)
})
}} className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
<span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span> <span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2"> <span className="absolute inset-y-0 right-0 flex items-center pr-2">
{(selectedItem && !notClearable)
{isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
: (selectedItem && !notClearable)
? ( ? (
<XMarkIcon <XMarkIcon
onClick={(e) => { onClick={(e) => {
</ListboxButton> </ListboxButton>
)} )}


{!disabled && (
{(!disabled) && (
<ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-xl bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}> <ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-xl bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => ( {items.map((item: Item) => (
<ListboxOption <ListboxOption

+ 2
- 0
web/app/components/header/account-setting/model-provider-page/declarations.ts 查看文件

toolSelector = 'tool-selector', toolSelector = 'tool-selector',
multiToolSelector = 'array[tools]', multiToolSelector = 'array[tools]',
appSelector = 'app-selector', appSelector = 'app-selector',
dynamicSelect = 'dynamic-select',
} }


export type FormOption = { export type FormOption = {
label: TypeWithI18N label: TypeWithI18N
value: string value: string
show_on: FormShowOnObject[] show_on: FormShowOnObject[]
icon?: string
} }


export enum ModelTypeEnum { export enum ModelTypeEnum {

+ 7
- 1
web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx 查看文件

readonly: boolean readonly: boolean
value: string value: string
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
onOpenChange?: (open: boolean) => void
isLoading?: boolean
} }


const DEFAULT_SCHEMA = {} as CredentialFormSchema const DEFAULT_SCHEMA = {} as CredentialFormSchema
readonly, readonly,
value, value,
onChange, onChange,
onOpenChange,
isLoading,
}) => { }) => {
const language = useLanguage() const language = useLanguage()
const placeholder = (schema as CredentialFormSchemaSelect).placeholder const placeholder = (schema as CredentialFormSchemaSelect).placeholder


return ( return (
<> <>
{schema.type === FormTypeEnum.select && (
{(schema.type === FormTypeEnum.select || schema.type === FormTypeEnum.dynamicSelect) && (
<SimpleSelect <SimpleSelect
wrapperClassName='w-full !h-8' wrapperClassName='w-full !h-8'
className='flex items-center' className='flex items-center'
items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))} items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleSelectChange(item.value)} onSelect={item => handleSelectChange(item.value)}
placeholder={placeholder?.[language] || placeholder?.en_US} placeholder={placeholder?.[language] || placeholder?.en_US}
onOpenChange={onOpenChange}
isLoading={isLoading}
/> />
)} )}
{schema.type === FormTypeEnum.textNumber && ( {schema.type === FormTypeEnum.textNumber && (

+ 59
- 4
web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx 查看文件

RiArrowDownSLine, RiArrowDownSLine,
RiCloseLine, RiCloseLine,
RiErrorWarningFill, RiErrorWarningFill,
RiLoader4Line,
RiMoreLine, RiMoreLine,
} from '@remixicon/react' } from '@remixicon/react'
import produce from 'immer' import produce from 'immer'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils' import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils'
import ConstantField from './constant-field' import ConstantField from './constant-field'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/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' import { BlockEnum } from '@/app/components/workflow/types'
import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common' import { Line3 } from '@/app/components/base/icons/src/public/common'
import { isExceptionVariable } from '@/app/components/workflow/utils' import { isExceptionVariable } from '@/app/components/workflow/utils'
import VarFullPathPanel from './var-full-path-panel' import VarFullPathPanel from './var-full-path-panel'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import type { Tool } from '@/app/components/tools/types'


const TRIGGER_DEFAULT_WIDTH = 227 const TRIGGER_DEFAULT_WIDTH = 227


minWidth?: number minWidth?: number
popupFor?: 'assigned' | 'toAssigned' popupFor?: 'assigned' | 'toAssigned'
zIndex?: number zIndex?: number
currentTool?: Tool
currentProvider?: ToolWithProvider
} }


const DEFAULT_VALUE_SELECTOR: Props['value'] = [] const DEFAULT_VALUE_SELECTOR: Props['value'] = []
minWidth, minWidth,
popupFor, popupFor,
zIndex, zIndex,
currentTool,
currentProvider,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const store = useStoreApi() const store = useStoreApi()


return null return null
}, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type]) }, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type])

const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
const [isLoading, setIsLoading] = useState(false)
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '', currentProvider?.name || '', currentTool?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
'tool',
)
const handleFetchDynamicOptions = async () => {
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider)
return
setIsLoading(true)
try {
const data = await fetchDynamicOptions()
setDynamicOptions(data?.options || [])
}
finally {
setIsLoading(false)
}
}
useEffect(() => {
handleFetchDynamicOptions()
}, [currentTool, currentProvider, schema])

const schemaWithDynamicSelect = useMemo(() => {
if (schema?.type !== FormTypeEnum.dynamicSelect)
return schema
// rewrite schema.options with dynamicOptions
if (dynamicOptions) {
return {
...schema,
options: dynamicOptions,
}
}
return schema
}, [dynamicOptions])

return ( return (
<div className={cn(className, !readonly && 'cursor-pointer')}> <div className={cn(className, !readonly && 'cursor-pointer')}>
<PortalToFollowElem <PortalToFollowElem
<ConstantField <ConstantField
value={value as string} value={value as string}
onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)} onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)}
schema={schema as CredentialFormSchema}
schema={schemaWithDynamicSelect as CredentialFormSchema}
readonly={readonly} readonly={readonly}
isLoading={isLoading}
/> />
) )
: ( : (
)} )}
<div className='flex items-center text-text-accent'> <div className='flex items-center text-text-accent'>
{!hasValue && <Variable02 className='h-3.5 w-3.5' />} {!hasValue && <Variable02 className='h-3.5 w-3.5' />}
{isLoading && <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />}
{isEnv && <Env className='h-3.5 w-3.5 text-util-colors-violet-violet-600' />} {isEnv && <Env className='h-3.5 w-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />} {isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
<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')} title={varName} style={{
{!isValidVar && <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' />} {!isValidVar && <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' />}
</> </>
) )
: <div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>{placeholder ?? t('workflow.common.setVarValuePlaceholder')}</div>}
: <div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>
{isLoading ? (
<div className='flex items-center'>
<RiLoader4Line className='mr-1 h-3.5 w-3.5 animate-spin text-text-secondary' />
<span>{placeholder ?? t('workflow.common.setVarValuePlaceholder')}</span>
</div>
) : (
placeholder ?? t('workflow.common.setVarValuePlaceholder')
)}
</div>}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>

+ 14
- 3
web/app/components/workflow/nodes/tool/components/input-var-list.tsx 查看文件

import type { ToolVarInputs } from '../types' import type { ToolVarInputs } from '../types'
import { VarType as VarKindType } from '../types' import { VarType as VarKindType } from '../types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import type { Tool } from '@/app/components/tools/types'


type Props = { type Props = {
readOnly: boolean readOnly: boolean
onOpen?: (index: number) => void onOpen?: (index: number) => void
isSupportConstantValue?: boolean isSupportConstantValue?: boolean
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
} }


const InputVarList: FC<Props> = ({ const InputVarList: FC<Props> = ({
onOpen = noop, onOpen = noop,
isSupportConstantValue, isSupportConstantValue,
filterVar, filterVar,
currentTool,
currentProvider,
}) => { }) => {
const language = useLanguage() const language = useLanguage()
const { t } = useTranslation() const { t } = useTranslation()
return 'ModelSelector' return 'ModelSelector'
else if (type === FormTypeEnum.toolSelector) else if (type === FormTypeEnum.toolSelector)
return 'ToolSelector' return 'ToolSelector'
else if (type === FormTypeEnum.dynamicSelect || type === FormTypeEnum.select)
return 'Select'
else else
return 'String' return 'String'
} }
const handleOpen = useCallback((index: number) => { const handleOpen = useCallback((index: number) => {
return () => onOpen(index) return () => onOpen(index)
}, [onOpen]) }, [onOpen])

return ( return (
<div className='space-y-3'> <div className='space-y-3'>
{ {
} = schema } = schema
const varInput = value[variable] const varInput = value[variable]
const isNumber = type === FormTypeEnum.textNumber const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isAppSelector = type === FormTypeEnum.appSelector const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector const isModelSelector = type === FormTypeEnum.modelSelector
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])} value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)} onChange={handleNotMixedTypeChange(variable)}
onOpen={handleOpen(index)} onOpen={handleOpen(index)}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}
defaultVarKindType={varInput?.type || ((isNumber || isDynamicSelect) ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue={isSupportConstantValue} isSupportConstantValue={isSupportConstantValue}
filterVar={isNumber ? filterVar : undefined} filterVar={isNumber ? filterVar : undefined}
availableVars={isSelect ? availableVars : undefined} availableVars={isSelect ? availableVars : undefined}
schema={schema} schema={schema}
currentTool={currentTool}
currentProvider={currentProvider}
/> />
)} )}
{isFile && ( {isFile && (

+ 3
- 0
web/app/components/workflow/nodes/tool/panel.tsx 查看文件

isLoading, isLoading,
outputSchema, outputSchema,
hasObjectOutput, hasObjectOutput,
currTool,
} = useConfig(id, data) } = useConfig(id, data)


if (isLoading) { if (isLoading) {
filterVar={filterVar} filterVar={filterVar}
isSupportConstantValue isSupportConstantValue
onOpen={handleOnVarOpen} onOpen={handleOnVarOpen}
currentProvider={currCollection}
currentTool={currTool}
/> />
</Field> </Field>
)} )}

+ 16
- 1
web/service/use-plugins.ts 查看文件

import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import type { import type {
FormOption,
ModelProvider, ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fetchModelProviderModelList } from '@/service/common' import { fetchModelProviderModelList } from '@/service/common'
refreshPluginList(category ? { category } as any : undefined, !category) refreshPluginList(category ? { category } as any : undefined, !category)
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRefetching]) }, [isRefetching])


const handleRefetch = useCallback(() => { const handleRefetch = useCallback(() => {
enabled: !!providerName, enabled: !!providerName,
}) })
} }

export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type: 'tool') => {
return useMutation({
mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
params: {
plugin_id,
provider,
action,
parameter,
provider_type,
},
}),
})
}

正在加载...
取消
保存