diff --git a/web/app/components/header/account-setting/sandbox-provider-page/config-modal.tsx b/web/app/components/header/account-setting/sandbox-provider-page/config-modal.tsx
index 35ffed5260..99dcbeec51 100644
--- a/web/app/components/header/account-setting/sandbox-provider-page/config-modal.tsx
+++ b/web/app/components/header/account-setting/sandbox-provider-page/config-modal.tsx
@@ -3,29 +3,66 @@
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
import type { SandboxProvider } from '@/types/sandbox-provider'
import { RiExternalLinkLine, RiLock2Fill } from '@remixicon/react'
-import { memo, useCallback, useMemo, useRef } from 'react'
+import { memo, useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { BaseForm } from '@/app/components/base/form/components/base'
import { FormTypeEnum } from '@/app/components/base/form/types'
import Modal from '@/app/components/base/modal'
+import RadioUI from '@/app/components/base/radio/ui'
import { useToastContext } from '@/app/components/base/toast'
import {
useDeleteSandboxProviderConfig,
useSaveSandboxProviderConfig,
} from '@/service/use-sandbox-provider'
+import { cn } from '@/utils/classnames'
import { PROVIDER_DOC_LINKS, PROVIDER_LABEL_KEYS, SANDBOX_FIELD_CONFIGS } from './constants'
import ProviderIcon from './provider-icon'
+type ConfigMode = 'managed' | 'byok'
+
+// Providers that support mode selection (must have system config available)
+const PROVIDERS_WITH_MODE_SELECTION: readonly string[] = ['e2b']
+
+type ModeOptionProps = {
+ isSelected: boolean
+ isDisabled?: boolean
+ title: string
+ description: string
+ onClick: () => void
+}
+
+function ModeOption({ isSelected, isDisabled = false, title, description, onClick }: ModeOptionProps) {
+ return (
+
!isDisabled && onClick()}
+ >
+
+
+
+
+ {title}
+ {description}
+
+
+ )
+}
+
type ConfigModalProps = {
provider: SandboxProvider
onClose: () => void
}
-const ConfigModal = ({
- provider,
- onClose,
-}: ConfigModalProps) => {
+function ConfigModal({ provider, onClose }: ConfigModalProps) {
const { t } = useTranslation()
const { notify } = useToastContext()
const formRef = useRef(null)
@@ -33,6 +70,21 @@ const ConfigModal = ({
const { mutateAsync: saveConfig, isPending: isSaving } = useSaveSandboxProviderConfig()
const { mutateAsync: deleteConfig, isPending: isDeleting } = useDeleteSandboxProviderConfig()
+ // Determine if mode selection should be shown (for providers that support it)
+ const shouldShowModeSelection = PROVIDERS_WITH_MODE_SELECTION.includes(provider.provider_type)
+
+ // Managed mode is only available when system has configured this provider
+ const isManagedModeAvailable = provider.is_system_configured
+
+ // Determine default mode based on configuration state
+ const defaultMode: ConfigMode = provider.is_tenant_configured
+ ? 'byok'
+ : provider.is_system_configured
+ ? 'managed'
+ : 'byok'
+
+ const [configMode, setConfigMode] = useState(defaultMode)
+
const formSchemas: FormSchema[] = useMemo(() => {
return provider.config_schema.map((schema) => {
const fieldConfig = SANDBOX_FIELD_CONFIGS[schema.name as keyof typeof SANDBOX_FIELD_CONFIGS]
@@ -50,6 +102,23 @@ const ConfigModal = ({
}, [provider.config_schema, provider.config, t])
const handleSave = useCallback(async () => {
+ // For managed mode, save empty config to use system defaults
+ if (shouldShowModeSelection && configMode === 'managed') {
+ try {
+ await saveConfig({
+ providerType: provider.provider_type,
+ config: {},
+ })
+ notify({ type: 'success', message: t('api.saved', { ns: 'common' }) })
+ onClose()
+ }
+ catch {
+ // Error toast is handled by fetch layer
+ }
+ return
+ }
+
+ // For BYOK mode, validate and save user-provided config
const formValues = formRef.current?.getFormValues({
needTransformWhenSecretFieldIsPristine: true,
})
@@ -68,7 +137,7 @@ const ConfigModal = ({
catch {
// Error toast is handled by fetch layer
}
- }, [saveConfig, provider.provider_type, notify, t, onClose])
+ }, [shouldShowModeSelection, configMode, saveConfig, provider.provider_type, notify, t, onClose])
const handleRevoke = useCallback(async () => {
try {
@@ -81,35 +150,61 @@ const ConfigModal = ({
}
}, [deleteConfig, provider.provider_type, notify, t, onClose])
- const isConfigured = provider.is_tenant_configured
const docLink = PROVIDER_DOC_LINKS[provider.provider_type]
+ const providerLabelKey = PROVIDER_LABEL_KEYS[provider.provider_type as keyof typeof PROVIDER_LABEL_KEYS] ?? 'sandboxProvider.e2b.label'
+ const providerLabel = t(providerLabelKey, { ns: 'common' })
+
+ // Only show revoke button when in BYOK mode and tenant has custom config
+ const showRevokeButton = provider.is_tenant_configured && (!shouldShowModeSelection || configMode === 'byok')
+ const isActionDisabled = isSaving || isDeleting
+ const showByokForm = !shouldShowModeSelection || configMode === 'byok'
return (
-
- {/* Custom Header: Title + Subtitle with 8px gap */}
+
+ {/* Header */}
{t('sandboxProvider.configModal.title', { ns: 'common' })}
-
- {t(PROVIDER_LABEL_KEYS[provider.provider_type as keyof typeof PROVIDER_LABEL_KEYS] ?? 'sandboxProvider.e2b.label', { ns: 'common' })}
-
+
{providerLabel}
-
+ {/* Mode Selection */}
+ {shouldShowModeSelection && (
+
+
+
+ setConfigMode('managed')}
+ />
+ setConfigMode('byok')}
+ />
+
+
+ )}
+
+ {/* Form fields (hidden when managed mode is selected) */}
+ {showByokForm && (
+
+ )}
{/* Footer Actions */}
@@ -121,36 +216,21 @@ const ConfigModal = ({
rel="noopener noreferrer"
className="system-xs-regular inline-flex items-center gap-1 text-text-accent hover:underline"
>
- {t('sandboxProvider.configModal.readDocLink', { ns: 'common', provider: t(PROVIDER_LABEL_KEYS[provider.provider_type as keyof typeof PROVIDER_LABEL_KEYS] ?? 'sandboxProvider.e2b.label', { ns: 'common' }) })}
+ {t('sandboxProvider.configModal.readDocLink', { ns: 'common', provider: providerLabel })}
)}
- {isConfigured && (
-
diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json
index 462e07e25c..a519728b31 100644
--- a/web/i18n/en-US/common.json
+++ b/web/i18n/en-US/common.json
@@ -564,8 +564,11 @@
"sandboxProvider.configModal.apiKeyPlaceholder": "Enter your API key",
"sandboxProvider.configModal.baseWorkingPath": "Base Working Path",
"sandboxProvider.configModal.baseWorkingPathPlaceholder": "/tmp/sandbox",
+ "sandboxProvider.configModal.bringYourOwnKey": "Bring Your Own E2B API Key",
+ "sandboxProvider.configModal.bringYourOwnKeyDesc": "Connect using your own E2B account. No usage limits from Dify, with full control over resources and billing.",
"sandboxProvider.configModal.cancel": "Cancel",
"sandboxProvider.configModal.confirm": "Confirm",
+ "sandboxProvider.configModal.connectionMode": "Connect Mode",
"sandboxProvider.configModal.dockerImage": "Docker Image",
"sandboxProvider.configModal.dockerImagePlaceholder": "ubuntu:latest",
"sandboxProvider.configModal.dockerSock": "Docker Socket",
@@ -574,6 +577,8 @@
"sandboxProvider.configModal.e2bApiUrlPlaceholder": "https://api.e2b.app",
"sandboxProvider.configModal.e2bTemplate": "E2B Template",
"sandboxProvider.configModal.e2bTemplatePlaceholder": "code-interpreter-v1",
+ "sandboxProvider.configModal.managedByDify": "Managed by Dify",
+ "sandboxProvider.configModal.managedByDifyDesc": "Ready to use instantly. Managed and billed by Dify, with standard usage limits. Best for testing, demos, and light workloads.",
"sandboxProvider.configModal.readDoc": "Read Documentation",
"sandboxProvider.configModal.readDocLink": "Read {{provider}} Docs",
"sandboxProvider.configModal.revoke": "Revoke",
diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json
index baa09f8bba..8c75e2d658 100644
--- a/web/i18n/zh-Hans/common.json
+++ b/web/i18n/zh-Hans/common.json
@@ -564,8 +564,11 @@
"sandboxProvider.configModal.apiKeyPlaceholder": "输入您的 API Key",
"sandboxProvider.configModal.baseWorkingPath": "基础工作路径",
"sandboxProvider.configModal.baseWorkingPathPlaceholder": "/tmp/sandbox",
+ "sandboxProvider.configModal.bringYourOwnKey": "使用自己的 E2B API Key",
+ "sandboxProvider.configModal.bringYourOwnKeyDesc": "使用您自己的 E2B 账户连接。无 Dify 使用限制,完全控制资源和计费。",
"sandboxProvider.configModal.cancel": "取消",
"sandboxProvider.configModal.confirm": "确认",
+ "sandboxProvider.configModal.connectionMode": "连接模式",
"sandboxProvider.configModal.dockerImage": "Docker 镜像",
"sandboxProvider.configModal.dockerImagePlaceholder": "ubuntu:latest",
"sandboxProvider.configModal.dockerSock": "Docker Socket",
@@ -574,6 +577,8 @@
"sandboxProvider.configModal.e2bApiUrlPlaceholder": "https://api.e2b.app",
"sandboxProvider.configModal.e2bTemplate": "E2B 模板",
"sandboxProvider.configModal.e2bTemplatePlaceholder": "code-interpreter-v1",
+ "sandboxProvider.configModal.managedByDify": "由 Dify 托管",
+ "sandboxProvider.configModal.managedByDifyDesc": "开箱即用。由 Dify 管理和计费,有标准使用限制。适合测试、演示和轻量工作负载。",
"sandboxProvider.configModal.readDoc": "查看文档",
"sandboxProvider.configModal.readDocLink": "查看 {{provider}} 文档",
"sandboxProvider.configModal.revoke": "撤销",