merge feat/tool-oauth

pull/22338/head^2
zxhlyh 11 months ago
commit 73e209505a

@ -122,7 +122,6 @@ class TencentVector(BaseVector):
metric_type,
params,
)
index_text = vdb_index.FilterIndex(self.field_text, enum.FieldType.String, enum.IndexType.FILTER)
index_metadate = vdb_index.FilterIndex(self.field_metadata, enum.FieldType.Json, enum.IndexType.FILTER)
index_sparse_vector = vdb_index.SparseIndex(
name="sparse_vector",
@ -130,7 +129,7 @@ class TencentVector(BaseVector):
index_type=enum.IndexType.SPARSE_INVERTED,
metric_type=enum.MetricType.IP,
)
indexes = [index_id, index_vector, index_text, index_metadate]
indexes = [index_id, index_vector, index_metadate]
if self._enable_hybrid_search:
indexes.append(index_sparse_vector)
try:
@ -149,7 +148,7 @@ class TencentVector(BaseVector):
index_metadate = vdb_index.FilterIndex(
self.field_metadata, enum.FieldType.String, enum.IndexType.FILTER
)
indexes = [index_id, index_vector, index_text, index_metadate]
indexes = [index_id, index_vector, index_metadate]
if self._enable_hybrid_search:
indexes.append(index_sparse_vector)
self._client.create_collection(

@ -17,6 +17,7 @@ from core.workflow.entities.workflow_execution import (
)
from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository
from core.workflow.workflow_type_encoder import WorkflowRuntimeTypeConverter
from libs.helper import extract_tenant_id
from models import (
Account,
CreatorUserRole,
@ -67,7 +68,7 @@ class SQLAlchemyWorkflowExecutionRepository(WorkflowExecutionRepository):
)
# Extract tenant_id from user
tenant_id: str | None = user.tenant_id if isinstance(user, EndUser) else user.current_tenant_id
tenant_id = extract_tenant_id(user)
if not tenant_id:
raise ValueError("User must have a tenant_id or current_tenant_id")
self._tenant_id = tenant_id

@ -20,6 +20,7 @@ from core.workflow.entities.workflow_node_execution import (
from core.workflow.nodes.enums import NodeType
from core.workflow.repositories.workflow_node_execution_repository import OrderConfig, WorkflowNodeExecutionRepository
from core.workflow.workflow_type_encoder import WorkflowRuntimeTypeConverter
from libs.helper import extract_tenant_id
from models import (
Account,
CreatorUserRole,
@ -70,7 +71,7 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository)
)
# Extract tenant_id from user
tenant_id: str | None = user.tenant_id if isinstance(user, EndUser) else user.current_tenant_id
tenant_id = extract_tenant_id(user)
if not tenant_id:
raise ValueError("User must have a tenant_id or current_tenant_id")
self._tenant_id = tenant_id

@ -12,6 +12,7 @@ from flask_login import user_loaded_from_request, user_logged_in # type: ignore
from configs import dify_config
from dify_app import DifyApp
from libs.helper import extract_tenant_id
from models import Account, EndUser
@ -24,11 +25,8 @@ def on_user_loaded(_sender, user: Union["Account", "EndUser"]):
if user:
try:
current_span = get_current_span()
if isinstance(user, Account) and user.current_tenant_id:
tenant_id = user.current_tenant_id
elif isinstance(user, EndUser):
tenant_id = user.tenant_id
else:
tenant_id = extract_tenant_id(user)
if not tenant_id:
return
if current_span:
current_span.set_attribute("service.tenant.id", tenant_id)

@ -17,6 +17,7 @@ class EnvironmentVariableField(fields.Raw):
"name": value.name,
"value": encrypter.obfuscated_token(value.value),
"value_type": value.value_type.value,
"description": value.description,
}
if isinstance(value, Variable):
return {
@ -24,6 +25,7 @@ class EnvironmentVariableField(fields.Raw):
"name": value.name,
"value": value.value,
"value_type": value.value_type.value,
"description": value.description,
}
if isinstance(value, dict):
value_type = value.get("value_type")

@ -25,6 +25,31 @@ from extensions.ext_redis import redis_client
if TYPE_CHECKING:
from models.account import Account
from models.model import EndUser
def extract_tenant_id(user: Union["Account", "EndUser"]) -> str | None:
"""
Extract tenant_id from Account or EndUser object.
Args:
user: Account or EndUser object
Returns:
tenant_id string if available, None otherwise
Raises:
ValueError: If user is neither Account nor EndUser
"""
from models.account import Account
from models.model import EndUser
if isinstance(user, Account):
return user.current_tenant_id
elif isinstance(user, EndUser):
return user.tenant_id
else:
raise ValueError(f"Invalid user type: {type(user)}. Expected Account or EndUser.")
def run(script):

@ -15,6 +15,7 @@ from core.variables import utils as variable_utils
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
from core.workflow.nodes.enums import NodeType
from factories.variable_factory import TypeMismatchError, build_segment_with_type
from libs.helper import extract_tenant_id
from ._workflow_exc import NodeNotFoundError, WorkflowDataError
@ -352,12 +353,7 @@ class Workflow(Base):
self._environment_variables = "{}"
# Get tenant_id from current_user (Account or EndUser)
if isinstance(current_user, Account):
# Account user
tenant_id = current_user.current_tenant_id
else:
# EndUser
tenant_id = current_user.tenant_id
tenant_id = extract_tenant_id(current_user)
if not tenant_id:
return []
@ -384,12 +380,7 @@ class Workflow(Base):
return
# Get tenant_id from current_user (Account or EndUser)
if isinstance(current_user, Account):
# Account user
tenant_id = current_user.current_tenant_id
else:
# EndUser
tenant_id = current_user.tenant_id
tenant_id = extract_tenant_id(current_user)
if not tenant_id:
self._environment_variables = "{}"

@ -18,6 +18,7 @@ from core.file import helpers as file_helpers
from core.rag.extractor.extract_processor import ExtractProcessor
from extensions.ext_database import db
from extensions.ext_storage import storage
from libs.helper import extract_tenant_id
from models.account import Account
from models.enums import CreatorUserRole
from models.model import EndUser, UploadFile
@ -61,11 +62,7 @@ class FileService:
# generate file key
file_uuid = str(uuid.uuid4())
if isinstance(user, Account):
current_tenant_id = user.current_tenant_id
else:
# end_user
current_tenant_id = user.tenant_id
current_tenant_id = extract_tenant_id(user)
file_key = "upload_files/" + (current_tenant_id or "") + "/" + file_uuid + "." + extension

@ -0,0 +1,65 @@
import pytest
from libs.helper import extract_tenant_id
from models.account import Account
from models.model import EndUser
class TestExtractTenantId:
"""Test cases for the extract_tenant_id utility function."""
def test_extract_tenant_id_from_account_with_tenant(self):
"""Test extracting tenant_id from Account with current_tenant_id."""
# Create a mock Account object
account = Account()
# Mock the current_tenant_id property
account._current_tenant = type("MockTenant", (), {"id": "account-tenant-123"})()
tenant_id = extract_tenant_id(account)
assert tenant_id == "account-tenant-123"
def test_extract_tenant_id_from_account_without_tenant(self):
"""Test extracting tenant_id from Account without current_tenant_id."""
# Create a mock Account object
account = Account()
account._current_tenant = None
tenant_id = extract_tenant_id(account)
assert tenant_id is None
def test_extract_tenant_id_from_enduser_with_tenant(self):
"""Test extracting tenant_id from EndUser with tenant_id."""
# Create a mock EndUser object
end_user = EndUser()
end_user.tenant_id = "enduser-tenant-456"
tenant_id = extract_tenant_id(end_user)
assert tenant_id == "enduser-tenant-456"
def test_extract_tenant_id_from_enduser_without_tenant(self):
"""Test extracting tenant_id from EndUser without tenant_id."""
# Create a mock EndUser object
end_user = EndUser()
end_user.tenant_id = None
tenant_id = extract_tenant_id(end_user)
assert tenant_id is None
def test_extract_tenant_id_with_invalid_user_type(self):
"""Test extracting tenant_id with invalid user type raises ValueError."""
invalid_user = "not_a_user_object"
with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"):
extract_tenant_id(invalid_user)
def test_extract_tenant_id_with_none_user(self):
"""Test extracting tenant_id with None user raises ValueError."""
with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"):
extract_tenant_id(None)
def test_extract_tenant_id_with_dict_user(self):
"""Test extracting tenant_id with dict user raises ValueError."""
dict_user = {"id": "123", "tenant_id": "456"}
with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"):
extract_tenant_id(dict_user)

@ -9,6 +9,7 @@ from core.file.models import File
from core.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable
from core.variables.segments import IntegerSegment, Segment
from factories.variable_factory import build_segment
from models.model import EndUser
from models.workflow import Workflow, WorkflowDraftVariable, WorkflowNodeExecutionModel, is_system_variable_editable
@ -43,7 +44,7 @@ def test_environment_variables():
)
# Mock current_user as an EndUser
mock_user = mock.Mock()
mock_user = mock.Mock(spec=EndUser)
mock_user.tenant_id = "tenant_id"
with (
@ -90,7 +91,7 @@ def test_update_environment_variables():
)
# Mock current_user as an EndUser
mock_user = mock.Mock()
mock_user = mock.Mock(spec=EndUser)
mock_user.tenant_id = "tenant_id"
with (
@ -136,7 +137,7 @@ def test_to_dict():
# Create some EnvironmentVariable instances
# Mock current_user as an EndUser
mock_user = mock.Mock()
mock_user = mock.Mock(spec=EndUser)
mock_user.tenant_id = "tenant_id"
with (

@ -826,6 +826,9 @@ MAX_ITERATIONS_NUM=99
# The timeout for the text generation in millisecond
TEXT_GENERATION_TIMEOUT_MS=60000
# Allow rendering unsafe URLs which have "data:" scheme.
ALLOW_UNSAFE_DATA_SCHEME=false
# ------------------------------
# Environment Variables for db Service
# ------------------------------

@ -67,6 +67,7 @@ services:
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
CSP_WHITELIST: ${CSP_WHITELIST:-}
ALLOW_EMBED: ${ALLOW_EMBED:-false}
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}

@ -364,6 +364,7 @@ x-shared-env: &shared-api-worker-env
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
POSTGRES_USER: ${POSTGRES_USER:-${DB_USERNAME}}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-${DB_PASSWORD}}
POSTGRES_DB: ${POSTGRES_DB:-${DB_DATABASE}}
@ -582,6 +583,7 @@ services:
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
CSP_WHITELIST: ${CSP_WHITELIST:-}
ALLOW_EMBED: ${ALLOW_EMBED:-false}
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}

