Merge branch 'feat/tool-plugin-oauth' into deploy/dev

# Conflicts:
#	api/controllers/console/workspace/tool_providers.py
pull/22338/head^2
Harry 11 months ago
commit 8d554a2dac

@ -17,6 +17,11 @@ APP_WEB_URL=http://127.0.0.1:3000
# Files URL
FILES_URL=http://127.0.0.1:5001
# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network.
# Set this to the internal Docker service URL for proper plugin file access.
# Example: INTERNAL_FILES_URL=http://api:5001
INTERNAL_FILES_URL=http://127.0.0.1:5001
# The time in seconds after the signature is rejected
FILES_ACCESS_TIMEOUT=300

@ -237,6 +237,13 @@ class FileAccessConfig(BaseSettings):
default="",
)
INTERNAL_FILES_URL: str = Field(
description="Internal base URL for file access within Docker network,"
" used for plugin daemon and internal service communication."
" Falls back to FILES_URL if not specified.",
default="",
)
FILES_ACCESS_TIMEOUT: int = Field(
description="Expiration time in seconds for file access URLs",
default=300,

@ -90,7 +90,11 @@ class AppMCPServerRefreshController(Resource):
def get(self, server_id):
if not current_user.is_editor:
raise NotFound()
server = db.session.query(AppMCPServer).filter(AppMCPServer.id == server_id).first()
server = (
db.session.query(AppMCPServer)
.filter(AppMCPServer.id == server_id and AppMCPServer.tenant_id == current_user.current_tenant_id)
.first()
)
if not server:
raise NotFound()
server.server_code = AppMCPServer.generate_server_code(16)

@ -12,7 +12,11 @@ from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.console import api
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
from controllers.console.wraps import (
account_initialization_required,
enterprise_license_required,
setup_required,
)
from core.mcp.auth.auth_flow import auth, handle_callback
from core.mcp.auth.auth_provider import OAuthClientProvider
from core.mcp.error import MCPAuthError, MCPError

@ -21,7 +21,9 @@ def get_signed_file_url(upload_file_id: str) -> str:
def get_signed_file_url_for_plugin(filename: str, mimetype: str, tenant_id: str, user_id: str) -> str:
url = f"{dify_config.FILES_URL}/files/upload/for-plugin"
# Plugin access should use internal URL for Docker network communication
base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
url = f"{base_url}/files/upload/for-plugin"
if user_id is None:
user_id = "DEFAULT-USER"

@ -89,6 +89,7 @@ class MCPServerStreamableHTTPRequestHandler:
types.ListToolsRequest: self.list_tools,
types.CallToolRequest: self.invoke_tool,
types.InitializedNotification: self.handle_notification,
types.PingRequest: self.handle_ping,
}
try:
if self.request_type in handle_map:
@ -105,16 +106,19 @@ class MCPServerStreamableHTTPRequestHandler:
def handle_notification(self):
return "ping"
def handle_ping(self):
return types.EmptyResult()
def initialize(self):
request = cast(types.InitializeRequest, self.request.root)
client_info = request.params.clientInfo
clinet_name = f"{client_info.name}@{client_info.version}"
client_name = f"{client_info.name}@{client_info.version}"
if not self.end_user:
end_user = EndUser(
tenant_id=self.app.tenant_id,
app_id=self.app.id,
type="mcp",
name=clinet_name,
name=client_name,
session_id=generate_session_id(),
external_user_id=self.mcp_server.id,
)

@ -123,6 +123,8 @@ class ProviderEntity(BaseModel):
description: Optional[I18nObject] = None
icon_small: Optional[I18nObject] = None
icon_large: Optional[I18nObject] = None
icon_small_dark: Optional[I18nObject] = None
icon_large_dark: Optional[I18nObject] = None
background: Optional[str] = None
help: Optional[ProviderHelpEntity] = None
supported_model_types: Sequence[ModelType]

@ -79,6 +79,7 @@ class PluginDeclaration(BaseModel):
name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$")
description: I18nObject
icon: str
icon_dark: Optional[str] = Field(default=None)
label: I18nObject
category: PluginCategory
created_at: datetime.datetime

