From ef330fec2c100f7b12a44c58647c2a4ed024d6e5 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 9 Jul 2025 11:54:10 +0800 Subject: [PATCH 01/15] feat(oauth): add credential validation for providers --- .../tools/builtin_tools_manage_service.py | 99 ++++++++++--------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 8e7b179ea7..f32df9763d 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -95,9 +95,7 @@ class BuiltinToolManageService: return entity @staticmethod - def list_builtin_provider_credentials_schema( - provider_name: str, credential_type: CredentialType, tenant_id: str - ): + def list_builtin_provider_credentials_schema(provider_name: str, credential_type: CredentialType, tenant_id: str): """ list builtin provider credentials schema @@ -141,7 +139,8 @@ class BuiltinToolManageService: if key in masked_credentials and value == masked_credentials[key]: credentials[key] = original_credentials[key] - provider_controller.validate_credentials(user_id, credentials) + if CredentialType.of(db_provider.credential_type).is_validate_allowed(): + provider_controller.validate_credentials(user_id, credentials) # encrypt credentials db_provider.encrypted_credentials = json.dumps(encrypter.encrypt(credentials)) @@ -159,6 +158,7 @@ class BuiltinToolManageService: ToolNotFoundError, ToolProviderCredentialValidationError, ) as e: + db.session.rollback() raise ValueError(str(e)) return {"result": "success"} @@ -176,46 +176,59 @@ class BuiltinToolManageService: add builtin tool provider """ lock = f"builtin_tool_provider_create_lock:{tenant_id}_{provider}" - with redis_client.lock(lock, timeout=20): - # check if the provider count is over the limit - provider_count = ( - db.session.query(BuiltinToolProvider).filter_by(tenant_id=tenant_id, provider=provider).count() - ) - if provider_count >= BuiltinToolManageService.__MAX_BUILTIN_TOOL_PROVIDER_COUNT__: - raise ValueError(f"you have reached the maximum number of providers for {provider}") - - # TODO should we get name from oauth authentication? - name = ( - name - if name - else BuiltinToolManageService.generate_builtin_tool_provider_name( - tenant_id=tenant_id, provider=provider, credential_type=api_type + try: + with redis_client.lock(lock, timeout=20): + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + if not provider_controller.need_credentials: + raise ValueError(f"provider {provider} does not need credentials") + + provider_count = ( + db.session.query(BuiltinToolProvider).filter_by(tenant_id=tenant_id, provider=provider).count() ) - ) - db_provider = BuiltinToolProvider( - tenant_id=tenant_id, - user_id=user_id, - provider=provider, - encrypted_credentials=json.dumps(credentials), - credential_type=api_type.value, - name=name, - ) + # check if the provider count is reached the limit + if provider_count >= BuiltinToolManageService.__MAX_BUILTIN_TOOL_PROVIDER_COUNT__: + raise ValueError(f"you have reached the maximum number of providers for {provider}") - provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) - if not provider_controller.need_credentials: - raise ValueError(f"provider {provider} does not need credentials") + # validate credentials if allowed + if CredentialType.of(api_type).is_validate_allowed(): + provider_controller.validate_credentials(user_id, credentials) - encrypter, cache = BuiltinToolManageService.create_tool_encrypter( - tenant_id, db_provider, provider, provider_controller - ) + # generate name if not provided + if name is None: + name = BuiltinToolManageService.generate_builtin_tool_provider_name( + tenant_id=tenant_id, provider=provider, credential_type=api_type + ) - # encrypt credentials - db_provider.encrypted_credentials = json.dumps(encrypter.encrypt(credentials)) + # create encrypter + encrypter, _ = create_provider_encrypter( + tenant_id=tenant_id, + config=[ + x.to_basic_provider_config() + for x in provider_controller.get_credentials_schema_by_type(api_type) + ], + cache=NoOpProviderCredentialCache(), + ) - cache.delete() - db.session.add(db_provider) - db.session.commit() + db_provider = BuiltinToolProvider( + tenant_id=tenant_id, + user_id=user_id, + provider=provider, + encrypted_credentials=json.dumps(encrypter.encrypt(credentials)), + credential_type=api_type.value, + name=name, + ) + + db.session.add(db_provider) + db.session.commit() + except ( + PluginDaemonClientSideError, + ToolProviderNotFoundError, + ToolNotFoundError, + ToolProviderCredentialValidationError, + ) as e: + db.session.rollback() + raise ValueError(str(e)) return {"result": "success"} @staticmethod @@ -236,9 +249,7 @@ class BuiltinToolManageService: return encrypter, cache @staticmethod - def generate_builtin_tool_provider_name( - tenant_id: str, provider: str, credential_type: CredentialType - ) -> str: + def generate_builtin_tool_provider_name(tenant_id: str, provider: str, credential_type: CredentialType) -> str: try: db_providers = ( db.session.query(BuiltinToolProvider) @@ -324,7 +335,7 @@ class BuiltinToolManageService: is_oauth_custom_client_enabled=BuiltinToolManageService.is_oauth_custom_client_enabled(tenant_id, provider), credentials=credentials, ) - + return credential_info @staticmethod @@ -362,8 +373,8 @@ class BuiltinToolManageService: # clear default provider session.query(BuiltinToolProvider).filter_by( - tenant_id=tenant_id, user_id=user_id, provider=provider, default=True - ).update({"default": False}) + tenant_id=tenant_id, user_id=user_id, provider=provider, is_default=True + ).update({"is_default": False}) # set new default provider target_provider.is_default = True From f35b8d6245837eeb2dca13cefe0e507ad5e4b978 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 9 Jul 2025 14:44:36 +0800 Subject: [PATCH 02/15] feat(oauth): refactor session management in tool provider operations --- .../tools/builtin_tools_manage_service.py | 228 ++++++++++-------- 1 file changed, 125 insertions(+), 103 deletions(-) diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index f32df9763d..fea74ba492 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -7,6 +7,7 @@ from typing import Any, Optional from sqlalchemy.orm import Session from configs import dify_config +from constants import HIDDEN_VALUE from core.helper.position_helper import is_filtered from core.helper.provider_cache import NoOpProviderCredentialCache, ToolProviderCredentialsCache from core.plugin.entities.plugin import ToolProviderID @@ -114,52 +115,65 @@ class BuiltinToolManageService: """ update builtin tool provider """ - # get if the provider exists - db_provider = BuiltinToolManageService.get_builtin_provider_by_id(tenant_id, credential_id) - - if db_provider is None: - raise ValueError(f"you have not added provider {provider}") + with Session(db.engine) as session: + # get if the provider exists + db_provider = ( + session.query(BuiltinToolProvider) + .filter( + BuiltinToolProvider.tenant_id == tenant_id, + BuiltinToolProvider.id == credential_id, + ) + .first() + ) + if db_provider is None: + raise ValueError(f"you have not added provider {provider}") - try: - if CredentialType.of(db_provider.credential_type).is_editable(): - provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) - if not provider_controller.need_credentials: - raise ValueError(f"provider {provider} does not need credentials") + try: + if CredentialType.of(db_provider.credential_type).is_editable(): + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + if not provider_controller.need_credentials: + raise ValueError(f"provider {provider} does not need credentials") - encrypter, cache = BuiltinToolManageService.create_tool_encrypter( - tenant_id, db_provider, provider, provider_controller - ) + encrypter, cache = BuiltinToolManageService.create_tool_encrypter( + tenant_id, db_provider, provider, provider_controller + ) - # Decrypt and restore original credentials for masked values - original_credentials = encrypter.decrypt(db_provider.credentials) - masked_credentials = encrypter.mask_tool_credentials(original_credentials) + original_credentials = encrypter.decrypt(db_provider.credentials) + new_credentials: dict = { + key: value if value != HIDDEN_VALUE else original_credentials.get(key, HIDDEN_VALUE) + for key, value in credentials.items() + } - # check if the credential has changed, save the original credential - for key, value in credentials.items(): - if key in masked_credentials and value == masked_credentials[key]: - credentials[key] = original_credentials[key] + if CredentialType.of(db_provider.credential_type).is_validate_allowed(): + provider_controller.validate_credentials(user_id, new_credentials) - if CredentialType.of(db_provider.credential_type).is_validate_allowed(): - provider_controller.validate_credentials(user_id, credentials) + # encrypt credentials + db_provider.encrypted_credentials = json.dumps(encrypter.encrypt(new_credentials)) - # encrypt credentials - db_provider.encrypted_credentials = json.dumps(encrypter.encrypt(credentials)) + cache.delete() - cache.delete() + # update name if provided + if name is not None and db_provider.name != name: + # check if the name is already used + if ( + session.query(BuiltinToolProvider) + .filter_by(tenant_id=tenant_id, provider=provider, name=name) + .count() + > 0 + ): + raise ValueError(f"the credential name '{name}' is already used") - # update name if provided - if name is not None and db_provider.name != name: - db_provider.name = name + db_provider.name = name - db.session.commit() - except ( - PluginDaemonClientSideError, - ToolProviderNotFoundError, - ToolNotFoundError, - ToolProviderCredentialValidationError, - ) as e: - db.session.rollback() - raise ValueError(str(e)) + session.commit() + except ( + PluginDaemonClientSideError, + ToolProviderNotFoundError, + ToolNotFoundError, + ToolProviderCredentialValidationError, + ) as e: + session.rollback() + raise ValueError(str(e)) return {"result": "success"} @@ -175,59 +189,69 @@ class BuiltinToolManageService: """ add builtin tool provider """ - lock = f"builtin_tool_provider_create_lock:{tenant_id}_{provider}" try: - with redis_client.lock(lock, timeout=20): - provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) - if not provider_controller.need_credentials: - raise ValueError(f"provider {provider} does not need credentials") - - provider_count = ( - db.session.query(BuiltinToolProvider).filter_by(tenant_id=tenant_id, provider=provider).count() - ) + with Session(db.engine) as session: + lock = f"builtin_tool_provider_create_lock:{tenant_id}_{provider}" + with redis_client.lock(lock, timeout=20): + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + if not provider_controller.need_credentials: + raise ValueError(f"provider {provider} does not need credentials") + + provider_count = ( + session.query(BuiltinToolProvider).filter_by(tenant_id=tenant_id, provider=provider).count() + ) - # check if the provider count is reached the limit - if provider_count >= BuiltinToolManageService.__MAX_BUILTIN_TOOL_PROVIDER_COUNT__: - raise ValueError(f"you have reached the maximum number of providers for {provider}") + # check if the provider count is reached the limit + if provider_count >= BuiltinToolManageService.__MAX_BUILTIN_TOOL_PROVIDER_COUNT__: + raise ValueError(f"you have reached the maximum number of providers for {provider}") - # validate credentials if allowed - if CredentialType.of(api_type).is_validate_allowed(): - provider_controller.validate_credentials(user_id, credentials) + # validate credentials if allowed + if CredentialType.of(api_type).is_validate_allowed(): + provider_controller.validate_credentials(user_id, credentials) - # generate name if not provided - if name is None: - name = BuiltinToolManageService.generate_builtin_tool_provider_name( - tenant_id=tenant_id, provider=provider, credential_type=api_type + # generate name if not provided + if name is None: + name = BuiltinToolManageService.generate_builtin_tool_provider_name( + session=session, tenant_id=tenant_id, provider=provider, credential_type=api_type + ) + else: + # check if the name is already used + if ( + session.query(BuiltinToolProvider) + .filter_by(tenant_id=tenant_id, provider=provider, name=name) + .count() + > 0 + ): + raise ValueError(f"the credential name '{name}' is already used") + + # create encrypter + encrypter, _ = create_provider_encrypter( + tenant_id=tenant_id, + config=[ + x.to_basic_provider_config() + for x in provider_controller.get_credentials_schema_by_type(api_type) + ], + cache=NoOpProviderCredentialCache(), ) - # create encrypter - encrypter, _ = create_provider_encrypter( - tenant_id=tenant_id, - config=[ - x.to_basic_provider_config() - for x in provider_controller.get_credentials_schema_by_type(api_type) - ], - cache=NoOpProviderCredentialCache(), - ) - - db_provider = BuiltinToolProvider( - tenant_id=tenant_id, - user_id=user_id, - provider=provider, - encrypted_credentials=json.dumps(encrypter.encrypt(credentials)), - credential_type=api_type.value, - name=name, - ) + db_provider = BuiltinToolProvider( + tenant_id=tenant_id, + user_id=user_id, + provider=provider, + encrypted_credentials=json.dumps(encrypter.encrypt(credentials)), + credential_type=api_type.value, + name=name, + ) - db.session.add(db_provider) - db.session.commit() + session.add(db_provider) + session.commit() except ( PluginDaemonClientSideError, ToolProviderNotFoundError, ToolNotFoundError, ToolProviderCredentialValidationError, ) as e: - db.session.rollback() + session.rollback() raise ValueError(str(e)) return {"result": "success"} @@ -249,10 +273,12 @@ class BuiltinToolManageService: return encrypter, cache @staticmethod - def generate_builtin_tool_provider_name(tenant_id: str, provider: str, credential_type: CredentialType) -> str: + def generate_builtin_tool_provider_name( + session: Session, tenant_id: str, provider: str, credential_type: CredentialType + ) -> str: try: db_providers = ( - db.session.query(BuiltinToolProvider) + session.query(BuiltinToolProvider) .filter_by( tenant_id=tenant_id, provider=provider, @@ -308,7 +334,7 @@ class BuiltinToolManageService: default_provider = providers[0] default_provider.is_default = True provider_controller = ToolManager.get_builtin_provider(default_provider.provider, tenant_id) - encrypter, cache = BuiltinToolManageService.create_tool_encrypter( + encrypter, _ = BuiltinToolManageService.create_tool_encrypter( tenant_id, default_provider, default_provider.provider, provider_controller ) @@ -343,20 +369,28 @@ class BuiltinToolManageService: """ delete tool provider """ - tool_provider = BuiltinToolManageService.get_builtin_provider_by_id(tenant_id, credential_id) + with Session(db.engine) as session: + db_provider = ( + session.query(BuiltinToolProvider) + .filter( + BuiltinToolProvider.tenant_id == tenant_id, + BuiltinToolProvider.id == credential_id, + ) + .first() + ) - if tool_provider is None: - raise ValueError(f"you have not added provider {provider}") + if db_provider is None: + raise ValueError(f"you have not added provider {provider}") - db.session.delete(tool_provider) - db.session.commit() + session.delete(db_provider) + session.commit() - # delete cache - provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) - _, cache = BuiltinToolManageService.create_tool_encrypter( - tenant_id, tool_provider, provider, provider_controller - ) - cache.delete() + # delete cache + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + _, cache = BuiltinToolManageService.create_tool_encrypter( + tenant_id, db_provider, provider, provider_controller + ) + cache.delete() return {"result": "success"} @@ -507,18 +541,6 @@ class BuiltinToolManageService: return BuiltinToolProviderSort.sort(result) - @staticmethod - def get_builtin_provider_by_id(tenant_id: str, credential_id: str) -> Optional[BuiltinToolProvider]: - provider: Optional[BuiltinToolProvider] = ( - db.session.query(BuiltinToolProvider) - .filter( - BuiltinToolProvider.tenant_id == tenant_id, - BuiltinToolProvider.id == credential_id, - ) - .first() - ) - return provider - @staticmethod def get_builtin_provider(provider_name: str, tenant_id: str) -> Optional[BuiltinToolProvider]: """ From edf5fd28c9279cb3b53d65591b671aba497582b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E9=9C=B2=E5=85=88=E7=94=9F?= Date: Thu, 10 Jul 2025 14:21:34 +0800 Subject: [PATCH 03/15] update worklow events logs. (#19871) Signed-off-by: zhanluxianshen --- api/core/workflow/callbacks/workflow_logging_callback.py | 6 +++--- api/core/workflow/graph_engine/entities/graph.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/core/workflow/callbacks/workflow_logging_callback.py b/api/core/workflow/callbacks/workflow_logging_callback.py index e6813a3997..12b5203ca3 100644 --- a/api/core/workflow/callbacks/workflow_logging_callback.py +++ b/api/core/workflow/callbacks/workflow_logging_callback.py @@ -232,14 +232,14 @@ class WorkflowLoggingCallback(WorkflowCallback): Publish loop started """ self.print_text("\n[LoopRunStartedEvent]", color="blue") - self.print_text(f"Loop Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") def on_workflow_loop_next(self, event: LoopRunNextEvent) -> None: """ Publish loop next """ self.print_text("\n[LoopRunNextEvent]", color="blue") - self.print_text(f"Loop Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") self.print_text(f"Loop Index: {event.index}", color="blue") def on_workflow_loop_completed(self, event: LoopRunSucceededEvent | LoopRunFailedEvent) -> None: @@ -250,7 +250,7 @@ class WorkflowLoggingCallback(WorkflowCallback): "\n[LoopRunSucceededEvent]" if isinstance(event, LoopRunSucceededEvent) else "\n[LoopRunFailedEvent]", color="blue", ) - self.print_text(f"Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") def print_text(self, text: str, color: Optional[str] = None, end: str = "\n") -> None: """Print text with highlighting and no end characters.""" diff --git a/api/core/workflow/graph_engine/entities/graph.py b/api/core/workflow/graph_engine/entities/graph.py index 8e5b1e7142..362777a199 100644 --- a/api/core/workflow/graph_engine/entities/graph.py +++ b/api/core/workflow/graph_engine/entities/graph.py @@ -334,7 +334,7 @@ class Graph(BaseModel): parallel = GraphParallel( start_from_node_id=start_node_id, - parent_parallel_id=parent_parallel.id if parent_parallel else None, + parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel.start_from_node_id if parent_parallel else None, ) parallel_mapping[parallel.id] = parallel From 94a13d7d62067f427d2419a5b8541c2236d95a40 Mon Sep 17 00:00:00 2001 From: Yeuoly <45712896+Yeuoly@users.noreply.github.com> Date: Thu, 10 Jul 2025 14:43:31 +0800 Subject: [PATCH 04/15] feat: add support for dark icons in provider and tool entities (#22081) --- api/core/model_runtime/entities/provider_entities.py | 2 ++ api/core/plugin/entities/plugin.py | 1 + api/core/tools/entities/api_entities.py | 2 ++ api/core/tools/entities/tool_entities.py | 1 + api/core/workflow/nodes/tool/tool_node.py | 2 ++ api/services/tools/tools_transform_service.py | 10 ++++++++++ 6 files changed, 18 insertions(+) diff --git a/api/core/model_runtime/entities/provider_entities.py b/api/core/model_runtime/entities/provider_entities.py index d0f9ee13e5..c9aa8d1474 100644 --- a/api/core/model_runtime/entities/provider_entities.py +++ b/api/core/model_runtime/entities/provider_entities.py @@ -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] diff --git a/api/core/plugin/entities/plugin.py b/api/core/plugin/entities/plugin.py index d6bbf05097..e5cf7ee03a 100644 --- a/api/core/plugin/entities/plugin.py +++ b/api/core/plugin/entities/plugin.py @@ -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 diff --git a/api/core/tools/entities/api_entities.py b/api/core/tools/entities/api_entities.py index b94d6bba21..90134ba71d 100644 --- a/api/core/tools/entities/api_entities.py +++ b/api/core/tools/entities/api_entities.py @@ -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, diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index ba5fa5e156..bd216dad64 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -317,6 +317,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=[], diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index f5898dd605..5c6fac9080 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -339,10 +339,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, diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index 8009c384b7..ac127ae93e 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -75,10 +75,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( @@ -96,6 +104,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={}, @@ -179,6 +188,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={}, From c51b4290dc77d194f1c54778dbe14e3eced9ac77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 10 Jul 2025 16:14:18 +0800 Subject: [PATCH 05/15] fix: mcp server card button display (#22141) --- web/app/components/tools/mcp/mcp-service-card.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx index 443d7a1d1f..c0c542da26 100644 --- a/web/app/components/tools/mcp/mcp-service-card.tsx +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -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')} + +
+ +
{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}
+
From 7b2cab576775b4c36bcfd4763b0227c8bf52ffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 10 Jul 2025 16:14:46 +0800 Subject: [PATCH 06/15] feat: support ping method for MCP server (#22144) --- api/core/mcp/server/streamable_http.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 6b422ae4ae..37eec3cd9c 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -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,6 +106,9 @@ 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 From 0e793a660de9f7f47a177ad76cdb36071972c2e7 Mon Sep 17 00:00:00 2001 From: Novice Date: Thu, 10 Jul 2025 17:13:48 +0800 Subject: [PATCH 07/15] fix: add the default value to the dark icon (#22149) --- api/core/workflow/nodes/tool/tool_node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 5c6fac9080..48627a229d 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -329,6 +329,7 @@ class ToolNode(BaseNode[ToolNodeData]): icon = current_plugin.declaration.icon except StopIteration: pass + icon_dark = None try: builtin_tool = next( provider From 11f9a897e89a383c08730193b0de2660cdfc59af Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 10 Jul 2025 17:33:11 +0800 Subject: [PATCH 08/15] chore: fix schema editor can not hover item (#22155) --- .../visual-editor/schema-node.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx index 96bbf999db..36671ab050 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx @@ -79,13 +79,13 @@ const SchemaNode: FC = ({ } 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 = ({
{depth > 0 && hasChildren && (
Date: Thu, 10 Jul 2025 17:49:32 +0800 Subject: [PATCH 09/15] chore(version): bump to 1.6.0 (#22136) --- api/pyproject.toml | 2 +- api/uv.lock | 2 +- docker/docker-compose-template.yaml | 6 +++--- docker/docker-compose.yaml | 6 +++--- web/package.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 9f2e3ed331..420bc771b6 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dify-api" -version = "1.5.1" +version = "1.6.0" requires-python = ">=3.11,<3.13" dependencies = [ diff --git a/api/uv.lock b/api/uv.lock index 45831e24a1..e108e0c445 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1217,7 +1217,7 @@ wheels = [ [[package]] name = "dify-api" -version = "1.5.1" +version = "1.6.0" source = { virtual = "." } dependencies = [ { name = "arize-phoenix-otel" }, diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index fd7c78c7e7..7c1544acb9 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -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. @@ -57,7 +57,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:-} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 0a95251ff0..647af62d96 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -518,7 +518,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. @@ -547,7 +547,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. @@ -573,7 +573,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:-} diff --git a/web/package.json b/web/package.json index 254c2ec1fd..c9219b53d0 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "1.5.1", + "version": "1.6.0", "private": true, "engines": { "node": ">=v22.11.0" From f4df80e093dfc433013fde12f52bb1b32b3f7b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AF=97=E6=B5=93?= Date: Thu, 10 Jul 2025 20:56:45 +0800 Subject: [PATCH 10/15] fix(custom_tool): omit optional parameters instead of setting them to None (#22171) --- api/core/tools/custom_tool/tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/core/tools/custom_tool/tool.py b/api/core/tools/custom_tool/tool.py index 2f5cc6d4c0..5cba4cf7f5 100644 --- a/api/core/tools/custom_tool/tool.py +++ b/api/core/tools/custom_tool/tool.py @@ -213,7 +213,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 From f929bfb94c240d87dbd22599f091129e0c45ef68 Mon Sep 17 00:00:00 2001 From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Date: Fri, 11 Jul 2025 09:40:17 +0800 Subject: [PATCH 11/15] minor fix: remove duplicates, fix typo, and add restriction for get mcp server (#22170) Signed-off-by: neatguycoding <15627489+NeatGuyCoding@users.noreply.github.com> --- api/controllers/console/app/mcp_server.py | 6 +++++- api/core/mcp/server/streamable_http.py | 4 ++-- api/services/tools/mcp_tools_mange_service.py | 1 - 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py index 4f9e75c0d3..ccda97d80c 100644 --- a/api/controllers/console/app/mcp_server.py +++ b/api/controllers/console/app/mcp_server.py @@ -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) diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 37eec3cd9c..1c2cf570e2 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -112,13 +112,13 @@ class MCPServerStreamableHTTPRequestHandler: 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, ) diff --git a/api/services/tools/mcp_tools_mange_service.py b/api/services/tools/mcp_tools_mange_service.py index 3b1592230a..7c23abda4b 100644 --- a/api/services/tools/mcp_tools_mange_service.py +++ b/api/services/tools/mcp_tools_mange_service.py @@ -69,7 +69,6 @@ class MCPToolManageService: MCPToolProvider.server_url_hash == server_url_hash, MCPToolProvider.server_identifier == server_identifier, ), - MCPToolProvider.tenant_id == tenant_id, ) .first() ) From e576b989b8eca4a7d8cfe6bdc91e9a451580489b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AF=97=E6=B5=93?= Date: Fri, 11 Jul 2025 10:39:20 +0800 Subject: [PATCH 12/15] feat(tool): add support for API key authentication via query parameter (#21656) --- api/core/tools/custom_tool/provider.py | 28 ++++++++-- api/core/tools/custom_tool/tool.py | 18 ++++++- api/core/tools/entities/tool_entities.py | 3 +- api/core/tools/tool_manager.py | 18 ++++++- api/services/tools/tools_transform_service.py | 11 ++-- .../config-credentials.tsx | 52 ++++++++++++++++--- web/app/components/tools/types.ts | 4 +- web/i18n/en-US/tools.ts | 6 ++- web/i18n/zh-Hans/tools.ts | 6 ++- 9 files changed, 126 insertions(+), 20 deletions(-) diff --git a/api/core/tools/custom_tool/provider.py b/api/core/tools/custom_tool/provider.py index 3137d32013..fbe1d79137 100644 --- a/api/core/tools/custom_tool/provider.py +++ b/api/core/tools/custom_tool/provider.py @@ -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 diff --git a/api/core/tools/custom_tool/tool.py b/api/core/tools/custom_tool/tool.py index 5cba4cf7f5..10653b9948 100644 --- a/api/core/tools/custom_tool/tool.py +++ b/api/core/tools/custom_tool/tool.py @@ -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) diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index bd216dad64..b5148e245f 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -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": diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index adae56cd27..22a9853b41 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -684,9 +684,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) @@ -745,9 +752,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 tool_configuration = ProviderConfigEncrypter( diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index ac127ae93e..3d0c35cd9b 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -159,11 +159,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 diff --git a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx index cbf1048b09..f0ad13f9b1 100644 --- a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx @@ -68,23 +68,34 @@ const ConfigCredential: FC = ({ 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, + })} /> 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, })} /> + setTempCredential({ + auth_type: value as AuthType, + api_key_query_param: tempCredential.api_key_query_param || 'key', + api_key_value: tempCredential.api_key_value || '', + })} + />
- {tempCredential.auth_type === AuthType.apiKey && ( + {tempCredential.auth_type === AuthType.apiKeyHeader && ( <>
{t('tools.createTool.authHeaderPrefix.title')}
@@ -136,6 +147,35 @@ const ConfigCredential: FC = ({ />
)} + {tempCredential.auth_type === AuthType.apiKeyQuery && ( + <> +
+
+ {t('tools.createTool.authMethod.queryParam')} + + {t('tools.createTool.authMethod.queryParamTooltip')} +
+ } + triggerClassName='ml-0.5 w-4 h-4' + /> +
+ setTempCredential({ ...tempCredential, api_key_query_param: e.target.value })} + placeholder={t('tools.createTool.authMethod.types.queryParamPlaceholder')!} + /> + +
+
{t('tools.createTool.authMethod.value')}
+ setTempCredential({ ...tempCredential, api_key_value: e.target.value })} + placeholder={t('tools.createTool.authMethod.types.apiValuePlaceholder')!} + /> +
+ )} diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index d444ee1f38..b83919ad18 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -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 { diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index 418d1cb076..4e1ce1308a 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -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', diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 4e0ccf476f..5c1eb13236 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -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: '值', From c805238471eb4f15daa830730dbc828bd468f051 Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:17:28 +0800 Subject: [PATCH 13/15] fix: adjust layout styles for header and dataset update (#22182) --- web/app/components/datasets/create/index.tsx | 2 +- .../notion-page-preview/index.module.css | 2 +- .../datasets/create/step-one/index.tsx | 43 ++++++++++++------- web/app/components/header/index.tsx | 2 +- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx index b1e4087226..a1ff2f5d87 100644 --- a/web/app/components/datasets/create/index.tsx +++ b/web/app/components/datasets/create/index.tsx @@ -122,7 +122,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { return return ( -
+
{step === 1 && 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 = ({
{ shouldShowDataSourceTypeList && ( -
+
{t('datasetCreation.steps.one')}
) @@ -158,8 +163,8 @@ const StepOne = ({ if (dataSourceTypeDisable) return changeType(DataSourceType.FILE) - hideFilePreview() hideNotionPagePreview() + hideWebsitePreview() }} > @@ -182,7 +187,7 @@ const StepOne = ({ return changeType(DataSourceType.NOTION) hideFilePreview() - hideNotionPagePreview() + hideWebsitePreview() }} > @@ -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() + }} >
{ } return ( -
+
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo From d5624ba671201137c32b80ad6e2131feb85fed19 Mon Sep 17 00:00:00 2001 From: K <84141602+krikera@users.noreply.github.com> Date: Fri, 11 Jul 2025 09:41:59 +0530 Subject: [PATCH 14/15] fix: resolve Docker file URL networking issue for plugins (#21334) (#21382) Co-authored-by: crazywoola <427733928@qq.com> --- api/.env.example | 5 +++++ api/configs/feature/__init__.py | 7 +++++++ api/core/file/helpers.py | 4 +++- api/core/tools/signature.py | 5 +++-- api/core/tools/tool_file_manager.py | 5 +++-- docker/.env.example | 5 +++++ docker/docker-compose.yaml | 1 + 7 files changed, 27 insertions(+), 5 deletions(-) diff --git a/api/.env.example b/api/.env.example index baa9c382c8..a7ea6cf937 100644 --- a/api/.env.example +++ b/api/.env.example @@ -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 diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index df15b92c35..963fcbedf9 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -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, diff --git a/api/core/file/helpers.py b/api/core/file/helpers.py index 73fabdb11b..335ad2266a 100644 --- a/api/core/file/helpers.py +++ b/api/core/file/helpers.py @@ -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" diff --git a/api/core/tools/signature.py b/api/core/tools/signature.py index e80005d7bf..5cdf473542 100644 --- a/api/core/tools/signature.py +++ b/api/core/tools/signature.py @@ -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())) diff --git a/api/core/tools/tool_file_manager.py b/api/core/tools/tool_file_manager.py index b849f51064..ece02f9d59 100644 --- a/api/core/tools/tool_file_manager.py +++ b/api/core/tools/tool_file_manager.py @@ -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())) diff --git a/docker/.env.example b/docker/.env.example index a403f25cb2..84b6152f0a 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -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 # ------------------------------ diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 647af62d96..ac9953aa33 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -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} From 545c21b1966db0c19ec6369779c1438fd0b66df8 Mon Sep 17 00:00:00 2001 From: Harry Date: Fri, 11 Jul 2025 13:51:31 +0800 Subject: [PATCH 15/15] feat(oauth): clean up imports and streamline OAuth client parameter retrieval --- api/controllers/console/workspace/tool_providers.py | 8 ++------ api/core/tools/tool_manager.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index 666759a566..bdcd5acdf3 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -1,7 +1,6 @@ import io from urllib.parse import urlparse -from flask import redirect, send_file from flask import make_response, redirect, request, send_file from flask_login import current_user from flask_restful import ( @@ -18,7 +17,6 @@ from controllers.console.wraps import ( 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 @@ -695,10 +693,7 @@ class ToolPluginOAuthApi(Resource): raise Forbidden() tenant_id = user.current_tenant_id - oauth_client_params = BuiltinToolManageService.get_oauth_client( - tenant_id=tenant_id, - provider=provider - ) + oauth_client_params = BuiltinToolManageService.get_oauth_client(tenant_id=tenant_id, provider=provider) if oauth_client_params is None: raise Forbidden("no oauth available client config found for this tool provider") @@ -851,6 +846,7 @@ api.add_resource(ToolOAuthCallback, "/oauth/plugin//tool/callback api.add_resource(ToolOAuthCustomClient, "/workspaces/current/tool-provider/builtin//oauth/custom-client") + class ToolProviderMCPApi(Resource): @setup_required @login_required diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index f96aac81bb..9d5000f8d4 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -46,7 +46,7 @@ from core.tools.entities.tool_entities import ( ToolParameter, ToolProviderType, ) -from core.tools.errors import ToolNotFoundError, ToolProviderNotFoundError +from core.tools.errors import ToolProviderNotFoundError from core.tools.tool_label_manager import ToolLabelManager from core.tools.utils.configuration import ( ToolParameterConfigurationManager,