@ -32,6 +32,9 @@ NEXT_PUBLIC_CSP_WHITELIST=
# Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
NEXT_PUBLIC_ALLOW_EMBED=
# Allow rendering unsafe URLs which have "data:" scheme.
NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false
# Github Access Token, used for invoking Github API
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN=
# The maximum number of top-k value for RAG.

@ -0,0 +1,87 @@
import {
isValidElement,
memo,
useMemo,
} from 'react'
import type { AnyFieldApi } from '@tanstack/react-form'
import { useStore } from '@tanstack/react-form'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'
import type { FormSchema } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
import { useRenderI18nObject } from '@/hooks/use-i18n'
export type BaseFieldProps = {
fieldClassName?: string
labelClassName?: string
inputContainerClassName?: string
inputClassName?: string
formSchema: FormSchema
field: AnyFieldApi
disabled?: boolean
}
const BaseField = ({
fieldClassName,
labelClassName,
inputContainerClassName,
inputClassName,
formSchema,
field,
disabled,
}: BaseFieldProps) => {
const renderI18nObject = useRenderI18nObject()
const {
label,
} = formSchema
const memorizedLabel = useMemo(() => {
if (isValidElement(label))
return label
if (typeof label === 'string')
return label
if (typeof label === 'object' && label !== null)
return renderI18nObject(label as Record<string, string>)
}, [label, renderI18nObject])
const value = useStore(field.form.store, s => s.values[field.name])
return (
<div className={cn(fieldClassName)}>
<div className={cn(labelClassName)}>
{memorizedLabel}
</div>
<div className={cn(inputContainerClassName)}>
{
formSchema.type === FormTypeEnum.textInput && (
<Input
id={field.name}
name={field.name}
className={cn(inputClassName)}
value={value}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
/>
)
}
{
formSchema.type === FormTypeEnum.secretInput && (
<Input
id={field.name}
name={field.name}
type='password'
className={cn(inputClassName)}
value={value}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
/>
)
}
</div>
</div>
)
}
export default memo(BaseField)