@ -39,19 +39,22 @@ class ApiToolProviderController(ToolProviderController):
type=ProviderConfig.Type.SELECT,
options=[
ProviderConfig.Option(value="none", label=I18nObject(en_US="None", zh_Hans="")),
ProviderConfig.Option(value="api_key", label=I18nObject(en_US="api_key", zh_Hans="api_key")),
ProviderConfig.Option(value="api_key_header", label=I18nObject(en_US="Header", zh_Hans="请求头")),
ProviderConfig.Option(
value="api_key_query", label=I18nObject(en_US="Query Param", zh_Hans="查询参数")
),
],
default="none",
help=I18nObject(en_US="The auth type of the api provider", zh_Hans="api provider 的认证类型"),
)
]
if auth_type == ApiProviderAuthType.API_KEY:
if auth_type == ApiProviderAuthType.API_KEY_HEADER:
credentials_schema = [
*credentials_schema,
ProviderConfig(
name="api_key_header",
required=False,
default="api_key",
default="Authorization",
type=ProviderConfig.Type.TEXT_INPUT,
help=I18nObject(en_US="The header name of the api key", zh_Hans="携带 api key 的 header 名称"),
),
@ -74,6 +77,25 @@ class ApiToolProviderController(ToolProviderController):
],
),
]
elif auth_type == ApiProviderAuthType.API_KEY_QUERY:
credentials_schema = [
*credentials_schema,
ProviderConfig(
name="api_key_query_param",
required=False,
default="key",
type=ProviderConfig.Type.TEXT_INPUT,
help=I18nObject(
en_US="The query parameter name of the api key", zh_Hans="携带 api key 的查询参数名称"
),
),
ProviderConfig(
name="api_key_value",
required=True,
type=ProviderConfig.Type.SECRET_INPUT,
help=I18nObject(en_US="The api key", zh_Hans="api key 的值"),
),
]
elif auth_type == ApiProviderAuthType.NONE:
pass

@ -78,8 +78,8 @@ class ApiTool(Tool):
if "auth_type" not in credentials:
raise ToolProviderCredentialValidationError("Missing auth_type")
if credentials["auth_type"] == "api_key":
api_key_header = "api_key"
if credentials["auth_type"] in ("api_key_header", "api_key"): # backward compatibility:
api_key_header = "Authorization"
if "api_key_header" in credentials:
api_key_header = credentials["api_key_header"]
@ -100,6 +100,11 @@ class ApiTool(Tool):
headers[api_key_header] = credentials["api_key_value"]
elif credentials["auth_type"] == "api_key_query":
# For query parameter authentication, we don't add anything to headers
# The query parameter will be added in do_http_request method
pass
needed_parameters = [parameter for parameter in (self.api_bundle.parameters or []) if parameter.required]
for parameter in needed_parameters:
if parameter.required and parameter.name not in parameters:
@ -154,6 +159,15 @@ class ApiTool(Tool):
cookies = {}
files = []
# Add API key to query parameters if auth_type is api_key_query
if self.runtime and self.runtime.credentials:
credentials = self.runtime.credentials
if credentials.get("auth_type") == "api_key_query":
api_key_query_param = credentials.get("api_key_query_param", "key")
api_key_value = credentials.get("api_key_value")
if api_key_value:
params[api_key_query_param] = api_key_value
# check parameters
for parameter in self.api_bundle.openapi.get("parameters", []):
value = self.get_parameter_value(parameter, parameters)
@ -213,7 +227,8 @@ class ApiTool(Tool):
elif "default" in property:
body[name] = property["default"]
else:
body[name] = None
# omit optional parameters that weren't provided, instead of setting them to None
pass
break
# replace path parameters

@ -28,6 +28,7 @@ class ToolProviderApiEntity(BaseModel):
name: str # identifier
description: I18nObject
icon: str | dict
icon_dark: Optional[str | dict] = Field(default=None, description="The dark icon of the tool")
label: I18nObject # label
type: ToolProviderType
masked_credentials: Optional[dict] = None
@ -72,6 +73,7 @@ class ToolProviderApiEntity(BaseModel):
"plugin_unique_identifier": self.plugin_unique_identifier,
"description": self.description.to_dict(),
"icon": self.icon,
"icon_dark": self.icon_dark,
"label": self.label.to_dict(),
"type": self.type.value,
"team_credentials": self.masked_credentials,

