mirror of
https://github.com/langgenius/dify.git
synced 2025-08-17 13:46:00 +00:00
feat: add DYNAMIC_SELECT parameter type for dynamic options in parameter entities (#21425)
This commit is contained in:
parent
ae00ba44db
commit
cea6522122
@ -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")
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.")
|
||||||
|
45
api/core/plugin/impl/dynamic_select.py
Normal file
45
api/core/plugin/impl/dynamic_select.py
Normal 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")
|
@ -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
|
||||||
|
@ -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]()
|
||||||
|
74
api/services/plugin/plugin_parameter_service.py
Normal file
74
api/services/plugin/plugin_parameter_service.py
Normal 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
|
||||||
|
)
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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 && (
|
||||||
|
@ -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>
|
||||||
|
@ -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 && (
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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'
|
||||||
@ -477,7 +478,7 @@ export const usePluginTaskList = (category?: PluginType) => {
|
|||||||
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(() => {
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user