@ -0,0 +1,96 @@
import {
memo,
useCallback,
useImperativeHandle,
} from 'react'
import type {
AnyFieldApi,
} from '@tanstack/react-form'
import { useForm } from '@tanstack/react-form'
import type {
FormRef,
FormSchema,
} from '@/app/components/base/form/types'
import {
BaseField,
} from '.'
import type {
BaseFieldProps,
} from '.'
import cn from '@/utils/classnames'
export type BaseFormProps = {
formSchemas?: FormSchema[]
defaultValues?: Record<string, any>
formClassName?: string
ref?: FormRef
disabled?: boolean
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>
const BaseForm = ({
formSchemas,
defaultValues,
formClassName,
fieldClassName,
labelClassName,
inputContainerClassName,
inputClassName,
ref,
disabled,
}: BaseFormProps) => {
const form = useForm({
defaultValues,
})
useImperativeHandle(ref, () => {
return {
getForm() {
return form
},
}
}, [form])
const renderField = useCallback((field: AnyFieldApi) => {
const formSchema = formSchemas?.find(schema => schema.name === field.name)
if (formSchema) {
return (
<BaseField
field={field}
formSchema={formSchema}
fieldClassName={fieldClassName}
labelClassName={labelClassName}
inputContainerClassName={inputContainerClassName}
inputClassName={inputClassName}
disabled={disabled}
/>
)
}
return null
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled])
if (!formSchemas?.length)
return null
return (
<form
className={cn(formClassName)}
>
{
formSchemas.map((formSchema) => {
return (
<form.Field
key={formSchema.name}
name={formSchema.name}
>
{renderField}
</form.Field>
)
})
}
</form>
)
}
export default memo(BaseForm)

@ -0,0 +1,2 @@
export { default as BaseForm, type BaseFormProps } from './base-form'
export { default as BaseField, type BaseFieldProps } from './base-field'

@ -0,0 +1,21 @@
import { memo } from 'react'
import { BaseForm } from '../../components/base'
import type { BaseFormProps } from '../../components/base'
const AuthForm = ({
formSchemas = [],
defaultValues,
ref,
}: BaseFormProps) => {
return (
<BaseForm
ref={ref}
formSchemas={formSchemas}
defaultValues={defaultValues}
formClassName='space-y-4'
labelClassName='h-6 flex items-center mb-1 system-sm-medium text-text-secondary'
/>
)
}
export default memo(AuthForm)

@ -0,0 +1,51 @@
import type {
ForwardedRef,
ReactNode,
} from 'react'
import type { AnyFormApi } from '@tanstack/react-form'
export type TypeWithI18N<T = string> = {
en_US: T
zh_Hans: T
[key: string]: T
}
export type FormShowOnObject = {
variable: string
value: string
}
export enum FormTypeEnum {
textInput = 'text-input',
textNumber = 'number-input',
secretInput = 'secret-input',
select = 'select',
radio = 'radio',
boolean = 'boolean',
files = 'files',
file = 'file',
modelSelector = 'model-selector',
toolSelector = 'tool-selector',
multiToolSelector = 'array[tools]',
appSelector = 'app-selector',
dynamicSelect = 'dynamic-select',
}
export type FormSchema = {
type: FormTypeEnum
name: string
label: string | ReactNode | TypeWithI18N
required: boolean
default?: any
tooltip?: string | TypeWithI18N
show_on?: FormShowOnObject[]
url?: string
scope?: string
}
export type FormValues = Record<string, any>
export type FromRefObject = {
getForm: () => AnyFormApi
}
export type FormRef = ForwardedRef<FromRefObject>

@ -1,3 +1,7 @@
import { ALLOW_UNSAFE_DATA_SCHEME } from '@/config'
export const isValidUrl = (url: string): boolean => {
return ['http:', 'https:', '//', 'mailto:'].some(prefix => url.startsWith(prefix))
const validPrefixes = ['http:', 'https:', '//', 'mailto:']
if (ALLOW_UNSAFE_DATA_SCHEME) validPrefixes.push('data:')
return validPrefixes.some(prefix => url.startsWith(prefix))
}

@ -4,6 +4,7 @@
* Includes preprocessing for LaTeX and custom "think" tags.
*/
import { flow } from 'lodash-es'
import { ALLOW_UNSAFE_DATA_SCHEME } from '@/config'
export const preprocessLaTeX = (content: string) => {
if (typeof content !== 'string')
@ -86,5 +87,8 @@ export const customUrlTransform = (uri: string): string | undefined => {
if (PERMITTED_SCHEME_REGEX.test(scheme))
return uri
if (ALLOW_UNSAFE_DATA_SCHEME && scheme === 'data:')
return uri
return undefined
}

@ -0,0 +1,123 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import cn from '@/utils/classnames'
type ModalProps = {
onClose?: () => void
size?: 'sm' | 'md'
title: string
subTitle?: string
children?: React.ReactNode
confirmButtonText?: string
onConfirm?: () => void
cancelButtonText?: string
onCancel?: () => void
showExtraButton?: boolean
extraButtonText?: string
extraButtonVariant?: ButtonProps['variant']
onExtraButtonClick?: () => void
footerSlot?: React.ReactNode
bottomSlot?: React.ReactNode
disabled?: boolean
}
const Modal = ({
onClose,
size = 'sm',
title,
subTitle,
children,
confirmButtonText,
onConfirm,
cancelButtonText,
onCancel,
showExtraButton,
extraButtonVariant = 'warning',
extraButtonText,
onExtraButtonClick,
footerSlot,
bottomSlot,
disabled,
}: ModalProps) => {
const { t } = useTranslation()
return (
<PortalToFollowElem open>
<PortalToFollowElemContent
className='z-[9998] flex h-full w-full items-center justify-center bg-background-overlay'
onClick={onClose}
>
<div
className={cn(
'max-h-[80%] w-[480px] overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xs',
size === 'sm' && 'w-[480px',
size === 'md' && 'w-[640px]',
)}
onClick={e => e.stopPropagation()}
>
<div className='title-2xl-semi-bold relative p-6 pb-3 pr-14 text-text-primary'>
{title}
{
subTitle && (
<div className='system-xs-regular mt-1 text-text-tertiary'>
{subTitle}
</div>
)
}
<div
className='absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg'
onClick={onClose}
>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
</div>
</div>
{
children && (
<div className='px-6 py-3'>{children}</div>
)
}
<div className='flex items-center justify-end p-6 pt-5'>
{footerSlot}
{
showExtraButton && (
<>
<Button
variant={extraButtonVariant}
onClick={onExtraButtonClick}
disabled={disabled}
>
{extraButtonText || t('common.operation.remove')}
</Button>
<div className='mx-3 h-4 w-[1px] bg-divider-regular'></div>
</>
)
}
<Button
onClick={onCancel}
disabled={disabled}
>
{cancelButtonText || t('common.operation.cancel')}
</Button>
<Button
className='ml-2'
variant='primary'
onClick={onConfirm}
disabled={disabled}
>
{confirmButtonText || t('common.operation.save')}
</Button>
</div>
{bottomSlot}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(Modal)

@ -101,7 +101,7 @@ const Tooltip: FC<TooltipProps> = ({
>
{popupContent && (<div
className={cn(
!noDecoration && 'system-xs-regular relative break-words rounded-md bg-components-panel-bg px-3 py-2 text-text-tertiary shadow-lg',
!noDecoration && 'system-xs-regular relative max-w-[300px] break-words rounded-md bg-components-panel-bg px-3 py-2 text-left text-text-tertiary shadow-lg',
popupClassName,
)}
onMouseEnter={() => triggerMethod === 'hover' && setHoverPopup()}

@ -0,0 +1,46 @@
import {
memo,
useState,
} from 'react'
import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import ApiKeyModal from './api-key-modal'
export type AddApiKeyButtonProps = {
provider?: string
buttonVariant?: ButtonProps['variant']
buttonText?: string
disabled?: boolean
}
const AddApiKeyButton = ({
provider = '',
buttonVariant = 'secondary-accent',
buttonText = 'use api key',
disabled,
}: AddApiKeyButtonProps) => {
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false)
return (
<>
<Button
className='grow'
variant={buttonVariant}
onClick={() => setIsApiKeyModalOpen(true)}
disabled={disabled}
>
{buttonText}
</Button>
{
isApiKeyModalOpen && (
<ApiKeyModal
provider={provider}
onClose={() => setIsApiKeyModalOpen(false)}
/>
)
}
</>
)
}
export default memo(AddApiKeyButton)

@ -0,0 +1,72 @@
import {
memo,
useState,
} from 'react'
import { RiEqualizer2Line } from '@remixicon/react'
import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import OAuthClientSettings from './oauth-client-settings'
import cn from '@/utils/classnames'
export type AddOAuthButtonProps = {
buttonVariant?: ButtonProps['variant']
buttonText?: string
className?: string
buttonLeftClassName?: string
buttonRightClassName?: string
dividerClassName?: string
disabled?: boolean
}
const AddOAuthButton = ({
buttonVariant = 'primary',
buttonText = 'use oauth',
className,
buttonLeftClassName,
buttonRightClassName,
dividerClassName,
disabled,
}: AddOAuthButtonProps) => {
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
return (
<>
<Button
variant={buttonVariant}
className={cn(
'grow px-0 py-0 hover:bg-components-button-primary-bg',
className,
)}
disabled={disabled}
>
<div className={cn(
'flex h-full grow items-center justify-center rounded-l-lg hover:bg-components-button-primary-bg-hover',
buttonLeftClassName,
)}>
{buttonText}
</div>
<div className={cn(
'h-4 w-[1px] bg-text-primary-on-surface opacity-[0.15]',
dividerClassName,
)}></div>
<div
className={cn(
'flex h-full w-8 shrink-0 items-center justify-center rounded-r-lg hover:bg-components-button-primary-bg-hover',
buttonRightClassName,
)}
onClick={() => setIsOAuthSettingsOpen(true)}
>
<RiEqualizer2Line className='h-4 w-4' />
</div>
</Button>
{
isOAuthSettingsOpen && (
<OAuthClientSettings
onClose={() => setIsOAuthSettingsOpen(false)}
/>
)
}
</>
)
}
export default memo(AddOAuthButton)

@ -0,0 +1,145 @@
import {
memo,
useCallback,
useMemo,
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiExternalLinkLine } from '@remixicon/react'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import Modal from '@/app/components/base/modal/modal'
import {
useAddPluginToolCredential,
useGetPluginToolCredentialSchema,
useInvalidPluginToolCredentialInfo,
useUpdatePluginToolCredential,
} from '@/service/use-plugins-auth'
import { CredentialTypeEnum } from '../types'
import { transformFormSchemasSecretInput } from '../utils'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type { FromRefObject } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast'
export type ApiKeyModalProps = {
provider: string
onClose?: () => void
editValues?: Record<string, any>
onRemove?: () => void
disabled?: boolean
}
const ApiKeyModal = ({
provider,
onClose,
editValues,
onRemove,
disabled,
}: ApiKeyModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { data = [] } = useGetPluginToolCredentialSchema(provider, CredentialTypeEnum.API_KEY)
const formSchemas = useMemo(() => {
return [
{
type: FormTypeEnum.textInput,
name: '__name__',
label: 'Authorization name',
required: false,
},
...data,
]
}, [data])
const { mutateAsync: addPluginToolCredential } = useAddPluginToolCredential(provider)
const { mutateAsync: updatePluginToolCredential } = useUpdatePluginToolCredential(provider)
const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider)
const formRef = useRef<FromRefObject>(null)
const handleConfirm = useCallback(async () => {
const form = formRef.current?.getForm()
const store = form?.store
const {
__name__,
__credential_id__,
...values
} = store?.state.values
const isPristineSecretInputNames: string[] = []
formSchemas.forEach((schema) => {
if (schema.type === FormTypeEnum.secretInput) {
const fieldMeta = form?.getFieldMeta(schema.name)
if (fieldMeta?.isPristine)
isPristineSecretInputNames.push(schema.name)
}
})
const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values)
if (editValues) {
await updatePluginToolCredential({
credentials: transformedValues,
credential_id: __credential_id__,
type: CredentialTypeEnum.API_KEY,
name: __name__ || '',
})
}
else {
await addPluginToolCredential({
credentials: transformedValues,
type: CredentialTypeEnum.API_KEY,
name: __name__ || '',
})
}
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
invalidatePluginToolCredentialInfo()
}, [addPluginToolCredential, onClose, invalidatePluginToolCredentialInfo, updatePluginToolCredential, notify, t, editValues, formSchemas])
return (
<Modal
size='md'
title='API Key Authorization Configuration'
subTitle='After configuring credentials, all members within the workspace can use this tool when orchestrating applications.'
onClose={onClose}
onCancel={onClose}
footerSlot={
<a
className='system-xs-regular flex h-8 grow items-center text-text-accent'
href=''
target='_blank'
>
Get your API Key from OpenAI
<RiExternalLinkLine className='ml-1 h-3 w-3' />
</a>
}
bottomSlot={
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')}
<a
className='mx-1 text-text-accent'
target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
</div>
}
onConfirm={handleConfirm}
showExtraButton={!!editValues}
onExtraButtonClick={onRemove}
disabled={disabled}
>
<AuthForm
ref={formRef}
formSchemas={formSchemas}
defaultValues={editValues}
disabled={disabled}
/>
</Modal>
)
}
export default memo(ApiKeyModal)

@ -0,0 +1,91 @@
import {
memo,
useMemo,
} from 'react'
import AddOAuthButton from './add-oauth-button'
import type { AddOAuthButtonProps } from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
import type { AddApiKeyButtonProps } from './add-api-key-button'
type AuthorizeProps = {
provider?: string
theme?: 'primary' | 'secondary'
showDivider?: boolean
canOAuth?: boolean
canApiKey?: boolean
disabled?: boolean
}
const Authorize = ({
provider = '',
theme = 'primary',
showDivider = true,
canOAuth,
canApiKey,
disabled,
}: AuthorizeProps) => {
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
if (theme === 'secondary') {
return {
buttonText: !canApiKey ? 'Add OAuth Authorization' : 'Add OAuth',
buttonVariant: 'secondary',
className: 'hover:bg-components-button-secondary-bg',
buttonLeftClassName: 'hover:bg-components-button-secondary-bg-hover',
buttonRightClassName: 'hover:bg-components-button-secondary-bg-hover',
dividerClassName: 'bg-divider-regular opacity-100',
}
}
return {
buttonText: !canApiKey ? 'Use OAuth Authorization' : 'Use OAuth',
}
}, [canApiKey, theme])
const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => {
if (theme === 'secondary') {
return {
provider,
buttonVariant: 'secondary',
buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Add API Key',
}
}
return {
provider,
buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Use API Key',
buttonVariant: !canOAuth ? 'primary' : 'secondary-accent',
}
}, [canOAuth, theme, provider])
return (
<>
<div className='flex items-center space-x-1.5'>
{
canOAuth && (
<AddOAuthButton
{...oAuthButtonProps}
disabled={disabled}
/>
)
}
{
showDivider && canOAuth && canApiKey && (
<div className='system-2xs-medium-uppercase flex shrink-0 flex-col items-center justify-between text-text-tertiary'>
<div className='h-2 w-[1px] bg-divider-subtle'></div>
or
<div className='h-2 w-[1px] bg-divider-subtle'></div>
</div>
)
}
{
canApiKey && (
<AddApiKeyButton
{...apiKeyButtonProps}
disabled={disabled}
/>
)
}
</div>
</>
)
}
export default memo(Authorize)

@ -0,0 +1,26 @@
import { memo } from 'react'
import Modal from '@/app/components/base/modal/modal'
type OAuthClientSettingsProps = {
onClose?: () => void
}
const OAuthClientSettings = ({
onClose,
}: OAuthClientSettingsProps) => {
return (
<Modal
title='Oauth client settings'
confirmButtonText='Save & Authorize'
cancelButtonText='Save only'
extraButtonText='Cancel'
showExtraButton
extraButtonVariant='secondary'
onExtraButtonClick={onClose}
onClose={onClose}
>
<div>oauth</div>
</Modal>
)
}
export default memo(OAuthClientSettings)

@ -0,0 +1,94 @@
import {
memo,
useCallback,
useMemo,
useState,
} from 'react'
import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
import type { Credential } from './types'
import {
Authorized,
usePluginAuth,
} from '.'
type AuthorizedInNodeProps = {
provider: string
onAuthorizationItemClick: (id: string) => void
credentialId?: string
}
const AuthorizedInNode = ({
provider = '',
onAuthorizationItemClick,
credentialId,
}: AuthorizedInNodeProps) => {
const [isOpen, setIsOpen] = useState(false)
const {
canApiKey,
canOAuth,
credentials,
disabled,
} = usePluginAuth(provider, isOpen)
const label = useMemo(() => {
if (!credentialId)
return 'Workspace default'
const credential = credentials.find(c => c.id === credentialId)
if (!credential)
return 'Auth removed'
return credential.name
}, [credentials, credentialId])
const renderTrigger = useCallback((open?: boolean) => {
return (
<Button
size='small'
className={cn(open && 'bg-components-button-ghost-bg-hover')}
>
<Indicator className='mr-1.5' />
{label}
<RiArrowDownSLine className='h-3.5 w-3.5 text-components-button-ghost-text' />
</Button>
)
}, [label])
const extraAuthorizationItems: Credential[] = [
{
id: '__workspace_default__',
name: 'Workspace default',
provider: '',
is_default: false,
isWorkspaceDefault: true,
},
]
const handleAuthorizationItemClick = useCallback((id: string) => {
onAuthorizationItemClick(id)
setIsOpen(false)
}, [
onAuthorizationItemClick,
setIsOpen,
])
return (
<Authorized
provider={provider}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
offset={4}
placement='bottom-end'
triggerPopupSameWidth={false}
popupClassName='w-[360px]'
disabled={disabled}
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
/>
)
}
export default memo(AuthorizedInNode)

@ -0,0 +1,267 @@
import {
memo,
useCallback,
useRef,
useState,
} from 'react'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type {
PortalToFollowElemOptions,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
import Authorize from '../authorize'
import type { Credential } from '../types'
import { CredentialTypeEnum } from '../types'
import ApiKeyModal from '../authorize/api-key-modal'
import Item from './item'
import {
useDeletePluginToolCredential,
useInvalidPluginToolCredentialInfo,
useSetPluginToolDefaultCredential,
} from '@/service/use-plugins-auth'
import { useToastContext } from '@/app/components/base/toast'
type AuthorizedProps = {
provider: string
credentials: Credential[]
canOAuth?: boolean
canApiKey?: boolean
disabled?: boolean
renderTrigger?: (open?: boolean) => React.ReactNode
isOpen?: boolean
onOpenChange?: (open: boolean) => void
offset?: PortalToFollowElemOptions['offset']
placement?: PortalToFollowElemOptions['placement']
triggerPopupSameWidth?: boolean
popupClassName?: string
disableSetDefault?: boolean
onItemClick?: (id: string) => void
extraAuthorizationItems?: Credential[]
}
const Authorized = ({
provider,
credentials,
canOAuth,
canApiKey,
disabled,
renderTrigger,
isOpen,
onOpenChange,
offset = 8,
placement = 'bottom-start',
triggerPopupSameWidth = true,
popupClassName,
disableSetDefault,
onItemClick,
extraAuthorizationItems,
}: AuthorizedProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [isLocalOpen, setIsLocalOpen] = useState(false)
const mergedIsOpen = isOpen ?? isLocalOpen
const setMergedIsOpen = useCallback((open: boolean) => {
if (onOpenChange)
onOpenChange(open)
setIsLocalOpen(open)
}, [onOpenChange])
const oAuthCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.OAUTH2)
const apiKeyCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.API_KEY)
const pendingOperationCredentialId = useRef<string | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const { mutateAsync: deletePluginToolCredential } = useDeletePluginToolCredential(provider)
const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider)
const openConfirm = useCallback((credentialId?: string) => {
if (credentialId)
pendingOperationCredentialId.current = credentialId
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const closeConfirm = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [])
const handleConfirm = useCallback(async () => {
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
await deletePluginToolCredential({ credential_id: pendingOperationCredentialId.current })
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
invalidatePluginToolCredentialInfo()
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [deletePluginToolCredential, invalidatePluginToolCredentialInfo, notify, t])
const [editValues, setEditValues] = useState<Record<string, any> | null>(null)
const handleEdit = useCallback((id: string, values: Record<string, any>) => {
pendingOperationCredentialId.current = id
setEditValues(values)
}, [])
const handleRemove = useCallback(() => {
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const { mutateAsync: setPluginToolDefaultCredential } = useSetPluginToolDefaultCredential(provider)
const handleSetDefault = useCallback(async (id: string) => {
await setPluginToolDefaultCredential(id)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
invalidatePluginToolCredentialInfo()
}, [setPluginToolDefaultCredential, invalidatePluginToolCredentialInfo, notify, t])
return (
<>
<PortalToFollowElem
open={mergedIsOpen}
onOpenChange={setMergedIsOpen}
placement={placement}
offset={offset}
triggerPopupSameWidth={triggerPopupSameWidth}
>
<PortalToFollowElemTrigger
onClick={() => setMergedIsOpen(!mergedIsOpen)}
asChild
>
{
renderTrigger
? renderTrigger(mergedIsOpen)
: (
<Button
className={cn(
'w-full',
isOpen && 'bg-components-button-secondary-bg-hover',
)}>
<Indicator className='mr-2' />
{credentials.length} Authorizations
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[100]'>
<div className={cn(
'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
popupClassName,
)}>
{
!!extraAuthorizationItems?.length && (
<div className='p-1'>
{
extraAuthorizationItems.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled={disabled}
onItemClick={onItemClick}
disableRename
disableEdit
disableDelete
disableSetDefault
/>
))
}
</div>
)
}
<div className='py-1'>
{
!!oAuthCredentials.length && (
<div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
OAuth
</div>
{
oAuthCredentials.map(credential => (
<Item
key={credential.id}
credential={credential}
/>
))
}
</div>
)
}
{
!!apiKeyCredentials.length && (
<div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
API Keys
</div>
{
apiKeyCredentials.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled={disabled}
onDelete={openConfirm}
onEdit={handleEdit}
onSetDefault={handleSetDefault}
disableSetDefault={disableSetDefault}
onItemClick={onItemClick}
/>
))
}
</div>
)
}
</div>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='p-2'>
<Authorize
provider={provider}
theme='secondary'
showDivider={false}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
/>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
{
deleteCredentialId && (
<Confirm
isShow
title='Are you sure?'
content='content'
onCancel={closeConfirm}
onConfirm={handleConfirm}
/>
)
}
{
!!editValues && (
<ApiKeyModal
provider={provider}
editValues={editValues}
onClose={() => {
setEditValues(null)
pendingOperationCredentialId.current = null
}}
onRemove={handleRemove}
disabled={disabled}
/>
)
}
</>
)
}
export default memo(Authorized)

@ -0,0 +1,139 @@
import {
memo,
useMemo,
} from 'react'
import {
RiDeleteBinLine,
RiEditLine,
RiEqualizer2Line,
} from '@remixicon/react'
import Indicator from '@/app/components/header/indicator'
import Badge from '@/app/components/base/badge'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import Button from '@/app/components/base/button'
import type { Credential } from '../types'
import { CredentialTypeEnum } from '../types'
type ItemProps = {
credential: Credential
disabled?: boolean
onDelete?: (id: string) => void
onEdit?: (id: string, values: Record<string, any>) => void
onSetDefault?: (id: string) => void
disableRename?: boolean
disableEdit?: boolean
disableDelete?: boolean
disableSetDefault?: boolean
onItemClick?: (id: string) => void
}
const Item = ({
credential,
disabled,
onDelete,
onEdit,
onSetDefault,
disableRename,
disableEdit,
disableDelete,
disableSetDefault,
onItemClick,
}: ItemProps) => {
const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2
const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete && disableSetDefault)
}, [disableRename, disableEdit, disableDelete, disableSetDefault])
return (
<div
key={credential.id}
className='group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover'
onClick={() => onItemClick?.(credential.id)}
>
<div className='flex w-0 grow items-center space-x-1.5 pl-2'>
<Indicator className='mr-1.5 shrink-0' />
<div
className='system-md-regular truncate text-text-secondary'
title={credential.name}
>
{credential.name}
</div>
{
credential.is_default && (
<Badge>
Default
</Badge>
)
}
</div>
{
showAction && (
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
{
!credential.is_default && !disableSetDefault && (
<Button
size='small'
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onSetDefault?.(credential.id)
}}
>
Set as default
</Button>
)
}
{
isOAuth && !disableRename && (
<Tooltip popupContent='rename'>
<ActionButton>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
{
!isOAuth && !disableEdit && (
<Tooltip popupContent='edit'>
<ActionButton
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onEdit?.(
credential.id,
{
...credential.credentials,
__name__: credential.name,
__credential_id__: credential.id,
},
)
}}
>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
{
!disableDelete && (
<Tooltip popupContent='delete'>
<ActionButton
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onDelete?.(credential.id)
}}
>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
</div>
)
}
</div>
)
}
export default memo(Item)