@ -96,7 +96,8 @@ class ApiProviderAuthType(Enum):
"""
NONE = "none"
API_KEY = "api_key"
API_KEY_HEADER = "api_key_header"
API_KEY_QUERY = "api_key_query"
@classmethod
def value_of(cls, value: str) -> "ApiProviderAuthType":
@ -317,6 +318,7 @@ class ToolProviderIdentity(BaseModel):
name: str = Field(..., description="The name of the tool")
description: I18nObject = Field(..., description="The description of the tool")
icon: str = Field(..., description="The icon of the tool")
icon_dark: Optional[str] = Field(default=None, description="The dark icon of the tool")
label: I18nObject = Field(..., description="The label of the tool")
tags: Optional[list[ToolLabelEnum]] = Field(
default=[],

@ -9,9 +9,10 @@ from configs import dify_config
def sign_tool_file(tool_file_id: str, extension: str) -> str:
"""
sign file to get a temporary url
sign file to get a temporary url for plugin access
"""
base_url = dify_config.FILES_URL
# Use internal URL for plugin/tool file access in Docker environments
base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
file_preview_url = f"{base_url}/files/tools/{tool_file_id}{extension}"
timestamp = str(int(time.time()))

@ -35,9 +35,10 @@ class ToolFileManager:
@staticmethod
def sign_file(tool_file_id: str, extension: str) -> str:
"""
sign file to get a temporary url
sign file to get a temporary url for plugin access
"""
base_url = dify_config.FILES_URL
# Use internal URL for plugin/tool file access in Docker environments
base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
file_preview_url = f"{base_url}/files/tools/{tool_file_id}{extension}"
timestamp = str(int(time.time()))

@ -692,9 +692,16 @@ class ToolManager:
if provider is None:
raise ToolProviderNotFoundError(f"api provider {provider_id} not found")
auth_type = ApiProviderAuthType.NONE
provider_auth_type = provider.credentials.get("auth_type")
if provider_auth_type in ("api_key_header", "api_key"): # backward compatibility
auth_type = ApiProviderAuthType.API_KEY_HEADER
elif provider_auth_type == "api_key_query":
auth_type = ApiProviderAuthType.API_KEY_QUERY
controller = ApiToolProviderController.from_db(
provider,
ApiProviderAuthType.API_KEY if provider.credentials["auth_type"] == "api_key" else ApiProviderAuthType.NONE,
auth_type,
)
controller.load_bundled_tools(provider.tools)
@ -753,9 +760,16 @@ class ToolManager:
credentials = {}
# package tool provider controller
auth_type = ApiProviderAuthType.NONE
credentials_auth_type = credentials.get("auth_type")
if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility
auth_type = ApiProviderAuthType.API_KEY_HEADER
elif credentials_auth_type == "api_key_query":
auth_type = ApiProviderAuthType.API_KEY_QUERY
controller = ApiToolProviderController.from_db(
provider_obj,
ApiProviderAuthType.API_KEY if credentials["auth_type"] == "api_key" else ApiProviderAuthType.NONE,
auth_type,
)
# init tool configuration
encrypter, _ = create_tool_provider_encrypter(

@ -329,6 +329,7 @@ class ToolNode(BaseNode[ToolNodeData]):
icon = current_plugin.declaration.icon
except StopIteration:
pass
icon_dark = None
try:
builtin_tool = next(
provider
@ -339,10 +340,12 @@ class ToolNode(BaseNode[ToolNodeData]):
if provider.name == dict_metadata["provider"]
)
icon = builtin_tool.icon
icon_dark = builtin_tool.icon_dark
except StopIteration:
pass
dict_metadata["icon"] = icon
dict_metadata["icon_dark"] = icon_dark
message.message.metadata = dict_metadata
agent_log = AgentLogEvent(
id=message.message.id,

@ -1,6 +1,6 @@
[project]
name = "dify-api"
version = "1.5.1"
version = "1.6.0"
requires-python = ">=3.11,<3.13"
dependencies = [

@ -70,7 +70,6 @@ class MCPToolManageService:
MCPToolProvider.server_url_hash == server_url_hash,
MCPToolProvider.server_identifier == server_identifier,
),
MCPToolProvider.tenant_id == tenant_id,
)
.first()
)

@ -77,10 +77,18 @@ class ToolTransformService:
provider.icon = ToolTransformService.get_plugin_icon_url(
tenant_id=tenant_id, filename=provider.icon
)
if isinstance(provider.icon_dark, str) and provider.icon_dark:
provider.icon_dark = ToolTransformService.get_plugin_icon_url(
tenant_id=tenant_id, filename=provider.icon_dark
)
else:
provider.icon = ToolTransformService.get_tool_provider_icon_url(
provider_type=provider.type.value, provider_name=provider.name, icon=provider.icon
)
if provider.icon_dark:
provider.icon_dark = ToolTransformService.get_tool_provider_icon_url(
provider_type=provider.type.value, provider_name=provider.name, icon=provider.icon_dark
)
@classmethod
def builtin_provider_to_user_provider(
@ -98,6 +106,7 @@ class ToolTransformService:
name=provider_controller.entity.identity.name,
description=provider_controller.entity.identity.description,
icon=provider_controller.entity.identity.icon,
icon_dark=provider_controller.entity.identity.icon_dark,
label=provider_controller.entity.identity.label,
type=ToolProviderType.BUILT_IN,
masked_credentials={},
@ -165,11 +174,16 @@ class ToolTransformService:
convert provider controller to user provider
"""
# package tool provider controller
auth_type = ApiProviderAuthType.NONE
credentials_auth_type = db_provider.credentials.get("auth_type")
if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility
auth_type = ApiProviderAuthType.API_KEY_HEADER
elif credentials_auth_type == "api_key_query":
auth_type = ApiProviderAuthType.API_KEY_QUERY
controller = ApiToolProviderController.from_db(
db_provider=db_provider,
auth_type=ApiProviderAuthType.API_KEY
if db_provider.credentials["auth_type"] == "api_key"
else ApiProviderAuthType.NONE,
auth_type=auth_type,
)
return controller
@ -194,6 +208,7 @@ class ToolTransformService:
name=provider_controller.entity.identity.name,
description=provider_controller.entity.identity.description,
icon=provider_controller.entity.identity.icon,
icon_dark=provider_controller.entity.identity.icon_dark,
label=provider_controller.entity.identity.label,
type=ToolProviderType.WORKFLOW,
masked_credentials={},

