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

This commit is contained in:
Yeuoly 2025-06-26 17:44:14 +08:00 committed by GitHub
parent ae00ba44db
commit cea6522122
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 293 additions and 16 deletions

View File

@ -13,6 +13,7 @@ from core.model_runtime.utils.encoders import jsonable_encoder
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
@ -497,6 +498,42 @@ class PluginFetchPermissionApi(Resource):
) )
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")
@ -521,3 +558,5 @@ api.add_resource(PluginFetchMarketplacePkgApi, "/workspaces/current/plugin/marke
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")

View File

@ -15,6 +15,11 @@ class CommonParameterType(StrEnum):
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"

View File

@ -35,6 +35,7 @@ class PluginParameterType(enum.StrEnum):
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

View File

@ -1,4 +1,4 @@
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
@ -9,6 +9,7 @@ from core.agent.plugin_entities import AgentProviderEntityWithPlugin
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
@ -186,3 +187,7 @@ class PluginOAuthCredentialsResponse(BaseModel):
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.")

View File

@ -0,0 +1,45 @@
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")

View File

@ -240,6 +240,7 @@ class ToolParameter(PluginParameter):
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

View File

@ -86,6 +86,7 @@ class ProviderConfigEncrypter(BaseModel):
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]()

View File

@ -0,0 +1,74 @@
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
)

View File

@ -1,10 +1,10 @@
'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 {
@ -51,6 +51,8 @@ export type ISelectProps = {
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,
@ -178,17 +180,20 @@ const SimpleSelect: FC<ISelectProps> = ({
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)
@ -199,8 +204,10 @@ const SimpleSelect: FC<ISelectProps> = ({
// 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) {
@ -212,10 +219,17 @@ const SimpleSelect: FC<ISelectProps> = ({
<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) => {
@ -237,7 +251,7 @@ const SimpleSelect: FC<ISelectProps> = ({
</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

View File

@ -19,12 +19,14 @@ export enum FormTypeEnum {
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 {

View File

@ -13,6 +13,8 @@ type Props = {
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
@ -22,6 +24,8 @@ const ConstantField: FC<Props> = ({
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
@ -36,7 +40,7 @@ const ConstantField: FC<Props> = ({
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'
@ -45,6 +49,8 @@ const ConstantField: FC<Props> = ({
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 && (

View File

@ -6,6 +6,7 @@ import {
RiArrowDownSLine, RiArrowDownSLine,
RiCloseLine, RiCloseLine,
RiErrorWarningFill, RiErrorWarningFill,
RiLoader4Line,
RiMoreLine, RiMoreLine,
} from '@remixicon/react' } from '@remixicon/react'
import produce from 'immer' import produce from 'immer'
@ -16,8 +17,9 @@ import VarReferencePopup from './var-reference-popup'
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 { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' 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'
@ -40,6 +42,8 @@ import Tooltip from '@/app/components/base/tooltip'
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
@ -68,6 +72,8 @@ type Props = {
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'] = []
@ -97,6 +103,8 @@ const VarReferencePicker: FC<Props> = ({
minWidth, minWidth,
popupFor, popupFor,
zIndex, zIndex,
currentTool,
currentProvider,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const store = useStoreApi() const store = useStoreApi()
@ -316,6 +324,42 @@ const VarReferencePicker: FC<Props> = ({
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
@ -366,8 +410,9 @@ const VarReferencePicker: FC<Props> = ({
<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}
/> />
) )
: ( : (
@ -412,6 +457,7 @@ const VarReferencePicker: FC<Props> = ({
)} )}
<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={{
@ -424,7 +470,16 @@ const VarReferencePicker: FC<Props> = ({
{!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>

View File

@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
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'
@ -17,6 +17,7 @@ import { VarType } from '@/app/components/workflow/types'
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
@ -27,6 +28,8 @@ type Props = {
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> = ({
@ -38,6 +41,8 @@ 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()
@ -58,6 +63,8 @@ const InputVarList: FC<Props> = ({
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'
} }
@ -149,6 +156,7 @@ const InputVarList: FC<Props> = ({
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'>
{ {
@ -163,7 +171,8 @@ const InputVarList: FC<Props> = ({
} = 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
@ -198,11 +207,13 @@ const InputVarList: FC<Props> = ({
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 && (

View File

@ -42,6 +42,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
isLoading, isLoading,
outputSchema, outputSchema,
hasObjectOutput, hasObjectOutput,
currTool,
} = useConfig(id, data) } = useConfig(id, data)
if (isLoading) { if (isLoading) {
@ -80,6 +81,8 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
filterVar={filterVar} filterVar={filterVar}
isSupportConstantValue isSupportConstantValue
onOpen={handleOnVarOpen} onOpen={handleOnVarOpen}
currentProvider={currCollection}
currentTool={currTool}
/> />
</Field> </Field>
)} )}

View File

@ -1,5 +1,6 @@
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'
@ -571,3 +572,17 @@ export const usePluginInfo = (providerName?: string) => {
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,
},
}),
})
}