@ -0,0 +1,20 @@
import { useAppContext } from '@/context/app-context'
import { useGetPluginToolCredentialInfo } from '@/service/use-plugins-auth'
import { CredentialTypeEnum } from './types'
export const usePluginAuth = (provider: string, enable?: boolean) => {
const { data } = useGetPluginToolCredentialInfo(enable ? provider : '')
const { isCurrentWorkspaceManager } = useAppContext()
const isAuthorized = !!data?.credentials.length
const canOAuth = data?.supported_credential_types.includes(CredentialTypeEnum.OAUTH2)
const canApiKey = data?.supported_credential_types.includes(CredentialTypeEnum.API_KEY)
return {
isAuthorized,
canOAuth,
canApiKey,
credentials: data?.credentials || [],
provider,
disabled: !isCurrentWorkspaceManager,
}
}

@ -0,0 +1,4 @@
export { default as PluginAuth } from './plugin-auth'
export { default as Authorized } from './authorized'
export { default as AuthorizedInNode } from './authorized-in-node'
export { usePluginAuth } from './hooks'

@ -0,0 +1,52 @@
import { memo } from 'react'
import Authorize from './authorize'
import Authorized from './authorized'
import { useAppContext } from '@/context/app-context'
import { useGetPluginToolCredentialInfo } from '@/service/use-plugins-auth'
import { CredentialTypeEnum } from './types'
type PluginAuthProps = {
provider?: string
children?: React.ReactNode
}
const PluginAuth = ({
provider = '',
children,
}: PluginAuthProps) => {
const { data } = useGetPluginToolCredentialInfo(provider)
const { isCurrentWorkspaceManager } = useAppContext()
const isAuthorized = !!data?.credentials.length
const canOAuth = data?.supported_credential_types.includes(CredentialTypeEnum.OAUTH2)
const canApiKey = data?.supported_credential_types.includes(CredentialTypeEnum.API_KEY)
return (
<>
{
!isAuthorized && (
<Authorize
provider={provider}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={!isCurrentWorkspaceManager}
/>
)
}
{
isAuthorized && !children && (
<Authorized
provider={provider}
credentials={data?.credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={!isCurrentWorkspaceManager}
/>
)
}
{
isAuthorized && children
}
</>
)
}
export default memo(PluginAuth)

@ -0,0 +1,14 @@
export enum CredentialTypeEnum {
OAUTH2 = 'oauth2',
API_KEY = 'api-key',
}
export type Credential = {
id: string
name: string
provider: string
credential_type?: CredentialTypeEnum
is_default: boolean
credentials?: Record<string, any>
isWorkspaceDefault?: boolean
}

@ -0,0 +1,10 @@
export const transformFormSchemasSecretInput = (isPristineSecretInputNames: string[], values: Record<string, any>) => {
const transformedValues: Record<string, any> = { ...values }
isPristineSecretInputNames.forEach((name) => {
if (transformedValues[name])
transformedValues[name] = '[__HIDDEN__]'
})
return transformedValues
}

@ -1,17 +1,9 @@
import React, { useMemo, useState } from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import Indicator from '@/app/components/header/indicator'
import ToolItem from '@/app/components/tools/provider/tool-item'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import {
useAllToolProviders,
useBuiltinTools,
useInvalidateAllToolProviders,
useRemoveProviderCredentials,
useUpdateProviderCredentials,
} from '@/service/use-tools'
import type { PluginDetail } from '@/app/components/plugins/types'
@ -23,35 +15,14 @@ const ActionList = ({
detail,
}: Props) => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const providerBriefInfo = detail.declaration.tool.identity
const providerKey = `${detail.plugin_id}/${providerBriefInfo.name}`
const { data: collectionList = [] } = useAllToolProviders()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const provider = useMemo(() => {
return collectionList.find(collection => collection.name === providerKey)
}, [collectionList, providerKey])
const { data } = useBuiltinTools(providerKey)
const [showSettingAuth, setShowSettingAuth] = useState(false)
const handleCredentialSettingUpdate = () => {
invalidateAllToolProviders()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setShowSettingAuth(false)
}
const { mutate: updatePermission, isPending } = useUpdateProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
const { mutate: removePermission } = useRemoveProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
if (!data || !provider)
return null
@ -60,26 +31,7 @@ const ActionList = ({
<div className='mb-1 py-1'>
<div className='system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary'>
{t('plugin.detailPanel.actionNum', { num: data.length, action: data.length > 1 ? 'actions' : 'action' })}
{provider.is_team_authorization && provider.allow_delete && (
<Button
variant='secondary'
size='small'
onClick={() => setShowSettingAuth(true)}
disabled={!isCurrentWorkspaceManager}
>
<Indicator className='mr-2' color={'green'} />
{t('tools.auth.authorized')}
</Button>
)}
</div>
{!provider.is_team_authorization && provider.allow_delete && (
<Button
variant='primary'
className='w-full'
onClick={() => setShowSettingAuth(true)}
disabled={!isCurrentWorkspaceManager}
>{t('workflow.nodes.tool.authorize')}</Button>
)}
</div>
<div className='flex flex-col gap-2'>
{data.map(tool => (
@ -93,18 +45,6 @@ const ActionList = ({
/>
))}
</div>
{showSettingAuth && (
<ConfigCredential
collection={provider}
onCancel={() => setShowSettingAuth(false)}
onSaved={async value => updatePermission({
providerName: provider.name,
credentials: value,
})}
onRemove={async () => removePermission(provider.name)}
isSaving={isPending}
/>
)}
</div>
)
}

@ -41,6 +41,8 @@ import { getMarketplaceUrl } from '@/utils/var'
import useReferenceSetting from '../plugin-page/use-reference-setting'
import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types'
import { useAppContext } from '@/context/app-context'
import { PluginAuth } from '@/app/components/plugins/plugin-auth'
import { useAllToolProviders } from '@/service/use-tools'
const i18nPrefix = 'plugin.action'
@ -75,7 +77,14 @@ const DetailHeader = ({
meta,
plugin_id,
} = detail
const { author, category, name, label, description, icon, verified } = detail.declaration
const { author, category, name, label, description, icon, verified, tool } = detail.declaration
const isTool = category === PluginType.tool
const providerBriefInfo = tool?.identity
const providerKey = `${plugin_id}/${providerBriefInfo?.name}`
const { data: collectionList = [] } = useAllToolProviders(isTool)
const provider = useMemo(() => {
return collectionList.find(collection => collection.name === providerKey)
}, [collectionList, providerKey])
const isFromGitHub = source === PluginSource.github
const isFromMarketplace = source === PluginSource.marketplace
@ -295,6 +304,13 @@ const DetailHeader = ({
</div>
</div>
<Description className='mt-3' text={description[locale]} descriptionLineRows={2}></Description>
{
category === PluginType.tool && (
<PluginAuth
provider={provider?.name}
/>
)
}
{isShowPluginInfo && (
<PluginInfo
repository={isFromGitHub ? meta?.repo : ''}

@ -34,6 +34,7 @@ export type ToolDefaultValue = {
paramSchemas: Record<string, any>[]
output_schema: Record<string, any>
meta?: PluginMeta
credential_id?: string
}
export type ToolValue = {

@ -462,6 +462,7 @@ const formatItem = (
return {
variable: `env.${env.name}`,
type: env.value_type,
description: env.description,
}
}) as Var[]
break
@ -472,7 +473,7 @@ const formatItem = (
return {
variable: `conversation.${chatVar.name}`,
type: chatVar.value_type,
des: chatVar.description,
description: chatVar.description,
}
}) as Var[]
break

@ -59,6 +59,11 @@ import { useLogs } from '@/app/components/workflow/run/hooks'
import PanelWrap from '../before-run-form/panel-wrap'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import {
AuthorizedInNode,
PluginAuth,
} from '@/app/components/plugins/plugin-auth'
import { canFindTool } from '@/utils'
type BasePanelProps = {
children: ReactNode
@ -215,6 +220,22 @@ const BasePanel: FC<BasePanelProps> = ({
return {}
})()
const buildInTools = useStore(s => s.buildInTools)
const currCollection = useMemo(() => {
return buildInTools.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, data.provider_id])
const showPluginAuth = useMemo(() => {
return data.type === BlockEnum.Tool && currCollection?.allow_delete && !currCollection.is_team_authorization
}, [currCollection, data.type])
const handleAuthorizationItemClick = useCallback((id: string) => {
handleNodeDataUpdate({
id,
data: {
credential_id: id === '__workspace_default__' ? undefined : id,
},
})
}, [handleNodeDataUpdate])
if(logParams.showSpecialResultPanel) {
return (
<div className={cn(
@ -347,12 +368,39 @@ const BasePanel: FC<BasePanelProps> = ({
onChange={handleDescriptionChange}
/>
</div>
{
showPluginAuth && (
<PluginAuth
provider={currCollection?.name}
>
<div className='pl-4'>
<Tab
value={tabType}
onChange={setTabType}
/>
</div>
</PluginAuth>
)
}
{
!showPluginAuth && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}
onChange={setTabType}
/>
{
currCollection?.allow_delete && (
<AuthorizedInNode
provider={currCollection?.name}
onAuthorizationItemClick={handleAuthorizationItemClick}
credentialId={data.credential_id}
/>
)
}
</div>
)
}
<Split />
</div>

@ -22,6 +22,7 @@ const nodeDefault: NodeDefault<HttpNodeType> = {
type: BodyType.none,
data: [],
},
ssl_verify: true,
timeout: {
max_connect_timeout: 0,
max_read_timeout: 0,

@ -10,6 +10,7 @@ import type { HttpNodeType } from './types'
import Timeout from './components/timeout'
import CurlPanel from './components/curl-panel'
import cn from '@/utils/classnames'
import Switch from '@/app/components/base/switch'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
@ -47,6 +48,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
showCurlPanel,
hideCurlPanel,
handleCurlImport,
handleSSLVerifyChange,
} = useConfig(id, data)
// To prevent prompt editor in body not update data.
if (!isDataReady)
@ -124,6 +126,18 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
onChange={setBody}
/>
</Field>
<Field
title={t(`${i18nPrefix}.verifySSL.title`)}
tooltip={t(`${i18nPrefix}.verifySSL.warningTooltip`)}
operations={
<Switch
defaultValue={!!inputs.ssl_verify}
onChange={handleSSLVerifyChange}
size='md'
disabled={readOnly}
/>
}>
</Field>
</div>
<Split />
<Timeout

@ -81,4 +81,5 @@ export type HttpNodeType = CommonNodeType & {
body: Body
authorization: Authorization
timeout: Timeout
ssl_verify?: boolean
}

@ -141,6 +141,13 @@ const useConfig = (id: string, payload: HttpNodeType) => {
setInputs(newInputs)
}, [inputs, setInputs])
const handleSSLVerifyChange = useCallback((checked: boolean) => {
const newInputs = produce(inputs, (draft: HttpNodeType) => {
draft.ssl_verify = checked
})
setInputs(newInputs)
}, [inputs, setInputs])
return {
readOnly,
isDataReady,
@ -164,6 +171,8 @@ const useConfig = (id: string, payload: HttpNodeType) => {
toggleIsParamKeyValueEdit,
// body
setBody,
// ssl verify
handleSSLVerifyChange,
// authorization
isShowAuthorization,
showAuthorization,

@ -5,10 +5,8 @@ import Split from '../_base/components/split'
import type { ToolNodeType } from './types'
import useConfig from './use-config'
import ToolForm from './components/tool-form'
import Button from '@/app/components/base/button'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import type { NodePanelProps } from '@/app/components/workflow/types'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import Loading from '@/app/components/base/loading'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
@ -32,10 +30,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
setToolSettingValue,
currCollection,
isShowAuthBtn,
showSetAuth,
showSetAuthModal,
hideSetAuthModal,
handleSaveAuth,
isLoading,
outputSchema,
hasObjectOutput,
@ -52,21 +46,8 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
return (
<div className='pt-2'>
{!readOnly && isShowAuthBtn && (
<>
<div className='px-4'>
<Button
variant='primary'
className='w-full'
onClick={showSetAuthModal}
>
{t(`${i18nPrefix}.authorize`)}
</Button>
</div>
</>
)}
{!isShowAuthBtn && (
<div className='relative'>
{!isShowAuthBtn && <>
<div className='space-y-4 px-4'>
{toolInputVarSchema.length > 0 && (
<Field
className='px-4'
@ -107,17 +88,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
</>
)}
</div>
)}
{showSetAuth && (
<ConfigCredential
collection={currCollection!}
onCancel={hideSetAuthModal}
onSaved={handleSaveAuth}
isHideRemoveBtn
/>
)}
</>}
<div>
<OutputVars>
<>

@ -13,7 +13,7 @@ import {
toolParametersToFormSchemas,
} from '@/app/components/tools/utils/to-form-schema'
import Toast from '@/app/components/base/toast'
import type { InputVar, Var } from '@/app/components/workflow/types'
import type { InputVar } from '@/app/components/workflow/types'
import {
useFetchToolsData,
useNodesReadOnly,

@ -80,7 +80,7 @@ const ChatVariableModal = ({
const [objectValue, setObjectValue] = React.useState<ObjectValueItem[]>([DEFAULT_OBJECT_VALUE])
const [editorContent, setEditorContent] = React.useState<string>()
const [editInJSON, setEditInJSON] = React.useState(false)
const [des, setDes] = React.useState<string>('')
const [description, setDescription] = React.useState<string>('')
const editorMinHeight = useMemo(() => {
if (type === ChatVarType.ArrayObject)
@ -237,7 +237,7 @@ const ChatVariableModal = ({
name,
value_type: type,
value: formatValue(value),
description: des,
description,
})
onClose()
}
@ -247,7 +247,7 @@ const ChatVariableModal = ({
setName(chatVar.name)
setType(chatVar.value_type)
setValue(chatVar.value)
setDes(chatVar.description)
setDescription(chatVar.description)
setObjectValue(getObjectValue())
if (chatVar.value_type === ChatVarType.ArrayObject) {
setEditorContent(JSON.stringify(chatVar.value))
@ -385,9 +385,9 @@ const ChatVariableModal = ({
<div className='flex'>
<textarea
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
value={des}
value={description}
placeholder={t('workflow.chatVariable.modal.descriptionPlaceholder') || ''}
onChange={e => setDes(e.target.value)}
onChange={e => setDescription(e.target.value)}
/>
</div>
</div>

@ -22,9 +22,10 @@ const EnvItem = ({
return (
<div className={cn(
'radius-md mb-1 border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2.5 py-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
'radius-md group mb-1 border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
destructive && 'border-state-destructive-border hover:bg-state-destructive-hover',
)}>
<div className='px-2.5 py-2'>
<div className='flex items-center justify-between'>
<div className='flex grow items-center gap-1'>
<Env className='h-4 w-4 text-util-colors-violet-violet-600' />
@ -47,6 +48,17 @@ const EnvItem = ({
</div>
<div className='system-xs-regular truncate text-text-tertiary'>{env.value_type === 'secret' ? envSecrets[env.id] : env.value}</div>
</div>
{env.description && (
<>
<div className={'h-[0.5px] bg-divider-subtle'} />
<div className={cn('rounded-bl-[8px] rounded-br-[8px] bg-background-default-subtle px-2.5 py-2 group-hover:bg-transparent',
destructive && 'bg-state-destructive-hover hover:bg-state-destructive-hover',
)}>
<div className='system-xs-regular truncate text-text-tertiary'>{env.description}</div>
</div>
</>
)}
</div>
)
}

@ -29,6 +29,7 @@ const VariableModal = ({
const [type, setType] = React.useState<'string' | 'number' | 'secret'>('string')
const [name, setName] = React.useState('')
const [value, setValue] = React.useState<any>()
const [description, setDescription] = React.useState<string>('')
const checkVariableName = (value: string) => {
const { isValid, errorMessageKey } = checkKeys([value], false)
@ -67,6 +68,7 @@ const VariableModal = ({
value_type: type,
name,
value: type === 'number' ? Number(value) : value,
description,
})
onClose()
}
@ -76,6 +78,7 @@ const VariableModal = ({
setType(env.value_type)
setName(env.name)
setValue(env.value_type === 'secret' ? envSecrets[env.id] : env.value)
setDescription(env.description)
}
}, [env, envSecrets])
@ -141,7 +144,7 @@ const VariableModal = ({
</div>
</div>
{/* value */}
<div className=''>
<div className='mb-4'>
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.value')}</div>
<div className='flex'>
{
@ -160,6 +163,18 @@ const VariableModal = ({
}
</div>
</div>
{/* description */}
<div className=''>
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.description')}</div>
<div className='flex'>
<textarea
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
value={description}
placeholder={t('workflow.env.modal.descriptionPlaceholder') || ''}
onChange={e => setDescription(e.target.value)}
/>
</div>
</div>
</div>
<div className='flex flex-row-reverse rounded-b-2xl p-4 pt-2'>
<div className='flex gap-2'>

@ -93,7 +93,7 @@ export type CommonNodeType<T = {}> = {
error_strategy?: ErrorHandleTypeEnum
retry_config?: WorkflowRetryConfig
default_value?: DefaultValueForm[]
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name' | 'credential_id'>>
export type CommonEdgeType = {
_hovering?: boolean
@ -150,6 +150,7 @@ export type EnvironmentVariable = {
name: string
value: any
value_type: 'string' | 'number' | 'secret'
description: string
}
export type ConversationVariable = {

@ -44,6 +44,7 @@ const LocaleLayout = async ({
[DatasetAttr.DATA_PUBLIC_LOOP_NODE_MAX_COUNT]: process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT,
[DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM]: process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM,
[DatasetAttr.DATA_PUBLIC_MAX_TREE_DEPTH]: process.env.NEXT_PUBLIC_MAX_TREE_DEPTH,
[DatasetAttr.DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME]: process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME,
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER,
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL,
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL,

@ -270,6 +270,7 @@ export const LOOP_NODE_MAX_COUNT = getNumberConfig(process.env.NEXT_PUBLIC_LOOP_
export const MAX_ITERATIONS_NUM = getNumberConfig(process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM, DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM, 99)
export const MAX_TREE_DEPTH = getNumberConfig(process.env.NEXT_PUBLIC_MAX_TREE_DEPTH, DatasetAttr.DATA_PUBLIC_MAX_TREE_DEPTH, 50)
export const ALLOW_UNSAFE_DATA_SCHEME = getBooleanConfig(process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME, DatasetAttr.DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME, false)
export const ENABLE_WEBSITE_JINAREADER = getBooleanConfig(process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER, DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER, true)
export const ENABLE_WEBSITE_FIRECRAWL = getBooleanConfig(process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL, DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL, true)
export const ENABLE_WEBSITE_WATERCRAWL = getBooleanConfig(process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL, DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL, false)

@ -26,6 +26,7 @@ export NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED}
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
export NEXT_PUBLIC_ALLOW_EMBED=${ALLOW_EMBED}
export NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=${ALLOW_UNSAFE_DATA_SCHEME:-false}
export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE}
export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
export NEXT_PUBLIC_MAX_TOOLS_NUM=${MAX_TOOLS_NUM}

@ -129,6 +129,8 @@ const translation = {
value: 'Wert',
valuePlaceholder: 'Umgebungswert',
secretTip: 'Wird verwendet, um sensible Informationen oder Daten zu definieren, wobei DSL-Einstellungen zur Verhinderung von Lecks konfiguriert sind.',
description: 'Beschreibung',
descriptionPlaceholder: 'Beschreiben Sie die Variable',
},
export: {
title: 'Geheime Umgebungsvariablen exportieren?',
@ -537,6 +539,10 @@ const translation = {
title: 'Importieren von cURL',
placeholder: 'Fügen Sie hier die cURL-Zeichenfolge ein',
},
verifySSL: {
title: 'SSL-Zertifikat überprüfen',
warningTooltip: 'Das Deaktivieren der SSL-Überprüfung wird für Produktionsumgebungen nicht empfohlen. Dies sollte nur in der Entwicklung oder im Test verwendet werden, da es die Verbindung anfällig für Sicherheitsbedrohungen wie Man-in-the-Middle-Angriffe macht.',
},
},
code: {
inputVars: 'Eingabevariablen',

@ -127,6 +127,8 @@ const translation = {
value: 'Value',
valuePlaceholder: 'env value',
secretTip: 'Used to define sensitive information or data, with DSL settings configured for leak prevention.',
description: 'Description',
descriptionPlaceholder: 'Describe the variable',
},
export: {
title: 'Export Secret environment variables?',
@ -548,6 +550,10 @@ const translation = {
title: 'Import from cURL',
placeholder: 'Paste cURL string here',
},
verifySSL: {
title: 'Verify SSL Certificate',
warningTooltip: 'Disabling SSL verification is not recommended for production environments. This should only be used in development or testing, as it makes the connection vulnerable to security threats like man-in-the-middle attacks.',
},
},
code: {
inputVars: 'Input Variables',

@ -129,6 +129,8 @@ const translation = {
value: 'Valor',
valuePlaceholder: 'valor de env',
secretTip: 'Se utiliza para definir información o datos sensibles, con configuraciones DSL configuradas para prevenir fugas.',
description: 'Descripción',
descriptionPlaceholder: 'Describa la variable',
},
export: {
title: '¿Exportar variables de entorno secretas?',
@ -535,6 +537,10 @@ const translation = {
title: 'Importar desde cURL',
placeholder: 'Pegar la cadena cURL aquí',
},
verifySSL: {
title: 'Verificar el certificado SSL',
warningTooltip: 'Deshabilitar la verificación SSL no se recomienda para entornos de producción. Esto solo debe utilizarse en desarrollo o pruebas, ya que hace que la conexión sea vulnerable a amenazas de seguridad como ataques de intermediario.',
},
},
code: {
inputVars: 'Variables de entrada',

@ -129,6 +129,8 @@ const translation = {
value: 'مقدار',
valuePlaceholder: 'مقدار متغیر',
secretTip: 'برای تعریف اطلاعات حساس یا داده‌ها، با تنظیمات DSL برای جلوگیری از نشت پیکربندی شده است.',
description: 'توضیحات',
descriptionPlaceholder: 'متغیر را توصیف کنید',
},
export: {
title: 'آیا متغیرهای محیطی مخفی را صادر کنید؟',
@ -537,6 +539,10 @@ const translation = {
title: 'وارد کردن از cURL',
placeholder: 'رشته cURL را اینجا بچسبانید',
},
verifySSL: {
title: 'گواهی SSL را تأیید کنید',
warningTooltip: 'غیرفعال کردن تأیید SSL برای محیط‌های تولید توصیه نمی‌شود. این فقط باید در توسعه یا آزمایش استفاده شود، زیرا این کار اتصال را در معرض تهدیدات امنیتی مانند حملات میانی قرار می‌دهد.',
},
},
code: {
inputVars: 'متغیرهای ورودی',

@ -129,6 +129,8 @@ const translation = {
value: 'valeur',
valuePlaceholder: 'Valeur de l\'env',
secretTip: 'Utilisé pour définir des informations ou des données sensibles, avec des paramètres DSL configurés pour la prévention des fuites.',
description: 'Description',
descriptionPlaceholder: 'Décrivez la variable',
},
export: {
title: 'Exporter des variables d\'environnement secrètes?',
@ -537,6 +539,10 @@ const translation = {
placeholder: 'Collez la chaîne cURL ici',
title: 'Importer à partir de cURL',
},
verifySSL: {
title: 'Vérifier le certificat SSL',
warningTooltip: 'Désactiver la vérification SSL n\'est pas recommandé pour les environnements de production. Cela ne devrait être utilisé que dans le développement ou les tests, car cela rend la connexion vulnérable aux menaces de sécurité telles que les attaques de type \'man-in-the-middle\'.',
},
},
code: {
inputVars: 'Variables de saisie',

@ -132,6 +132,8 @@ const translation = {
value: 'मान',
valuePlaceholder: 'पर्यावरण मान',
secretTip: 'संवेदनशील जानकारी या डेटा को परिभाषित करने के लिए उपयोग किया जाता है, DSL सेटिंग्स लीक रोकथाम के लिए कॉन्फ़िगर की गई हैं।',
description: 'विवरण',
descriptionPlaceholder: 'चर का वर्णन करें',
},
export: {
title: 'गुप्त पर्यावरण चर निर्यात करें?',
@ -550,6 +552,10 @@ const translation = {
placeholder: 'यहां cURL स्ट्रिंग पेस्ट करें',
title: 'cURL से आयात करें',
},
verifySSL: {
title: 'SSL प्रमाणपत्र की पुष्टि करें',
warningTooltip: 'SSL सत्यापन को अक्षम करना उत्पादन वातावरण के लिए अनुशंसित नहीं है। इसका उपयोग केवल विकास या परीक्षण में किया जाना चाहिए, क्योंकि यह कनेक्शन को मिडल-मैन हमलों जैसे सुरक्षा खतरों के लिए कमजोर बना देता है।',
},
},
code: {
inputVars: 'इनपुट वेरिएबल्स',

@ -133,6 +133,8 @@ const translation = {
value: 'Valore',
valuePlaceholder: 'valore env',
secretTip: 'Utilizzato per definire informazioni o dati sensibili, con impostazioni DSL configurate per la prevenzione delle fughe.',
description: 'Descrizione',
descriptionPlaceholder: 'Descrivi la variabile',
},
export: {
title: 'Esportare variabili d\'ambiente segrete?',
@ -553,6 +555,10 @@ const translation = {
placeholder: 'Incolla qui la stringa cURL',
title: 'Importazione da cURL',
},
verifySSL: {
title: 'Verifica il certificato SSL',
warningTooltip: 'Disabilitare la verifica SSL non è raccomandato per gli ambienti di produzione. Questo dovrebbe essere utilizzato solo in sviluppo o test, poiché rende la connessione vulnerabile a minacce alla sicurezza come gli attacchi man-in-the-middle.',
},
},
code: {
inputVars: 'Variabili di Input',

@ -127,6 +127,8 @@ const translation = {
value: '値',
valuePlaceholder: '変数値を入力',
secretTip: 'この変数は機密情報やデータを定義するために使用されます。DSL をエクスポートするときに漏洩防止メカニズムを設定されます。',
description: '説明',
descriptionPlaceholder: '変数の説明を入力',
},
export: {
title: 'シークレット環境変数をエクスポートしますか?',
@ -543,6 +545,10 @@ const translation = {
title: 'cURL からインポート',
placeholder: 'ここに cURL 文字列を貼り付けます',
},
verifySSL: {
title: 'SSL証明書を確認する',
warningTooltip: 'SSL検証を無効にすることは、本番環境では推奨されません。これは開発またはテストのみに使用すべきであり、中間者攻撃などのセキュリティ脅威に対して接続を脆弱にするためです。',
},
},
code: {
inputVars: '入力変数',

@ -135,6 +135,8 @@ const translation = {
valuePlaceholder: '환경 값',
secretTip:
'민감한 정보나 데이터를 정의하는 데 사용되며, DSL 설정은 유출 방지를 위해 구성됩니다.',
description: '설명',
descriptionPlaceholder: '변수에 대해 설명하세요',
},
export: {
title: '비밀 환경 변수를 내보내시겠습니까?',
@ -565,6 +567,10 @@ const translation = {
title: 'cURL 에서 가져오기',
placeholder: '여기에 cURL 문자열 붙여 넣기',
},
verifySSL: {
title: 'SSL 인증서 확인',
warningTooltip: 'SSL 검증을 비활성화하는 것은 프로덕션 환경에서는 권장되지 않습니다. 이는 연결이 중간자 공격과 같은 보안 위협에 취약하게 만들므로 개발 또는 테스트에서만 사용해야 합니다.',
},
},
code: {
inputVars: '입력 변수',

@ -129,6 +129,8 @@ const translation = {
value: 'Wartość',
valuePlaceholder: 'wartość środowiska',
secretTip: 'Używane do definiowania wrażliwych informacji lub danych, z ustawieniami DSL skonfigurowanymi do zapobiegania wyciekom.',
description: 'Opis',
descriptionPlaceholder: 'Opisz zmienną',
},
export: {
title: 'Eksportować tajne zmienne środowiskowe?',
@ -537,6 +539,10 @@ const translation = {
placeholder: 'Wklej tutaj ciąg cURL',
title: 'Importowanie z cURL',
},
verifySSL: {
title: 'Zweryfikuj certyfikat SSL',
warningTooltip: 'Wyłączenie weryfikacji SSL nie jest zalecane w środowiskach produkcyjnych. Powinno to być używane tylko w rozwoju lub testowaniu, ponieważ naraża połączenie na zagrożenia bezpieczeństwa, takie jak ataki typu man-in-the-middle.',
},
},
code: {
inputVars: 'Zmienne wejściowe',

@ -129,6 +129,8 @@ const translation = {
value: 'Valor',
valuePlaceholder: 'valor da env',
secretTip: 'Usado para definir informações ou dados sensíveis, com configurações DSL configuradas para prevenção de vazamentos.',
description: 'Descrição',
descriptionPlaceholder: 'Descreva a variável',
},
export: {
title: 'Exportar variáveis de ambiente secretas?',
@ -537,6 +539,10 @@ const translation = {
placeholder: 'Cole a string cURL aqui',
title: 'Importar do cURL',
},
verifySSL: {
title: 'Verificar o certificado SSL',
warningTooltip: 'Desabilitar a verificação SSL não é recomendado para ambientes de produção. Isso deve ser usado apenas em desenvolvimento ou teste, pois torna a conexão vulnerável a ameaças de segurança, como ataques man-in-the-middle.',
},
},
code: {
inputVars: 'Variáveis de entrada',

@ -129,6 +129,8 @@ const translation = {
value: 'Valoare',
valuePlaceholder: 'valoare mediu',
secretTip: 'Utilizat pentru a defini informații sau date sensibile, cu setări DSL configurate pentru prevenirea scurgerilor.',
description: 'Descriere',
descriptionPlaceholder: 'Descrieți variabila',
},
export: {
title: 'Exportă variabile de mediu secrete?',
@ -537,6 +539,10 @@ const translation = {
placeholder: 'Lipiți șirul cURL aici',
title: 'Importați din cURL',
},
verifySSL: {
title: 'Verifică certificatul SSL',
warningTooltip: 'Dezactivarea verificării SSL nu este recomandată pentru medii de producție. Acest lucru ar trebui să fie folosit doar în dezvoltare sau testare, deoarece face conexiunea vulnerabilă la amenințări de securitate, cum ar fi atacurile man-in-the-middle.',
},
},
code: {
inputVars: 'Variabile de intrare',

@ -129,6 +129,8 @@ const translation = {
value: 'Значение',
valuePlaceholder: 'Значение переменной среды',
secretTip: 'Используется для определения конфиденциальной информации или данных, с настройками DSL, настроенными для предотвращения утечки.',
description: 'Описание',
descriptionPlaceholder: 'Опишите переменную',
},
export: {
title: 'Экспортировать секретные переменные среды?',
@ -537,6 +539,10 @@ const translation = {
placeholder: 'Вставьте сюда строку cURL',
title: 'Импорт из cURL',
},
verifySSL: {
title: 'Проверить SSL-сертификат',
warningTooltip: 'Отключение проверки SSL не рекомендуется для производственных сред. Это следует использовать только в разработке или тестировании, так как это делает соединение уязвимым для угроз безопасности, таких как атаки «человек посередине».',
},
},
code: {
inputVars: 'Входные переменные',

@ -124,6 +124,8 @@ const translation = {
type: 'Tip',
editTitle: 'Uredi okoljsko spremenljivko',
secretTip: 'Uporablja se za opredelitev občutljivih informacij ali podatkov, s konfiguriranimi nastavitvami DSL za preprečevanje puščanja.',
description: 'Opis',
descriptionPlaceholder: 'Opisujte spremenljivko',
},
export: {
export: 'Izvozi DSL z skrivnimi vrednostmi',
@ -143,6 +145,7 @@ const translation = {
objectKey: 'Ključ',
valuePlaceholder: 'Privzeta vrednost, pustite prazno, da je ne nastavite',
description: 'Opis',
descriptionPlaceholder: 'Opisujte spremenljivko',
type: 'Tip',
value: 'Privzeta vrednost',
name: 'Ime',
@ -153,7 +156,6 @@ const translation = {
objectType: 'Tip',
oneByOne: 'Dodaj eno po eno',
objectValue: 'Privzeta vrednost',
descriptionPlaceholder: 'Opisujte spremenljivko',
},
updatedAt: 'Posodobljeno ob',
docLink: 'Obiščite našo dokumentacijo, da se naučite več.',
@ -539,6 +541,10 @@ const translation = {
value: 'Vrednost',
params: 'Parametri',
insertVarPlaceholder: 'vnesite \'/\' za vstavljanje spremenljivke',
verifySSL: {
title: 'Preverite SSL certifikat',
warningTooltip: 'Onemogočanje preverjanja SSL ni priporočljivo za proizvodna okolja. To bi se moralo uporabljati le pri razvoju ali testiranju, saj povezavo izpostavi varnostnim grožnjam, kot so napadi človek-v-sredini.',
},
},
code: {
searchDependencies: 'Išči odvisnosti',

@ -129,6 +129,8 @@ const translation = {
value: 'ค่า',
valuePlaceholder: 'ค่า env',
secretTip: 'ใช้เพื่อกําหนดข้อมูลหรือข้อมูลที่ละเอียดอ่อน โดยมีการตั้งค่า DSL ที่กําหนดค่าไว้เพื่อป้องกันการรั่วไหล',
description: 'คำอธิบาย',
descriptionPlaceholder: 'อธิบายตัวแปร',
},
export: {
title: 'ส่งออกตัวแปรสภาพแวดล้อม Secret หรือไม่',
@ -537,6 +539,10 @@ const translation = {
title: 'นําเข้าจาก cURL',
placeholder: 'วางสตริง cURL ที่นี่',
},
verifySSL: {
title: 'ตรวจสอบใบรับรอง SSL',
warningTooltip: 'การปิดการตรวจสอบ SSL ไม่แนะนำให้ใช้ในสภาพแวดล้อมการผลิต ควรใช้เฉพาะในระหว่างการพัฒนาหรือการทดสอบเท่านั้น เนื่องจากจะทำให้การเชื่อมต่อมีความเสี่ยงต่อภัยคุกคามด้านความปลอดภัย เช่น การโจมตีแบบ Man-in-the-middle.',
},
},
code: {
inputVars: 'ตัวแปรอินพุต',

@ -129,6 +129,8 @@ const translation = {
value: 'Değer',
valuePlaceholder: 'env değeri',
secretTip: 'Hassas bilgileri veya verileri tanımlamak için kullanılır, bilgi sızıntısını önlemek için DSL ayarları yapılandırılmıştır.',
description: 'Açıklama',
descriptionPlaceholder: 'Değişkeni açıklayın',
},
export: {
title: 'Gizli çevre değişkenleri dışa aktarılsın mı?',
@ -538,6 +540,10 @@ const translation = {
placeholder: 'cURL dizesini buraya yapıştırın',
title: 'cURL\'den içe aktar',
},
verifySSL: {
title: 'SSL Sertifikasını Doğrula',
warningTooltip: 'SSL doğrulamasını devre dışı bırakmak, üretim ortamları için önerilmez. Bu yalnızca geliştirme veya test aşamalarında kullanılmalıdır, çünkü bağlantıyı adam ortada saldırıları gibi güvenlik tehditlerine karşı savunmasız hale getirir.',
},
},
code: {
inputVars: 'Giriş Değişkenleri',

@ -129,6 +129,8 @@ const translation = {
value: 'Значення',
valuePlaceholder: 'значення середовища',
secretTip: 'Використовується для визначення конфіденційної інформації або даних, з налаштуваннями DSL, сконфігурованими для запобігання витоку.',
description: 'Опис',
descriptionPlaceholder: 'Опишіть змінну',
},
export: {
title: 'Експортувати секретні змінні середовища?',
@ -537,6 +539,10 @@ const translation = {
title: 'Імпорт з cURL',
placeholder: 'Вставте сюди рядок cURL',
},
verifySSL: {
title: 'Перевірити SSL сертифікат',
warningTooltip: 'Вимкнення перевірки SSL не рекомендується для виробничих середовищ. Це слід використовувати лише в розробці або тестуванні, оскільки це робить з\'єднання вразливим до загроз безпеці, таких як атаки «людина посередині».',
},
},
code: {
inputVars: 'Вхідні змінні',

@ -129,6 +129,8 @@ const translation = {
value: 'Giá trị',
valuePlaceholder: 'giá trị môi trường',
secretTip: 'Được sử dụng để xác định thông tin hoặc dữ liệu nhạy cảm, với cài đặt DSL được cấu hình để ngăn chặn rò rỉ.',
description: 'Mô tả',
descriptionPlaceholder: 'Mô tả biến',
},
export: {
title: 'Xuất biến môi trường bí mật?',
@ -537,6 +539,10 @@ const translation = {
title: 'Nhập từ cURL',
placeholder: 'Dán chuỗi cURL vào đây',
},
verifySSL: {
title: 'Xác thực chứng chỉ SSL',
warningTooltip: 'Việc vô hiệu hóa xác minh SSL không được khuyến khích cho các môi trường sản xuất. Điều này chỉ nên được sử dụng trong phát triển hoặc thử nghiệm, vì nó làm cho kết nối dễ bị tổn thương trước các mối đe dọa an ninh như cuộc tấn công man-in-the-middle.',
},
},
code: {
inputVars: 'Biến đầu vào',

@ -127,6 +127,8 @@ const translation = {
value: '值',
valuePlaceholder: '变量值',
secretTip: '用于定义敏感信息或数据,导出 DSL 时设置了防泄露机制。',
description: '描述',
descriptionPlaceholder: '变量的描述',
},
export: {
title: '导出 Secret 类型环境变量?',
@ -549,6 +551,10 @@ const translation = {
title: '导入 cURL',
placeholder: '粘贴 cURL 字符串',
},
verifySSL: {
title: '验证 SSL 证书',
warningTooltip: '不建议在生产环境中禁用 SSL 验证。这仅应在开发或测试中使用,因为它会使连接容易受到诸如中间人攻击等安全威胁。',
},
},
code: {
inputVars: '输入变量',

@ -129,6 +129,8 @@ const translation = {
value: '值',
valuePlaceholder: '環境值',
secretTip: '用於定義敏感信息或數據DSL 設置配置為防止洩露。',
description: '描述',
descriptionPlaceholder: '描述此變數',
},
export: {
title: '導出機密環境變數?',
@ -537,6 +539,10 @@ const translation = {
placeholder: '在此處粘貼 cURL 字串',
title: '從 cURL 導入',
},
verifySSL: {
title: '驗證 SSL 證書',
warningTooltip: '不建議在生產環境中禁用SSL驗證。這僅應用於開發或測試因為這樣會使連接容易受到中間人攻擊等安全威脅的威脅。',
},
},
code: {
inputVars: '輸入變量',

@ -0,0 +1,139 @@
import {
useMutation,
useQuery,
} from '@tanstack/react-query'
import { get, post } from './base'
import { useInvalid } from './use-base'
import type {
Credential,
CredentialTypeEnum,
} from '@/app/components/plugins/plugin-auth/types'
import type { FormSchema } from '@/app/components/base/form/types'
const NAME_SPACE = 'plugins-auth'
export const useGetPluginToolCredentialInfo = (
provider: string,
) => {
return useQuery({
enabled: !!provider,
queryKey: [NAME_SPACE, 'credential-info', provider],
queryFn: () => get<{
supported_credential_types: string[]
credentials: Credential[]
is_oauth_custom_client_enabled: boolean
}>(`/workspaces/current/tool-provider/builtin/${provider}/credential/info`),
staleTime: 0,
})
}
export const useInvalidPluginToolCredentialInfo = (
provider: string,
) => {
return useInvalid([NAME_SPACE, 'credential-info', provider])
}
export const useSetPluginToolDefaultCredential = (
provider: string,
) => {
return useMutation({
mutationFn: (id: string) => {
return post(`/workspaces/current/tool-provider/builtin/${provider}/default-credential`, { body: { id } })
},
})
}
export const useGetPluginToolCredentialList = (
provider: string,
) => {
return useQuery({
queryKey: [NAME_SPACE, 'credential-list', provider],
queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/credentials`),
})
}
export const useAddPluginToolCredential = (
provider: string,
) => {
return useMutation({
mutationFn: (params: {
credentials: Record<string, any>
type: CredentialTypeEnum
name?: string
}) => {
return post(`/workspaces/current/tool-provider/builtin/${provider}/add`, { body: params })
},
})
}
export const useUpdatePluginToolCredential = (
provider: string,
) => {
return useMutation({
mutationFn: (params: {
credential_id: string
credentials: Record<string, any>
type: CredentialTypeEnum
name?: string
}) => {
return post(`/workspaces/current/tool-provider/builtin/${provider}/update`, { body: params })
},
})
}
export const useDeletePluginToolCredential = (
provider: string,
) => {
return useMutation({
mutationFn: (params: { credential_id: string }) => {
return post(`/workspaces/current/tool-provider/builtin/${provider}/delete`, { body: params })
},
})
}
export const useGetPluginToolCredentialSchema = (
provider: string,
credential_type: CredentialTypeEnum,
) => {
return useQuery({
queryKey: [NAME_SPACE, 'credential-schema', provider, credential_type],
queryFn: () => get<FormSchema[]>(`/workspaces/current/tool-provider/builtin/${provider}/credential/schema/${credential_type}`),
})
}
export const useGetPluginToolOAuthUrl = (
provider: string,
) => {
return useQuery({
queryKey: [NAME_SPACE, 'oauth-url', provider],
queryFn: () => get(`oauth/plugin/${provider}/tool/authorization-url`),
})
}
export const useGetPluginToolOAuthClientSchema = (
provider: string,
) => {
return useQuery({
queryKey: [NAME_SPACE, 'oauth-client-schema', provider],
queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`),
})
}
export const useSetPluginToolOAuthCustomClient = (
provider: string,
) => {
return useMutation({
mutationFn: (params) => {
return post(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, { body: params })
},
})
}
export const useGetPluginToolOAuthCustomClientSchema = (
provider: string,
) => {
return useQuery({
queryKey: [NAME_SPACE, 'oauth-custom-client-schema', provider],
queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`),
})
}

@ -16,10 +16,11 @@ import {
const NAME_SPACE = 'tools'
const useAllToolProvidersKey = [NAME_SPACE, 'allToolProviders']
export const useAllToolProviders = () => {
export const useAllToolProviders = (enabled = true) => {
return useQuery<Collection[]>({
queryKey: useAllToolProvidersKey,
queryFn: () => get<Collection[]>('/workspaces/current/tool-providers'),
enabled,
})
}

@ -116,6 +116,7 @@ export enum DatasetAttr {
DATA_PUBLIC_LOOP_NODE_MAX_COUNT = 'data-public-loop-node-max-count',
DATA_PUBLIC_MAX_ITERATIONS_NUM = 'data-public-max-iterations-num',
DATA_PUBLIC_MAX_TREE_DEPTH = 'data-public-max-tree-depth',
DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME = 'data-public-allow-unsafe-data-scheme',
DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER = 'data-public-enable-website-jinareader',
DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL = 'data-public-enable-website-firecrawl',
DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL = 'data-public-enable-website-watercrawl',

Loading…
Cancel
Save