@ -1217,7 +1217,7 @@ wheels = [
[[package]]
name = "dify-api"
version = "1.5.1"
version = "1.6.0"
source = { virtual = "." }
dependencies = [
{ name = "arize-phoenix-otel" },

@ -47,6 +47,11 @@ APP_WEB_URL=
# ensuring port 5001 is externally accessible (see docker-compose.yaml).
FILES_URL=
# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network.
# Set this to the internal Docker service URL for proper plugin file access.
# Example: INTERNAL_FILES_URL=http://api:5001
INTERNAL_FILES_URL=
# ------------------------------
# Server Configuration
# ------------------------------

@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.5.1
image: langgenius/dify-api:1.6.0
restart: always
environment:
# Use the shared environment variables.
@ -31,7 +31,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.5.1
image: langgenius/dify-api:1.6.0
restart: always
environment:
# Use the shared environment variables.
@ -79,7 +79,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.5.1
image: langgenius/dify-web:1.6.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}

@ -11,6 +11,7 @@ x-shared-env: &shared-api-worker-env
APP_API_URL: ${APP_API_URL:-}
APP_WEB_URL: ${APP_WEB_URL:-}
FILES_URL: ${FILES_URL:-}
INTERNAL_FILES_URL: ${INTERNAL_FILES_URL:-}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
LOG_FILE: ${LOG_FILE:-/app/logs/server.log}
LOG_FILE_MAX_SIZE: ${LOG_FILE_MAX_SIZE:-20}
@ -526,7 +527,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.5.1
image: langgenius/dify-api:1.6.0
restart: always
environment:
# Use the shared environment variables.
@ -555,7 +556,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.5.1
image: langgenius/dify-api:1.6.0
restart: always
environment:
# Use the shared environment variables.
@ -603,7 +604,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.5.1
image: langgenius/dify-web:1.6.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}

@ -122,7 +122,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
return <AppUnavailable code={500} unknownReason={t('datasetCreation.error.unavailable') as string} />
return (
<div className='flex flex-col bg-components-panel-bg' style={{ height: 'calc(100vh - 56px)' }}>
<div className='flex flex-col overflow-hidden bg-components-panel-bg' style={{ height: 'calc(100vh - 56px)' }}>
<TopBar activeIndex={step - 1} datasetId={datasetId} />
<div style={{ height: 'calc(100% - 52px)' }}>
{step === 1 && <StepOne

@ -1,6 +1,5 @@
.filePreview {
@apply flex flex-col border-l border-components-panel-border shrink-0 bg-background-default-lighter;
width: 528px;
}
.previewHeader {
@ -28,6 +27,7 @@
background: #f9fafb center no-repeat url(../assets/Loading.svg);
background-size: contain;
}
.fileContent {
white-space: pre-line;
}

@ -1,5 +1,5 @@
'use client'
import React, { useMemo, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react'
import FilePreview from '../file-preview'
@ -95,24 +95,29 @@ const StepOne = ({
const modalShowHandle = () => setShowModal(true)
const modalCloseHandle = () => setShowModal(false)
const updateCurrentFile = (file: File) => {
const updateCurrentFile = useCallback((file: File) => {
setCurrentFile(file)
}
const hideFilePreview = () => {
}, [])
const hideFilePreview = useCallback(() => {
setCurrentFile(undefined)
}
}, [])
const updateCurrentPage = (page: NotionPage) => {
const updateCurrentPage = useCallback((page: NotionPage) => {
setCurrentNotionPage(page)
}
}, [])
const hideNotionPagePreview = () => {
const hideNotionPagePreview = useCallback(() => {
setCurrentNotionPage(undefined)
}
}, [])
const updateWebsite = useCallback((website: CrawlResultItem) => {
setCurrentWebsite(website)
}, [])
const hideWebsitePreview = () => {
const hideWebsitePreview = useCallback(() => {
setCurrentWebsite(undefined)
}
}, [])
const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type)
const isInCreatePage = shouldShowDataSourceTypeList
@ -139,7 +144,7 @@ const StepOne = ({
<div className={classNames(s.form)}>
{
shouldShowDataSourceTypeList && (
<div className={classNames(s.stepHeader, 'text-text-secondary system-md-semibold')}>
<div className={classNames(s.stepHeader, 'system-md-semibold text-text-secondary')}>
{t('datasetCreation.steps.one')}
</div>
)
@ -158,8 +163,8 @@ const StepOne = ({
if (dataSourceTypeDisable)
return
changeType(DataSourceType.FILE)
hideFilePreview()
hideNotionPagePreview()
hideWebsitePreview()
}}
>
<span className={cn(s.datasetIcon)} />
@ -182,7 +187,7 @@ const StepOne = ({
return
changeType(DataSourceType.NOTION)
hideFilePreview()
hideNotionPagePreview()
hideWebsitePreview()
}}
>
<span className={cn(s.datasetIcon, s.notion)} />
@ -201,7 +206,13 @@ const StepOne = ({
dataSourceType === DataSourceType.WEB && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
)}
onClick={() => changeType(DataSourceType.WEB)}
onClick={() => {
if (dataSourceTypeDisable)
return
changeType(DataSourceType.WEB)
hideFilePreview()
hideNotionPagePreview()
}}
>
<span className={cn(s.datasetIcon, s.web)} />
<span
@ -276,7 +287,7 @@ const StepOne = ({
<>
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
<Website
onPreview={setCurrentWebsite}
onPreview={updateWebsite}
checkedCrawlResult={websitePages}
onCheckedCrawlResultChange={updateWebsitePages}
onCrawlProviderChange={onWebsiteCrawlProviderChange}

@ -79,7 +79,7 @@ const Header = () => {
}
return (
<div className='flex h-[60px] items-center'>
<div className='flex h-[56px] items-center'>
<div className='flex min-w-0 flex-[1] items-center pl-3 pr-2 min-[1280px]:pr-3'>
<Link href="/apps" className='flex h-8 shrink-0 items-center justify-center px-0.5'>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo

@ -68,23 +68,34 @@ const ConfigCredential: FC<Props> = ({
text={t('tools.createTool.authMethod.types.none')}
value={AuthType.none}
isChecked={tempCredential.auth_type === AuthType.none}
onClick={value => setTempCredential({ ...tempCredential, auth_type: value as AuthType })}
onClick={value => setTempCredential({
auth_type: value as AuthType,
})}
/>
<SelectItem
text={t('tools.createTool.authMethod.types.api_key')}
value={AuthType.apiKey}
isChecked={tempCredential.auth_type === AuthType.apiKey}
text={t('tools.createTool.authMethod.types.api_key_header')}
value={AuthType.apiKeyHeader}
isChecked={tempCredential.auth_type === AuthType.apiKeyHeader}
onClick={value => setTempCredential({
...tempCredential,
auth_type: value as AuthType,
api_key_header: tempCredential.api_key_header || 'Authorization',
api_key_value: tempCredential.api_key_value || '',
api_key_header_prefix: tempCredential.api_key_header_prefix || AuthHeaderPrefix.custom,
})}
/>
<SelectItem
text={t('tools.createTool.authMethod.types.api_key_query')}
value={AuthType.apiKeyQuery}
isChecked={tempCredential.auth_type === AuthType.apiKeyQuery}
onClick={value => setTempCredential({
auth_type: value as AuthType,
api_key_query_param: tempCredential.api_key_query_param || 'key',
api_key_value: tempCredential.api_key_value || '',
})}
/>
</div>
</div>
{tempCredential.auth_type === AuthType.apiKey && (
{tempCredential.auth_type === AuthType.apiKeyHeader && (
<>
<div>
<div className='system-sm-medium py-2 text-text-primary'>{t('tools.createTool.authHeaderPrefix.title')}</div>
@ -136,6 +147,35 @@ const ConfigCredential: FC<Props> = ({
/>
</div>
</>)}
{tempCredential.auth_type === AuthType.apiKeyQuery && (
<>
<div>
<div className='system-sm-medium flex items-center py-2 text-text-primary'>
{t('tools.createTool.authMethod.queryParam')}
<Tooltip
popupContent={
<div className='w-[261px] text-text-tertiary'>
{t('tools.createTool.authMethod.queryParamTooltip')}
</div>
}
triggerClassName='ml-0.5 w-4 h-4'
/>
</div>
<Input
value={tempCredential.api_key_query_param}
onChange={e => setTempCredential({ ...tempCredential, api_key_query_param: e.target.value })}
placeholder={t('tools.createTool.authMethod.types.queryParamPlaceholder')!}
/>
</div>
<div>
<div className='system-sm-medium py-2 text-text-primary'>{t('tools.createTool.authMethod.value')}</div>
<Input
value={tempCredential.api_key_value}
onChange={e => setTempCredential({ ...tempCredential, api_key_value: e.target.value })}
placeholder={t('tools.createTool.authMethod.types.apiValuePlaceholder')!}
/>
</div>
</>)}
</div>

@ -1,9 +1,7 @@
'use client'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiLoopLeftLine,
} from '@remixicon/react'
import { RiEditLine, RiLoopLeftLine } from '@remixicon/react'
import {
Mcp,
} from '@/app/components/base/icons/src/vender/other'
@ -209,7 +207,11 @@ function MCPServiceCard({
variant='ghost'
onClick={() => setShowMCPServerModal(true)}
>
{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}
<div className="flex items-center justify-center gap-[1px]">
<RiEditLine className="h-3.5 w-3.5" />
<div className="system-xs-medium px-[3px] text-text-tertiary">{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}</div>
</div>
</Button>
</div>
</div>

@ -7,7 +7,8 @@ export enum LOC {
export enum AuthType {
none = 'none',
apiKey = 'api_key',
apiKeyHeader = 'api_key_header',
apiKeyQuery = 'api_key_query',
}
export enum AuthHeaderPrefix {
@ -21,6 +22,7 @@ export type Credential = {
api_key_header?: string
api_key_value?: string
api_key_header_prefix?: AuthHeaderPrefix
api_key_query_param?: string
}
export enum CollectionType {

@ -61,7 +61,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
>
<div
className={classNames(
'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative flex items-center justify-center rounded-[6px]',
'relative flex size-5 items-center justify-center rounded-[6px] border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge',
)}
ref={containerRef}
>
@ -73,7 +73,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
src={icon}
alt='tool icon'
className={classNames(
'w-full h-full size-3.5 object-cover',
'size-3.5 h-full w-full object-cover',
notSuccess && 'opacity-50',
)}
onError={() => setIconFetchError(true)}
@ -82,7 +82,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
if (typeof icon === 'object') {
return <AppIcon
className={classNames(
'w-full h-full size-3.5 object-cover',
'size-3.5 h-full w-full object-cover',
notSuccess && 'opacity-50',
)}
icon={icon?.content}

@ -79,13 +79,13 @@ const SchemaNode: FC<SchemaNodeProps> = ({
}
const handleMouseEnter = () => {
if(!readOnly) return
if(readOnly) return
if (advancedEditing || isAddingNewField) return
setHoveringPropertyDebounced(path.join('.'))
}
const handleMouseLeave = () => {
if(!readOnly) return
if(readOnly) return
if (advancedEditing || isAddingNewField) return
setHoveringPropertyDebounced(null)
}
@ -95,7 +95,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
<div className={classNames('relative z-10', indentPadding[depth])}>
{depth > 0 && hasChildren && (
<div className={classNames(
'flex items-center absolute top-0 w-5 h-7 px-0.5 z-10 bg-background-section-burn',
'absolute top-0 z-10 flex h-7 w-5 items-center bg-background-section-burn px-0.5',
indentLeft[depth - 1],
)}>
<button
@ -140,8 +140,8 @@ const SchemaNode: FC<SchemaNodeProps> = ({
</div>
<div className={classNames(
'flex justify-center w-5 absolute z-0',
schema.description ? 'h-[calc(100%-3rem)] top-12' : 'h-[calc(100%-1.75rem)] top-7',
'absolute z-0 flex w-5 justify-center',
schema.description ? 'top-12 h-[calc(100%-3rem)]' : 'top-7 h-[calc(100%-1.75rem)]',
indentLeft[depth],
)}>
<Divider

@ -80,11 +80,15 @@ const translation = {
title: 'Authorization method',
type: 'Authorization type',
keyTooltip: 'Http Header Key, You can leave it with "Authorization" if you have no idea what it is or set it to a custom value',
queryParam: 'Query Parameter',
queryParamTooltip: 'The name of the API key query parameter to pass, e.g. "key" in "https://example.com/test?key=API_KEY".',
types: {
none: 'None',
api_key: 'API Key',
api_key_header: 'Header',
api_key_query: 'Query Param',
apiKeyPlaceholder: 'HTTP header name for API Key',
apiValuePlaceholder: 'Enter API Key',
queryParamPlaceholder: 'Query parameter name for API Key',
},
key: 'Key',
value: 'Value',

@ -80,11 +80,15 @@ const translation = {
title: '鉴权方法',
type: '鉴权类型',
keyTooltip: 'HTTP 头部名称,如果你不知道是什么,可以将其保留为 Authorization 或设置为自定义值',
queryParam: '查询参数',
queryParamTooltip: '用于传递 API 密钥查询参数的名称, 如 "https://example.com/test?key=API_KEY" 中的 "key"参数',
types: {
none: '无',
api_key: 'API Key',
api_key_header: '请求头',
api_key_query: '查询参数',
apiKeyPlaceholder: 'HTTP 头部名称,用于传递 API Key',
apiValuePlaceholder: '输入 API Key',
queryParamPlaceholder: '查询参数名称,用于传递 API Key',
},
key: '键',
value: '值',

@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "1.5.1",
"version": "1.6.0",
"private": true,
"engines": {
"node": ">=v22.11.0"

Loading…
Cancel
Save