diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index cc735ae67c..b933560a5e 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -6,6 +6,7 @@ on: - "main" - "deploy/dev" - "deploy/enterprise" + - "build/**" tags: - "*" diff --git a/README.md b/README.md index 1dc7e2dd98..b8bd6d0725 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ README in বাংলা

-Dify is an open-source LLM app development platform. Its intuitive interface combines agentic AI workflow, RAG pipeline, agent capabilities, model management, observability features, and more, allowing you to quickly move from prototype to production. +Dify is an open-source platform for developing LLM applications. Its intuitive interface combines agentic AI workflows, RAG pipelines, agent capabilities, model management, observability features, and more—allowing you to quickly move from prototype to production. ## Quick start diff --git a/api/.env.example b/api/.env.example index 7b08c032ed..acbb069605 100644 --- a/api/.env.example +++ b/api/.env.example @@ -456,6 +456,16 @@ APP_MAX_ACTIVE_REQUESTS=0 # Celery beat configuration CELERY_BEAT_SCHEDULER_TIME=1 +# Celery schedule tasks configuration +ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false +ENABLE_CLEAN_UNUSED_DATASETS_TASK=false +ENABLE_CREATE_TIDB_SERVERLESS_TASK=false +ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false +ENABLE_CLEAN_MESSAGES=false +ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false +ENABLE_DATASETS_QUEUE_MONITOR=false +ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true + # Position configuration POSITION_TOOL_PINS= POSITION_TOOL_INCLUDES= diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 2fd9f94e06..75d66b1d81 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -805,6 +805,41 @@ class CeleryBeatConfig(BaseSettings): ) +class CeleryScheduleTasksConfig(BaseSettings): + ENABLE_CLEAN_EMBEDDING_CACHE_TASK: bool = Field( + description="Enable clean embedding cache task", + default=False, + ) + ENABLE_CLEAN_UNUSED_DATASETS_TASK: bool = Field( + description="Enable clean unused datasets task", + default=False, + ) + ENABLE_CREATE_TIDB_SERVERLESS_TASK: bool = Field( + description="Enable create tidb service job task", + default=False, + ) + ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK: bool = Field( + description="Enable update tidb service job status task", + default=False, + ) + ENABLE_CLEAN_MESSAGES: bool = Field( + description="Enable clean messages task", + default=False, + ) + ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK: bool = Field( + description="Enable mail clean document notify task", + default=False, + ) + ENABLE_DATASETS_QUEUE_MONITOR: bool = Field( + description="Enable queue monitor task", + default=False, + ) + ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK: bool = Field( + description="Enable check upgradable plugin task", + default=True, + ) + + class PositionConfig(BaseSettings): POSITION_PROVIDER_PINS: str = Field( description="Comma-separated list of pinned model providers", @@ -933,5 +968,6 @@ class FeatureConfig( # hosted services config HostedServiceConfig, CeleryBeatConfig, + CeleryScheduleTasksConfig, ): pass diff --git a/api/constants/__init__.py b/api/constants/__init__.py index a84de0a451..9e052320ac 100644 --- a/api/constants/__init__.py +++ b/api/constants/__init__.py @@ -1,6 +1,7 @@ from configs import dify_config HIDDEN_VALUE = "[__HIDDEN__]" +UNKNOWN_VALUE = "[__UNKNOWN__]" UUID_NIL = "00000000-0000-0000-0000-000000000000" DEFAULT_FILE_NUMBER_LIMITS = 3 diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py index ccda97d80c..0f53860f56 100644 --- a/api/controllers/console/app/mcp_server.py +++ b/api/controllers/console/app/mcp_server.py @@ -92,7 +92,8 @@ class AppMCPServerRefreshController(Resource): raise NotFound() server = ( db.session.query(AppMCPServer) - .filter(AppMCPServer.id == server_id and AppMCPServer.tenant_id == current_user.current_tenant_id) + .filter(AppMCPServer.id == server_id) + .filter(AppMCPServer.tenant_id == current_user.current_tenant_id) .first() ) if not server: diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index a9f088a276..fc91462e2b 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -23,10 +23,11 @@ from core.app.app_config.features.file_upload.manager import FileUploadConfigMan from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom from core.file.models import File +from core.llm_generator.llm_generator import LLMGenerator from extensions.ext_database import db from factories import file_factory, variable_factory from fields.workflow_fields import workflow_fields, workflow_pagination_fields -from fields.workflow_run_fields import workflow_run_node_execution_fields +from fields.workflow_run_fields import workflow_node_ai_modify_fields, workflow_run_node_execution_fields from libs import helper from libs.helper import TimestampField, uuid_value from libs.login import current_user, login_required @@ -788,6 +789,43 @@ class DraftWorkflowNodeLastRunApi(Resource): raise NotFound("last run not found") return node_exec +class DraftWorkflowNodeAiModifyApi(Resource): + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @marshal_with(workflow_node_ai_modify_fields) + def post(self, app_model: App, node_id: str): + parser = reqparse.RequestParser() + parser.add_argument("current", type=str, required=False, location="json") + parser.add_argument("model_config", type=dict, required=False, location="json") + args = parser.parse_args() + workflow_service = WorkflowService() + workflow_draft = workflow_service.get_draft_workflow(app_model) + if not workflow_draft: + raise NotFound("Workflow not found") + node_last_execution = workflow_service.get_node_last_run( + app_model=app_model, + workflow=workflow_draft, + node_id=node_id, + ) + if node_last_execution is None: + raise NotFound("last run not found") + result = LLMGenerator.generate_prompt_optimization( + tenant_id= app_model.tenant_id, + message=args.get("message", ""), + last_run= { + "inputs": node_last_execution.inputs, + "process_data": node_last_execution.process_data, + "outputs": node_last_execution.outputs, + }, + current=args.get("current", ""), + model_config=args.get("model_config", {}), + ) + return result + + + api.add_resource( DraftWorkflowApi, @@ -857,3 +895,7 @@ api.add_resource( DraftWorkflowNodeLastRunApi, "/apps//workflows/draft/nodes//last-run", ) +api.add_resource( + DraftWorkflowNodeAiModifyApi, + "/apps//workflows/draft/nodes//ai-modify" +) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 913f820b59..c392404abd 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -415,6 +415,7 @@ class ChangeEmailSendEmailApi(Resource): if user_email != current_user.email: raise InvalidEmailError() + else: with Session(db.engine) as session: account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none() diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index c0a4734828..09846d5c94 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -12,7 +12,8 @@ from controllers.console.wraps import account_initialization_required, setup_req from core.model_runtime.utils.encoders import jsonable_encoder from core.plugin.impl.exc import PluginDaemonClientSideError from libs.login import login_required -from models.account import TenantPluginPermission +from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission +from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService from services.plugin.plugin_parameter_service import PluginParameterService from services.plugin.plugin_permission_service import PluginPermissionService from services.plugin.plugin_service import PluginService @@ -534,6 +535,114 @@ class PluginFetchDynamicSelectOptionsApi(Resource): return jsonable_encoder({"options": options}) +class PluginChangePreferencesApi(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self): + user = current_user + if not user.is_admin_or_owner: + raise Forbidden() + + req = reqparse.RequestParser() + req.add_argument("permission", type=dict, required=True, location="json") + req.add_argument("auto_upgrade", type=dict, required=True, location="json") + args = req.parse_args() + + tenant_id = user.current_tenant_id + + permission = args["permission"] + + install_permission = TenantPluginPermission.InstallPermission(permission.get("install_permission", "everyone")) + debug_permission = TenantPluginPermission.DebugPermission(permission.get("debug_permission", "everyone")) + + auto_upgrade = args["auto_upgrade"] + + strategy_setting = TenantPluginAutoUpgradeStrategy.StrategySetting( + auto_upgrade.get("strategy_setting", "fix_only") + ) + upgrade_time_of_day = auto_upgrade.get("upgrade_time_of_day", 0) + upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode(auto_upgrade.get("upgrade_mode", "exclude")) + exclude_plugins = auto_upgrade.get("exclude_plugins", []) + include_plugins = auto_upgrade.get("include_plugins", []) + + # set permission + set_permission_result = PluginPermissionService.change_permission( + tenant_id, + install_permission, + debug_permission, + ) + if not set_permission_result: + return jsonable_encoder({"success": False, "message": "Failed to set permission"}) + + # set auto upgrade strategy + set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy( + tenant_id, + strategy_setting, + upgrade_time_of_day, + upgrade_mode, + exclude_plugins, + include_plugins, + ) + if not set_auto_upgrade_strategy_result: + return jsonable_encoder({"success": False, "message": "Failed to set auto upgrade strategy"}) + + return jsonable_encoder({"success": True}) + + +class PluginFetchPreferencesApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self): + tenant_id = current_user.current_tenant_id + + permission = PluginPermissionService.get_permission(tenant_id) + permission_dict = { + "install_permission": TenantPluginPermission.InstallPermission.EVERYONE, + "debug_permission": TenantPluginPermission.DebugPermission.EVERYONE, + } + + if permission: + permission_dict["install_permission"] = permission.install_permission + permission_dict["debug_permission"] = permission.debug_permission + + auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id) + auto_upgrade_dict = { + "strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED, + "upgrade_time_of_day": 0, + "upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + "exclude_plugins": [], + "include_plugins": [], + } + + if auto_upgrade: + auto_upgrade_dict = { + "strategy_setting": auto_upgrade.strategy_setting, + "upgrade_time_of_day": auto_upgrade.upgrade_time_of_day, + "upgrade_mode": auto_upgrade.upgrade_mode, + "exclude_plugins": auto_upgrade.exclude_plugins, + "include_plugins": auto_upgrade.include_plugins, + } + + return jsonable_encoder({"permission": permission_dict, "auto_upgrade": auto_upgrade_dict}) + + +class PluginAutoUpgradeExcludePluginApi(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self): + # exclude one single plugin + tenant_id = current_user.current_tenant_id + + req = reqparse.RequestParser() + req.add_argument("plugin_id", type=str, required=True, location="json") + args = req.parse_args() + + return jsonable_encoder({"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args["plugin_id"])}) + + api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") api.add_resource(PluginListApi, "/workspaces/current/plugin/list") api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") @@ -560,3 +669,7 @@ api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permissi api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options") + +api.add_resource(PluginFetchPreferencesApi, "/workspaces/current/plugin/preferences/fetch") +api.add_resource(PluginChangePreferencesApi, "/workspaces/current/plugin/preferences/change") +api.add_resource(PluginAutoUpgradeExcludePluginApi, "/workspaces/current/plugin/preferences/autoupgrade/exclude") diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index df50871a38..9999f7725e 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -1,23 +1,32 @@ 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 Resource, reqparse -from sqlalchemy.orm import Session +from flask_restful import ( + Resource, + reqparse, +) 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 from core.mcp.mcp_client import MCPClient from core.model_runtime.utils.encoders import jsonable_encoder -from extensions.ext_database import db -from libs.helper import alphanumeric, uuid_value +from core.plugin.entities.plugin import ToolProviderID +from core.plugin.impl.oauth import OAuthHandler +from core.tools.entities.tool_entities import CredentialType +from libs.helper import StrLen, alphanumeric, uuid_value from libs.login import login_required +from services.plugin.oauth_service import OAuthProxyService from services.tools.api_tools_manage_service import ApiToolManageService from services.tools.builtin_tools_manage_service import BuiltinToolManageService from services.tools.mcp_tools_mange_service import MCPToolManageService @@ -84,12 +93,9 @@ class ToolBuiltinProviderInfoApi(Resource): @login_required @account_initialization_required def get(self, provider): - user = current_user - - user_id = user.id - tenant_id = user.current_tenant_id + tenant_id = current_user.current_tenant_id - return jsonable_encoder(BuiltinToolManageService.get_builtin_tool_provider_info(user_id, tenant_id, provider)) + return jsonable_encoder(BuiltinToolManageService.get_builtin_tool_provider_info(tenant_id, provider)) class ToolBuiltinProviderDeleteApi(Resource): @@ -98,17 +104,47 @@ class ToolBuiltinProviderDeleteApi(Resource): @account_initialization_required def post(self, provider): user = current_user - if not user.is_admin_or_owner: raise Forbidden() - user_id = user.id tenant_id = user.current_tenant_id + req = reqparse.RequestParser() + req.add_argument("credential_id", type=str, required=True, nullable=False, location="json") + args = req.parse_args() return BuiltinToolManageService.delete_builtin_tool_provider( - user_id, tenant_id, provider, + args["credential_id"], + ) + + +class ToolBuiltinProviderAddApi(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self, provider): + user = current_user + + user_id = user.id + tenant_id = user.current_tenant_id + + parser = reqparse.RequestParser() + parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json") + parser.add_argument("name", type=StrLen(30), required=False, nullable=False, location="json") + parser.add_argument("type", type=str, required=True, nullable=False, location="json") + args = parser.parse_args() + + if args["type"] not in CredentialType.values(): + raise ValueError(f"Invalid credential type: {args['type']}") + + return BuiltinToolManageService.add_builtin_tool_provider( + user_id=user_id, + tenant_id=tenant_id, + provider=provider, + credentials=args["credentials"], + name=args["name"], + api_type=CredentialType.of(args["type"]), ) @@ -126,19 +162,20 @@ class ToolBuiltinProviderUpdateApi(Resource): tenant_id = user.current_tenant_id parser = reqparse.RequestParser() - parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json") + parser.add_argument("credential_id", type=str, required=True, nullable=False, location="json") + parser.add_argument("credentials", type=dict, required=False, nullable=True, location="json") + parser.add_argument("name", type=StrLen(30), required=False, nullable=True, location="json") args = parser.parse_args() - with Session(db.engine) as session: - result = BuiltinToolManageService.update_builtin_tool_provider( - session=session, - user_id=user_id, - tenant_id=tenant_id, - provider_name=provider, - credentials=args["credentials"], - ) - session.commit() + result = BuiltinToolManageService.update_builtin_tool_provider( + user_id=user_id, + tenant_id=tenant_id, + provider=provider, + credential_id=args["credential_id"], + credentials=args.get("credentials", None), + name=args.get("name", ""), + ) return result @@ -149,9 +186,11 @@ class ToolBuiltinProviderGetCredentialsApi(Resource): def get(self, provider): tenant_id = current_user.current_tenant_id - return BuiltinToolManageService.get_builtin_tool_provider_credentials( - tenant_id=tenant_id, - provider_name=provider, + return jsonable_encoder( + BuiltinToolManageService.get_builtin_tool_provider_credentials( + tenant_id=tenant_id, + provider_name=provider, + ) ) @@ -344,12 +383,15 @@ class ToolBuiltinProviderCredentialsSchemaApi(Resource): @setup_required @login_required @account_initialization_required - def get(self, provider): + def get(self, provider, credential_type): user = current_user - tenant_id = user.current_tenant_id - return BuiltinToolManageService.list_builtin_provider_credentials_schema(provider, tenant_id) + return jsonable_encoder( + BuiltinToolManageService.list_builtin_provider_credentials_schema( + provider, CredentialType.of(credential_type), tenant_id + ) + ) class ToolApiProviderSchemaApi(Resource): @@ -586,15 +628,12 @@ class ToolApiListApi(Resource): @account_initialization_required def get(self): user = current_user - - user_id = user.id tenant_id = user.current_tenant_id return jsonable_encoder( [ provider.to_dict() for provider in ApiToolManageService.list_api_tools( - user_id, tenant_id, ) ] @@ -631,6 +670,168 @@ class ToolLabelsApi(Resource): return jsonable_encoder(ToolLabelsService.list_tool_labels()) +class ToolPluginOAuthApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, provider): + tool_provider = ToolProviderID(provider) + plugin_id = tool_provider.plugin_id + provider_name = tool_provider.provider_name + + # todo check permission + user = current_user + + if not user.is_admin_or_owner: + raise Forbidden() + + tenant_id = user.current_tenant_id + 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") + + oauth_handler = OAuthHandler() + context_id = OAuthProxyService.create_proxy_context( + user_id=current_user.id, tenant_id=tenant_id, plugin_id=plugin_id, provider=provider_name + ) + redirect_uri = f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider}/tool/callback" + authorization_url_response = oauth_handler.get_authorization_url( + tenant_id=tenant_id, + user_id=user.id, + plugin_id=plugin_id, + provider=provider_name, + redirect_uri=redirect_uri, + system_credentials=oauth_client_params, + ) + response = make_response(jsonable_encoder(authorization_url_response)) + response.set_cookie( + "context_id", + context_id, + httponly=True, + samesite="Lax", + max_age=OAuthProxyService.__MAX_AGE__, + ) + return response + + +class ToolOAuthCallback(Resource): + @setup_required + def get(self, provider): + context_id = request.cookies.get("context_id") + if not context_id: + raise Forbidden("context_id not found") + + context = OAuthProxyService.use_proxy_context(context_id) + if context is None: + raise Forbidden("Invalid context_id") + + tool_provider = ToolProviderID(provider) + plugin_id = tool_provider.plugin_id + provider_name = tool_provider.provider_name + user_id, tenant_id = context.get("user_id"), context.get("tenant_id") + + oauth_handler = OAuthHandler() + oauth_client_params = BuiltinToolManageService.get_oauth_client(tenant_id, provider) + if oauth_client_params is None: + raise Forbidden("no oauth available client config found for this tool provider") + + redirect_uri = f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider}/tool/callback" + credentials = oauth_handler.get_credentials( + tenant_id=tenant_id, + user_id=user_id, + plugin_id=plugin_id, + provider=provider_name, + redirect_uri=redirect_uri, + system_credentials=oauth_client_params, + request=request, + ).credentials + + if not credentials: + raise Exception("the plugin credentials failed") + + # add credentials to database + BuiltinToolManageService.add_builtin_tool_provider( + user_id=user_id, + tenant_id=tenant_id, + provider=provider, + credentials=dict(credentials), + api_type=CredentialType.OAUTH2, + ) + return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback") + + +class ToolBuiltinProviderSetDefaultApi(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self, provider): + parser = reqparse.RequestParser() + parser.add_argument("id", type=str, required=True, nullable=False, location="json") + args = parser.parse_args() + return BuiltinToolManageService.set_default_provider( + tenant_id=current_user.current_tenant_id, user_id=current_user.id, provider=provider, id=args["id"] + ) + + +class ToolOAuthCustomClient(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self, provider): + parser = reqparse.RequestParser() + parser.add_argument("client_params", type=dict, required=False, nullable=True, location="json") + parser.add_argument("enable_oauth_custom_client", type=bool, required=False, nullable=True, location="json") + args = parser.parse_args() + + user = current_user + + if not user.is_admin_or_owner: + raise Forbidden() + + return BuiltinToolManageService.save_custom_oauth_client_params( + tenant_id=user.current_tenant_id, + provider=provider, + client_params=args.get("client_params", {}), + enable_oauth_custom_client=args.get("enable_oauth_custom_client", True), + ) + + @setup_required + @login_required + @account_initialization_required + def get(self, provider): + return jsonable_encoder( + BuiltinToolManageService.get_custom_oauth_client_params( + tenant_id=current_user.current_tenant_id, provider=provider + ) + ) + + +class ToolBuiltinProviderGetOauthClientSchemaApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, provider): + return jsonable_encoder( + BuiltinToolManageService.get_builtin_tool_provider_oauth_client_schema( + tenant_id=current_user.current_tenant_id, provider_name=provider + ) + ) + + +class ToolBuiltinProviderGetCredentialInfoApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, provider): + tenant_id = current_user.current_tenant_id + + return jsonable_encoder( + BuiltinToolManageService.get_builtin_tool_provider_credential_info( + tenant_id=tenant_id, + provider=provider, + ) + ) + class ToolProviderMCPApi(Resource): @setup_required @login_required @@ -794,17 +995,33 @@ class ToolMCPCallbackApi(Resource): # tool provider api.add_resource(ToolProviderListApi, "/workspaces/current/tool-providers") +# tool oauth +api.add_resource(ToolPluginOAuthApi, "/oauth/plugin//tool/authorization-url") +api.add_resource(ToolOAuthCallback, "/oauth/plugin//tool/callback") +api.add_resource(ToolOAuthCustomClient, "/workspaces/current/tool-provider/builtin//oauth/custom-client") + # builtin tool provider api.add_resource(ToolBuiltinProviderListToolsApi, "/workspaces/current/tool-provider/builtin//tools") api.add_resource(ToolBuiltinProviderInfoApi, "/workspaces/current/tool-provider/builtin//info") +api.add_resource(ToolBuiltinProviderAddApi, "/workspaces/current/tool-provider/builtin//add") api.add_resource(ToolBuiltinProviderDeleteApi, "/workspaces/current/tool-provider/builtin//delete") api.add_resource(ToolBuiltinProviderUpdateApi, "/workspaces/current/tool-provider/builtin//update") +api.add_resource( + ToolBuiltinProviderSetDefaultApi, "/workspaces/current/tool-provider/builtin//default-credential" +) +api.add_resource( + ToolBuiltinProviderGetCredentialInfoApi, "/workspaces/current/tool-provider/builtin//credential/info" +) api.add_resource( ToolBuiltinProviderGetCredentialsApi, "/workspaces/current/tool-provider/builtin//credentials" ) api.add_resource( ToolBuiltinProviderCredentialsSchemaApi, - "/workspaces/current/tool-provider/builtin//credentials_schema", + "/workspaces/current/tool-provider/builtin//credential/schema/", +) +api.add_resource( + ToolBuiltinProviderGetOauthClientSchemaApi, + "/workspaces/current/tool-provider/builtin//oauth/client-schema", ) api.add_resource(ToolBuiltinProviderIconApi, "/workspaces/current/tool-provider/builtin//icon") diff --git a/api/controllers/inner_api/plugin/plugin.py b/api/controllers/inner_api/plugin/plugin.py index 327e9ce834..6c4500af50 100644 --- a/api/controllers/inner_api/plugin/plugin.py +++ b/api/controllers/inner_api/plugin/plugin.py @@ -175,6 +175,7 @@ class PluginInvokeToolApi(Resource): provider=payload.provider, tool_name=payload.tool, tool_parameters=payload.tool_parameters, + credential_id=payload.credential_id ), ) diff --git a/api/core/agent/entities.py b/api/core/agent/entities.py index 143a3a51aa..a31c1050bd 100644 --- a/api/core/agent/entities.py +++ b/api/core/agent/entities.py @@ -16,6 +16,7 @@ class AgentToolEntity(BaseModel): tool_name: str tool_parameters: dict[str, Any] = Field(default_factory=dict) plugin_unique_identifier: str | None = None + credential_id: str | None = None class AgentPromptEntity(BaseModel): diff --git a/api/core/agent/strategy/base.py b/api/core/agent/strategy/base.py index ead81a7a0e..a52a1dfd7a 100644 --- a/api/core/agent/strategy/base.py +++ b/api/core/agent/strategy/base.py @@ -4,6 +4,7 @@ from typing import Any, Optional from core.agent.entities import AgentInvokeMessage from core.agent.plugin_entities import AgentStrategyParameter +from core.plugin.entities.request import InvokeCredentials class BaseAgentStrategy(ABC): @@ -18,11 +19,12 @@ class BaseAgentStrategy(ABC): conversation_id: Optional[str] = None, app_id: Optional[str] = None, message_id: Optional[str] = None, + credentials: Optional[InvokeCredentials] = None, ) -> Generator[AgentInvokeMessage, None, None]: """ Invoke the agent strategy. """ - yield from self._invoke(params, user_id, conversation_id, app_id, message_id) + yield from self._invoke(params, user_id, conversation_id, app_id, message_id, credentials) def get_parameters(self) -> Sequence[AgentStrategyParameter]: """ @@ -38,5 +40,6 @@ class BaseAgentStrategy(ABC): conversation_id: Optional[str] = None, app_id: Optional[str] = None, message_id: Optional[str] = None, + credentials: Optional[InvokeCredentials] = None, ) -> Generator[AgentInvokeMessage, None, None]: pass diff --git a/api/core/agent/strategy/plugin.py b/api/core/agent/strategy/plugin.py index 4cfcfbf86a..90e5782c47 100644 --- a/api/core/agent/strategy/plugin.py +++ b/api/core/agent/strategy/plugin.py @@ -4,6 +4,7 @@ from typing import Any, Optional from core.agent.entities import AgentInvokeMessage from core.agent.plugin_entities import AgentStrategyEntity, AgentStrategyParameter from core.agent.strategy.base import BaseAgentStrategy +from core.plugin.entities.request import InvokeCredentials, PluginInvokeContext from core.plugin.impl.agent import PluginAgentClient from core.plugin.utils.converter import convert_parameters_to_plugin_format @@ -40,6 +41,7 @@ class PluginAgentStrategy(BaseAgentStrategy): conversation_id: Optional[str] = None, app_id: Optional[str] = None, message_id: Optional[str] = None, + credentials: Optional[InvokeCredentials] = None, ) -> Generator[AgentInvokeMessage, None, None]: """ Invoke the agent strategy. @@ -58,4 +60,7 @@ class PluginAgentStrategy(BaseAgentStrategy): conversation_id=conversation_id, app_id=app_id, message_id=message_id, + context=PluginInvokeContext( + credentials=credentials or InvokeCredentials() + ), ) diff --git a/api/core/app/app_config/easy_ui_based_app/agent/manager.py b/api/core/app/app_config/easy_ui_based_app/agent/manager.py index 590b944c0d..8887d2500c 100644 --- a/api/core/app/app_config/easy_ui_based_app/agent/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/agent/manager.py @@ -39,6 +39,7 @@ class AgentConfigManager: "provider_id": tool["provider_id"], "tool_name": tool["tool_name"], "tool_parameters": tool.get("tool_parameters", {}), + "credential_id": tool.get("credential_id", None), } agent_tools.append(AgentToolEntity(**agent_tool_properties)) diff --git a/api/core/helper/code_executor/template_transformer.py b/api/core/helper/code_executor/template_transformer.py index 84f212a9c1..a8e9f41a84 100644 --- a/api/core/helper/code_executor/template_transformer.py +++ b/api/core/helper/code_executor/template_transformer.py @@ -5,6 +5,8 @@ from base64 import b64encode from collections.abc import Mapping from typing import Any +from core.variables.utils import SegmentJSONEncoder + class TemplateTransformer(ABC): _code_placeholder: str = "{{code}}" @@ -95,7 +97,7 @@ class TemplateTransformer(ABC): @classmethod def serialize_inputs(cls, inputs: Mapping[str, Any]) -> str: - inputs_json_str = json.dumps(inputs, ensure_ascii=False).encode() + inputs_json_str = json.dumps(inputs, ensure_ascii=False, cls=SegmentJSONEncoder).encode() input_base64_encoded = b64encode(inputs_json_str).decode("utf-8") return input_base64_encoded diff --git a/api/core/helper/marketplace.py b/api/core/helper/marketplace.py index 65bf4fc1db..fe3078923d 100644 --- a/api/core/helper/marketplace.py +++ b/api/core/helper/marketplace.py @@ -25,9 +25,29 @@ def batch_fetch_plugin_manifests(plugin_ids: list[str]) -> Sequence[MarketplaceP url = str(marketplace_api_url / "api/v1/plugins/batch") response = requests.post(url, json={"plugin_ids": plugin_ids}) response.raise_for_status() + return [MarketplacePluginDeclaration(**plugin) for plugin in response.json()["data"]["plugins"]] +def batch_fetch_plugin_manifests_ignore_deserialization_error( + plugin_ids: list[str], +) -> Sequence[MarketplacePluginDeclaration]: + if len(plugin_ids) == 0: + return [] + + url = str(marketplace_api_url / "api/v1/plugins/batch") + response = requests.post(url, json={"plugin_ids": plugin_ids}) + response.raise_for_status() + result: list[MarketplacePluginDeclaration] = [] + for plugin in response.json()["data"]["plugins"]: + try: + result.append(MarketplacePluginDeclaration(**plugin)) + except Exception as e: + pass + + return result + + def record_install_plugin_event(plugin_unique_identifier: str): url = str(marketplace_api_url / "api/v1/stats/plugins/install_count") response = requests.post(url, json={"unique_identifier": plugin_unique_identifier}) diff --git a/api/core/helper/provider_cache.py b/api/core/helper/provider_cache.py new file mode 100644 index 0000000000..48ec3be5c8 --- /dev/null +++ b/api/core/helper/provider_cache.py @@ -0,0 +1,84 @@ +import json +from abc import ABC, abstractmethod +from json import JSONDecodeError +from typing import Any, Optional + +from extensions.ext_redis import redis_client + + +class ProviderCredentialsCache(ABC): + """Base class for provider credentials cache""" + + def __init__(self, **kwargs): + self.cache_key = self._generate_cache_key(**kwargs) + + @abstractmethod + def _generate_cache_key(self, **kwargs) -> str: + """Generate cache key based on subclass implementation""" + pass + + def get(self) -> Optional[dict]: + """Get cached provider credentials""" + cached_credentials = redis_client.get(self.cache_key) + if cached_credentials: + try: + cached_credentials = cached_credentials.decode("utf-8") + return dict(json.loads(cached_credentials)) + except JSONDecodeError: + return None + return None + + def set(self, config: dict[str, Any]) -> None: + """Cache provider credentials""" + redis_client.setex(self.cache_key, 86400, json.dumps(config)) + + def delete(self) -> None: + """Delete cached provider credentials""" + redis_client.delete(self.cache_key) + + +class SingletonProviderCredentialsCache(ProviderCredentialsCache): + """Cache for tool single provider credentials""" + + def __init__(self, tenant_id: str, provider_type: str, provider_identity: str): + super().__init__( + tenant_id=tenant_id, + provider_type=provider_type, + provider_identity=provider_identity, + ) + + def _generate_cache_key(self, **kwargs) -> str: + tenant_id = kwargs["tenant_id"] + provider_type = kwargs["provider_type"] + identity_name = kwargs["provider_identity"] + identity_id = f"{provider_type}.{identity_name}" + return f"{provider_type}_credentials:tenant_id:{tenant_id}:id:{identity_id}" + + +class ToolProviderCredentialsCache(ProviderCredentialsCache): + """Cache for tool provider credentials""" + + def __init__(self, tenant_id: str, provider: str, credential_id: str): + super().__init__(tenant_id=tenant_id, provider=provider, credential_id=credential_id) + + def _generate_cache_key(self, **kwargs) -> str: + tenant_id = kwargs["tenant_id"] + provider = kwargs["provider"] + credential_id = kwargs["credential_id"] + return f"tool_credentials:tenant_id:{tenant_id}:provider:{provider}:credential_id:{credential_id}" + + +class NoOpProviderCredentialCache: + """No-op provider credential cache""" + + def get(self) -> Optional[dict]: + """Get cached provider credentials""" + return None + + def set(self, config: dict[str, Any]) -> None: + """Cache provider credentials""" + pass + + def delete(self) -> None: + """Delete cached provider credentials""" + pass diff --git a/api/core/helper/tool_provider_cache.py b/api/core/helper/tool_provider_cache.py deleted file mode 100644 index 2e4a04c579..0000000000 --- a/api/core/helper/tool_provider_cache.py +++ /dev/null @@ -1,51 +0,0 @@ -import json -from enum import Enum -from json import JSONDecodeError -from typing import Optional - -from extensions.ext_redis import redis_client - - -class ToolProviderCredentialsCacheType(Enum): - PROVIDER = "tool_provider" - ENDPOINT = "endpoint" - - -class ToolProviderCredentialsCache: - def __init__(self, tenant_id: str, identity_id: str, cache_type: ToolProviderCredentialsCacheType): - self.cache_key = f"{cache_type.value}_credentials:tenant_id:{tenant_id}:id:{identity_id}" - - def get(self) -> Optional[dict]: - """ - Get cached model provider credentials. - - :return: - """ - cached_provider_credentials = redis_client.get(self.cache_key) - if cached_provider_credentials: - try: - cached_provider_credentials = cached_provider_credentials.decode("utf-8") - cached_provider_credentials = json.loads(cached_provider_credentials) - except JSONDecodeError: - return None - - return dict(cached_provider_credentials) - else: - return None - - def set(self, credentials: dict) -> None: - """ - Cache model provider credentials. - - :param credentials: provider credentials - :return: - """ - redis_client.setex(self.cache_key, 86400, json.dumps(credentials)) - - def delete(self) -> None: - """ - Delete cached model provider credentials. - - :return: - """ - redis_client.delete(self.cache_key) diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index e01896a491..1e73500cef 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -11,6 +11,7 @@ from core.llm_generator.prompts import ( CONVERSATION_TITLE_PROMPT, GENERATOR_QA_PROMPT, JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE, + PROMPT_OPTIMIZATION_METAPROMPT_SYSTEM, PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE, SYSTEM_STRUCTURED_OUTPUT_GENERATE, WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE, @@ -394,3 +395,55 @@ class LLMGenerator: except Exception as e: logging.exception(f"Failed to invoke LLM model, model: {model_config.get('name')}") return {"output": "", "error": f"An unexpected error occurred: {str(e)}"} + + @staticmethod + def generate_prompt_optimization( + tenant_id: str, + message: str, + last_run: dict, + current: str, + model_config: dict, + ) -> dict: + prompt_messages = [ + SystemPromptMessage(content=PROMPT_OPTIMIZATION_METAPROMPT_SYSTEM), + UserPromptMessage(content=json.dumps({ + "current": current, + "last_run": last_run, + "message": message, + })) + ] + model_instance = ModelManager().get_model_instance( + tenant_id=tenant_id, + model_type=ModelType.LLM, + provider=model_config.get("provider", ""), + model=model_config.get("name", ""), + ) + model_parameters = model_config.get("model_parameters", {}) + try: + response = cast( + LLMResult, + model_instance.invoke_llm( + prompt_messages=list(prompt_messages), + model_parameters=model_parameters, + stream=False + ), + ) + raw_content = response.message.content + if not isinstance(raw_content, str): + raise ValueError(f"LLM response content must be a string, got: {type(raw_content)}") + cleaned_content = re.sub(r'^[^{]*({.*})[^}]*$', r'\1', raw_content, flags=re.DOTALL) + result = json.loads(cleaned_content) + return { + "message": result.get("message", ""), + "modified": result.get("modified", ""), + } + except InvokeError as e: + error = str(e) + return { + "error": f"Failed to invoke LLM model for prompt optimization. Error: {error}", + } + except Exception as e: + logging.exception(f"Failed to invoke LLM model for prompt optimization, model: {model_config.get('name')}") + return { + "error": f"An unexpected error occurred: {str(e)}", + } diff --git a/api/core/llm_generator/prompts.py b/api/core/llm_generator/prompts.py index ef81e38dc5..eb59ccabcb 100644 --- a/api/core/llm_generator/prompts.py +++ b/api/core/llm_generator/prompts.py @@ -309,3 +309,56 @@ eg: Here is the JSON schema: {{schema}} """ # noqa: E501 + +PROMPT_OPTIMIZATION_METAPROMPT_SYSTEM = """ +Both your input and output should be in JSON format. + +! Below is the schema for input content ! +{ + "type": "object", + "description": "The user is trying to process some content with a prompt, but the output is not as expected. They hope to achieve their goal by modifying the prompt.", + "properties": { + "current": { + "type": "string", + "description": "The prompt before modification, where placeholders {{}} will be replaced with actual values for the large language model." + }, + "last_run": { + "type": "object", + "description": "Last running process from the large language model after receiving the prompt.", + }, + "message": { + "type": "string", + "description": "User's feedback on the current prompt, input, and output." + } + }, + "required": [ + "current", + "last_run", + "message" + ] +} +! Above is the schema for input content ! + +! Below is the schema for output content ! +{ + "type": "object", + "description": "Your feedback to the user after they provide modification suggestions.", + "properties": { + "modified": { + "type": "string", + "description": "Your modified prompt. You should change the original prompt as little as possible to achieve the goal." + }, + "message": { + "type": "string", + "description": "Your feedback to the user, explaining what you did and your thought process in text, providing sufficient emotional value to the user." + } + }, + "required": [ + "modified", + "message" + ] +} +! Above is the schema for output content ! + +Your output must strictly follow the schema format, do not output any content outside of the JSON body. +""" # noqa: E501 diff --git a/api/core/plugin/backwards_invocation/encrypt.py b/api/core/plugin/backwards_invocation/encrypt.py index 81a5d033a0..213f5c726a 100644 --- a/api/core/plugin/backwards_invocation/encrypt.py +++ b/api/core/plugin/backwards_invocation/encrypt.py @@ -1,16 +1,20 @@ +from core.helper.provider_cache import SingletonProviderCredentialsCache from core.plugin.entities.request import RequestInvokeEncrypt -from core.tools.utils.configuration import ProviderConfigEncrypter +from core.tools.utils.encryption import create_provider_encrypter from models.account import Tenant class PluginEncrypter: @classmethod def invoke_encrypt(cls, tenant: Tenant, payload: RequestInvokeEncrypt) -> dict: - encrypter = ProviderConfigEncrypter( + encrypter, cache = create_provider_encrypter( tenant_id=tenant.id, config=payload.config, - provider_type=payload.namespace, - provider_identity=payload.identity, + cache=SingletonProviderCredentialsCache( + tenant_id=tenant.id, + provider_type=payload.namespace, + provider_identity=payload.identity, + ), ) if payload.opt == "encrypt": @@ -22,7 +26,7 @@ class PluginEncrypter: "data": encrypter.decrypt(payload.data), } elif payload.opt == "clear": - encrypter.delete_tool_credentials_cache() + cache.delete() return { "data": {}, } diff --git a/api/core/plugin/backwards_invocation/tool.py b/api/core/plugin/backwards_invocation/tool.py index 1d62743f13..06773504d9 100644 --- a/api/core/plugin/backwards_invocation/tool.py +++ b/api/core/plugin/backwards_invocation/tool.py @@ -1,5 +1,5 @@ from collections.abc import Generator -from typing import Any +from typing import Any, Optional from core.callback_handler.workflow_tool_callback_handler import DifyWorkflowCallbackHandler from core.plugin.backwards_invocation.base import BaseBackwardsInvocation @@ -23,6 +23,7 @@ class PluginToolBackwardsInvocation(BaseBackwardsInvocation): provider: str, tool_name: str, tool_parameters: dict[str, Any], + credential_id: Optional[str] = None, ) -> Generator[ToolInvokeMessage, None, None]: """ invoke tool @@ -30,7 +31,7 @@ class PluginToolBackwardsInvocation(BaseBackwardsInvocation): # get tool runtime try: tool_runtime = ToolManager.get_tool_runtime_from_plugin( - tool_type, tenant_id, provider, tool_name, tool_parameters + tool_type, tenant_id, provider, tool_name, tool_parameters, credential_id ) response = ToolEngine.generic_invoke( tool_runtime, tool_parameters, user_id, DifyWorkflowCallbackHandler(), workflow_call_depth=1 diff --git a/api/core/plugin/entities/request.py b/api/core/plugin/entities/request.py index 89f595ec46..3a783dad3e 100644 --- a/api/core/plugin/entities/request.py +++ b/api/core/plugin/entities/request.py @@ -27,6 +27,20 @@ from core.workflow.nodes.question_classifier.entities import ( ) +class InvokeCredentials(BaseModel): + tool_credentials: dict[str, str] = Field( + default_factory=dict, + description="Map of tool provider to credential id, used to store the credential id for the tool provider.", + ) + + +class PluginInvokeContext(BaseModel): + credentials: Optional[InvokeCredentials] = Field( + default_factory=InvokeCredentials, + description="Credentials context for the plugin invocation or backward invocation.", + ) + + class RequestInvokeTool(BaseModel): """ Request to invoke a tool @@ -36,6 +50,7 @@ class RequestInvokeTool(BaseModel): provider: str tool: str tool_parameters: dict + credential_id: Optional[str] = None class BaseRequestInvokeModel(BaseModel): diff --git a/api/core/plugin/impl/agent.py b/api/core/plugin/impl/agent.py index 66b77c7489..9575c57ac8 100644 --- a/api/core/plugin/impl/agent.py +++ b/api/core/plugin/impl/agent.py @@ -6,6 +6,7 @@ from core.plugin.entities.plugin import GenericProviderID from core.plugin.entities.plugin_daemon import ( PluginAgentProviderEntity, ) +from core.plugin.entities.request import PluginInvokeContext from core.plugin.impl.base import BasePluginClient @@ -83,6 +84,7 @@ class PluginAgentClient(BasePluginClient): conversation_id: Optional[str] = None, app_id: Optional[str] = None, message_id: Optional[str] = None, + context: Optional[PluginInvokeContext] = None, ) -> Generator[AgentInvokeMessage, None, None]: """ Invoke the agent with the given tenant, user, plugin, provider, name and parameters. @@ -99,6 +101,7 @@ class PluginAgentClient(BasePluginClient): "conversation_id": conversation_id, "app_id": app_id, "message_id": message_id, + "context": context.model_dump() if context else {}, "data": { "agent_strategy_provider": agent_provider_id.provider_name, "agent_strategy": agent_strategy, diff --git a/api/core/plugin/impl/oauth.py b/api/core/plugin/impl/oauth.py index b006bf1d4b..4338c9cf1f 100644 --- a/api/core/plugin/impl/oauth.py +++ b/api/core/plugin/impl/oauth.py @@ -15,6 +15,7 @@ class OAuthHandler(BasePluginClient): user_id: str, plugin_id: str, provider: str, + redirect_uri: str, system_credentials: Mapping[str, Any], ) -> PluginOAuthAuthorizationUrlResponse: response = self._request_with_plugin_daemon_response_stream( @@ -25,6 +26,7 @@ class OAuthHandler(BasePluginClient): "user_id": user_id, "data": { "provider": provider, + "redirect_uri": redirect_uri, "system_credentials": system_credentials, }, }, @@ -43,6 +45,7 @@ class OAuthHandler(BasePluginClient): user_id: str, plugin_id: str, provider: str, + redirect_uri: str, system_credentials: Mapping[str, Any], request: Request, ) -> PluginOAuthCredentialsResponse: @@ -61,6 +64,7 @@ class OAuthHandler(BasePluginClient): "user_id": user_id, "data": { "provider": provider, + "redirect_uri": redirect_uri, "system_credentials": system_credentials, # for json serialization "raw_http_request": binascii.hexlify(raw_request_bytes).decode(), diff --git a/api/core/plugin/impl/tool.py b/api/core/plugin/impl/tool.py index 19b26c8fe3..04225f95ee 100644 --- a/api/core/plugin/impl/tool.py +++ b/api/core/plugin/impl/tool.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from core.plugin.entities.plugin import GenericProviderID, ToolProviderID from core.plugin.entities.plugin_daemon import PluginBasicBooleanResponse, PluginToolProviderEntity from core.plugin.impl.base import BasePluginClient -from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter +from core.tools.entities.tool_entities import CredentialType, ToolInvokeMessage, ToolParameter class PluginToolManager(BasePluginClient): @@ -78,6 +78,7 @@ class PluginToolManager(BasePluginClient): tool_provider: str, tool_name: str, credentials: dict[str, Any], + credential_type: CredentialType, tool_parameters: dict[str, Any], conversation_id: Optional[str] = None, app_id: Optional[str] = None, @@ -102,6 +103,7 @@ class PluginToolManager(BasePluginClient): "provider": tool_provider_id.provider_name, "tool": tool_name, "credentials": credentials, + "credential_type": credential_type, "tool_parameters": tool_parameters, }, }, diff --git a/api/core/tools/__base/tool_runtime.py b/api/core/tools/__base/tool_runtime.py index c9e157cb77..1068b07062 100644 --- a/api/core/tools/__base/tool_runtime.py +++ b/api/core/tools/__base/tool_runtime.py @@ -4,7 +4,7 @@ from openai import BaseModel from pydantic import Field from core.app.entities.app_invoke_entities import InvokeFrom -from core.tools.entities.tool_entities import ToolInvokeFrom +from core.tools.entities.tool_entities import CredentialType, ToolInvokeFrom class ToolRuntime(BaseModel): @@ -17,6 +17,7 @@ class ToolRuntime(BaseModel): invoke_from: Optional[InvokeFrom] = None tool_invoke_from: Optional[ToolInvokeFrom] = None credentials: dict[str, Any] = Field(default_factory=dict) + credential_type: Optional[CredentialType] = CredentialType.API_KEY runtime_parameters: dict[str, Any] = Field(default_factory=dict) diff --git a/api/core/tools/builtin_tool/provider.py b/api/core/tools/builtin_tool/provider.py index cf75bd3d7e..39de030f18 100644 --- a/api/core/tools/builtin_tool/provider.py +++ b/api/core/tools/builtin_tool/provider.py @@ -7,7 +7,13 @@ from core.helper.module_import_helper import load_single_subclass_from_source from core.tools.__base.tool_provider import ToolProviderController from core.tools.__base.tool_runtime import ToolRuntime from core.tools.builtin_tool.tool import BuiltinTool -from core.tools.entities.tool_entities import ToolEntity, ToolProviderEntity, ToolProviderType +from core.tools.entities.tool_entities import ( + CredentialType, + OAuthSchema, + ToolEntity, + ToolProviderEntity, + ToolProviderType, +) from core.tools.entities.values import ToolLabelEnum, default_tool_label_dict from core.tools.errors import ( ToolProviderNotFoundError, @@ -39,10 +45,18 @@ class BuiltinToolProviderController(ToolProviderController): credential_dict = provider_yaml.get("credentials_for_provider", {}).get(credential, {}) credentials_schema.append(credential_dict) + oauth_schema = None + if provider_yaml.get("oauth_schema", None) is not None: + oauth_schema = OAuthSchema( + client_schema=provider_yaml.get("oauth_schema", {}).get("client_schema", []), + credentials_schema=provider_yaml.get("oauth_schema", {}).get("credentials_schema", []), + ) + super().__init__( entity=ToolProviderEntity( identity=provider_yaml["identity"], credentials_schema=credentials_schema, + oauth_schema=oauth_schema, ), ) @@ -97,10 +111,39 @@ class BuiltinToolProviderController(ToolProviderController): :return: the credentials schema """ - if not self.entity.credentials_schema: - return [] + return self.get_credentials_schema_by_type(CredentialType.API_KEY.value) + + def get_credentials_schema_by_type(self, credential_type: str) -> list[ProviderConfig]: + """ + returns the credentials schema of the provider + + :param credential_type: the type of the credential + :return: the credentials schema of the provider + """ + if credential_type == CredentialType.OAUTH2.value: + return self.entity.oauth_schema.credentials_schema.copy() if self.entity.oauth_schema else [] + if credential_type == CredentialType.API_KEY.value: + return self.entity.credentials_schema.copy() if self.entity.credentials_schema else [] + raise ValueError(f"Invalid credential type: {credential_type}") + + def get_oauth_client_schema(self) -> list[ProviderConfig]: + """ + returns the oauth client schema of the provider - return self.entity.credentials_schema.copy() + :return: the oauth client schema + """ + return self.entity.oauth_schema.client_schema.copy() if self.entity.oauth_schema else [] + + def get_supported_credential_types(self) -> list[str]: + """ + returns the credential support type of the provider + """ + types = [] + if self.entity.credentials_schema is not None and len(self.entity.credentials_schema) > 0: + types.append(CredentialType.API_KEY.value) + if self.entity.oauth_schema is not None and len(self.entity.oauth_schema.credentials_schema) > 0: + types.append(CredentialType.OAUTH2.value) + return types def get_tools(self) -> list[BuiltinTool]: """ @@ -123,7 +166,11 @@ class BuiltinToolProviderController(ToolProviderController): :return: whether the provider needs credentials """ - return self.entity.credentials_schema is not None and len(self.entity.credentials_schema) != 0 + return ( + self.entity.credentials_schema is not None + and len(self.entity.credentials_schema) != 0 + or (self.entity.oauth_schema is not None and len(self.entity.oauth_schema.credentials_schema) != 0) + ) @property def provider_type(self) -> ToolProviderType: diff --git a/api/core/tools/entities/api_entities.py b/api/core/tools/entities/api_entities.py index 90134ba71d..27ce96b90e 100644 --- a/api/core/tools/entities/api_entities.py +++ b/api/core/tools/entities/api_entities.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field, field_validator from core.model_runtime.utils.encoders import jsonable_encoder from core.tools.__base.tool import ToolParameter from core.tools.entities.common_entities import I18nObject -from core.tools.entities.tool_entities import ToolProviderType +from core.tools.entities.tool_entities import CredentialType, ToolProviderType class ToolApiEntity(BaseModel): @@ -87,3 +87,22 @@ class ToolProviderApiEntity(BaseModel): def optional_field(self, key: str, value: Any) -> dict: """Return dict with key-value if value is truthy, empty dict otherwise.""" return {key: value} if value else {} + + +class ToolProviderCredentialApiEntity(BaseModel): + id: str = Field(description="The unique id of the credential") + name: str = Field(description="The name of the credential") + provider: str = Field(description="The provider of the credential") + credential_type: CredentialType = Field(description="The type of the credential") + is_default: bool = Field( + default=False, description="Whether the credential is the default credential for the provider in the workspace" + ) + credentials: dict = Field(description="The credentials of the provider") + + +class ToolProviderCredentialInfoApiEntity(BaseModel): + supported_credential_types: list[str] = Field(description="The supported credential types of the provider") + is_oauth_custom_client_enabled: bool = Field( + default=False, description="Whether the OAuth custom client is enabled for the provider" + ) + credentials: list[ToolProviderCredentialApiEntity] = Field(description="The credentials of the provider") diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index b5148e245f..08fd632ba5 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -355,10 +355,18 @@ class ToolEntity(BaseModel): return v or [] +class OAuthSchema(BaseModel): + client_schema: list[ProviderConfig] = Field(default_factory=list, description="The schema of the OAuth client") + credentials_schema: list[ProviderConfig] = Field( + default_factory=list, description="The schema of the OAuth credentials" + ) + + class ToolProviderEntity(BaseModel): identity: ToolProviderIdentity plugin_id: Optional[str] = None credentials_schema: list[ProviderConfig] = Field(default_factory=list) + oauth_schema: Optional[OAuthSchema] = None class ToolProviderEntityWithPlugin(ToolProviderEntity): @@ -438,6 +446,7 @@ class ToolSelector(BaseModel): options: Optional[list[PluginParameterOption]] = None provider_id: str = Field(..., description="The id of the provider") + credential_id: Optional[str] = Field(default=None, description="The id of the credential") tool_name: str = Field(..., description="The name of the tool") tool_description: str = Field(..., description="The description of the tool") tool_configuration: Mapping[str, Any] = Field(..., description="Configuration, type form") @@ -445,3 +454,36 @@ class ToolSelector(BaseModel): def to_plugin_parameter(self) -> dict[str, Any]: return self.model_dump() + + +class CredentialType(enum.StrEnum): + API_KEY = "api-key" + OAUTH2 = "oauth2" + + def get_name(self): + if self == CredentialType.API_KEY: + return "API KEY" + elif self == CredentialType.OAUTH2: + return "AUTH" + else: + return self.value.replace("-", " ").upper() + + def is_editable(self): + return self == CredentialType.API_KEY + + def is_validate_allowed(self): + return self == CredentialType.API_KEY + + @classmethod + def values(cls): + return [item.value for item in cls] + + @classmethod + def of(cls, credential_type: str) -> "CredentialType": + type_name = credential_type.lower() + if type_name == "api-key": + return cls.API_KEY + elif type_name == "oauth2": + return cls.OAUTH2 + else: + raise ValueError(f"Invalid credential type: {credential_type}") diff --git a/api/core/tools/plugin_tool/tool.py b/api/core/tools/plugin_tool/tool.py index d21e3d7d1c..aef2677c36 100644 --- a/api/core/tools/plugin_tool/tool.py +++ b/api/core/tools/plugin_tool/tool.py @@ -44,6 +44,7 @@ class PluginTool(Tool): tool_provider=self.entity.identity.provider, tool_name=self.entity.identity.name, credentials=self.runtime.credentials, + credential_type=self.runtime.credential_type, tool_parameters=tool_parameters, conversation_id=conversation_id, app_id=app_id, diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 22a9853b41..d38b80372b 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast from yarl import URL import contexts +from core.helper.provider_cache import ToolProviderCredentialsCache from core.plugin.entities.plugin import ToolProviderID from core.plugin.impl.tool import PluginToolManager from core.tools.__base.tool_provider import ToolProviderController @@ -24,7 +25,6 @@ from services.tools.mcp_tools_mange_service import MCPToolManageService if TYPE_CHECKING: from core.workflow.nodes.tool.entities import ToolEntity - from configs import dify_config from core.agent.entities import AgentToolEntity from core.app.entities.app_invoke_entities import InvokeFrom @@ -41,16 +41,17 @@ from core.tools.entities.api_entities import ToolProviderApiEntity, ToolProvider from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ( ApiProviderAuthType, + CredentialType, ToolInvokeFrom, 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 ( - ProviderConfigEncrypter, ToolParameterConfigurationManager, ) +from core.tools.utils.encryption import create_provider_encrypter, create_tool_provider_encrypter from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider @@ -68,8 +69,11 @@ class ToolManager: @classmethod def get_hardcoded_provider(cls, provider: str) -> BuiltinToolProviderController: """ + get the hardcoded provider + """ + if len(cls._hardcoded_providers) == 0: # init the builtin providers cls.load_hardcoded_providers_cache() @@ -113,7 +117,12 @@ class ToolManager: contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(Lock()) + plugin_tool_providers = contexts.plugin_tool_providers.get() + if provider in plugin_tool_providers: + return plugin_tool_providers[provider] + with contexts.plugin_tool_providers_lock.get(): + # double check plugin_tool_providers = contexts.plugin_tool_providers.get() if provider in plugin_tool_providers: return plugin_tool_providers[provider] @@ -131,25 +140,7 @@ class ToolManager: ) plugin_tool_providers[provider] = controller - - return controller - - @classmethod - def get_builtin_tool(cls, provider: str, tool_name: str, tenant_id: str) -> BuiltinTool | PluginTool | None: - """ - get the builtin tool - - :param provider: the name of the provider - :param tool_name: the name of the tool - :param tenant_id: the id of the tenant - :return: the provider, the tool - """ - provider_controller = cls.get_builtin_provider(provider, tenant_id) - tool = provider_controller.get_tool(tool_name) - if tool is None: - raise ToolNotFoundError(f"tool {tool_name} not found") - - return tool + return controller @classmethod def get_tool_runtime( @@ -160,6 +151,7 @@ class ToolManager: tenant_id: str, invoke_from: InvokeFrom = InvokeFrom.DEBUGGER, tool_invoke_from: ToolInvokeFrom = ToolInvokeFrom.AGENT, + credential_id: Optional[str] = None, ) -> Union[BuiltinTool, PluginTool, ApiTool, WorkflowTool, MCPTool]: """ get the tool runtime @@ -170,6 +162,7 @@ class ToolManager: :param tenant_id: the tenant id :param invoke_from: invoke from :param tool_invoke_from: the tool invoke from + :param credential_id: the credential id :return: the tool """ @@ -197,45 +190,59 @@ class ToolManager: if isinstance(provider_controller, PluginToolProviderController): provider_id_entity = ToolProviderID(provider_id) # get credentials - builtin_provider: BuiltinToolProvider | None = ( - db.session.query(BuiltinToolProvider) - .filter( - BuiltinToolProvider.tenant_id == tenant_id, - (BuiltinToolProvider.provider == str(provider_id_entity)) - | (BuiltinToolProvider.provider == provider_id_entity.provider_name), + if credential_id: + builtin_provider = ( + db.session.query(BuiltinToolProvider) + .filter( + BuiltinToolProvider.tenant_id == tenant_id, + BuiltinToolProvider.id == credential_id, + ) + .first() + ) + if builtin_provider is None: + raise ToolProviderNotFoundError(f"builtin provider {credential_id} not found") + else: + builtin_provider = ( + db.session.query(BuiltinToolProvider) + .filter( + BuiltinToolProvider.tenant_id == tenant_id, + (BuiltinToolProvider.provider == str(provider_id_entity)) + | (BuiltinToolProvider.provider == provider_id_entity.provider_name), + ) + .order_by(BuiltinToolProvider.is_default.desc(), BuiltinToolProvider.created_at.asc()) + .first() ) - .first() - ) - if builtin_provider is None: - raise ToolProviderNotFoundError(f"builtin provider {provider_id} not found") + if builtin_provider is None: + raise ToolProviderNotFoundError(f"builtin provider {provider_id} not found") else: builtin_provider = ( db.session.query(BuiltinToolProvider) .filter(BuiltinToolProvider.tenant_id == tenant_id, (BuiltinToolProvider.provider == provider_id)) + .order_by(BuiltinToolProvider.is_default.desc(), BuiltinToolProvider.created_at.asc()) .first() ) if builtin_provider is None: raise ToolProviderNotFoundError(f"builtin provider {provider_id} not found") - # decrypt the credentials - credentials = builtin_provider.credentials - tool_configuration = ProviderConfigEncrypter( + encrypter, _ = create_provider_encrypter( tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + config=[ + x.to_basic_provider_config() + for x in provider_controller.get_credentials_schema_by_type(builtin_provider.credential_type) + ], + cache=ToolProviderCredentialsCache( + tenant_id=tenant_id, provider=provider_id, credential_id=builtin_provider.id + ), ) - - decrypted_credentials = tool_configuration.decrypt(credentials) - return cast( BuiltinTool, builtin_tool.fork_tool_runtime( runtime=ToolRuntime( tenant_id=tenant_id, - credentials=decrypted_credentials, + credentials=encrypter.decrypt(builtin_provider.credentials), + credential_type=CredentialType.of(builtin_provider.credential_type), runtime_parameters={}, invoke_from=invoke_from, tool_invoke_from=tool_invoke_from, @@ -245,22 +252,16 @@ class ToolManager: elif provider_type == ToolProviderType.API: api_provider, credentials = cls.get_api_provider_controller(tenant_id, provider_id) - - # decrypt the credentials - tool_configuration = ProviderConfigEncrypter( + encrypter, _ = create_tool_provider_encrypter( tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in api_provider.get_credentials_schema()], - provider_type=api_provider.provider_type.value, - provider_identity=api_provider.entity.identity.name, + controller=api_provider, ) - decrypted_credentials = tool_configuration.decrypt(credentials) - return cast( ApiTool, api_provider.get_tool(tool_name).fork_tool_runtime( runtime=ToolRuntime( tenant_id=tenant_id, - credentials=decrypted_credentials, + credentials=encrypter.decrypt(credentials), invoke_from=invoke_from, tool_invoke_from=tool_invoke_from, ) @@ -320,6 +321,7 @@ class ToolManager: tenant_id=tenant_id, invoke_from=invoke_from, tool_invoke_from=ToolInvokeFrom.AGENT, + credential_id=agent_tool.credential_id, ) runtime_parameters = {} parameters = tool_entity.get_merged_runtime_parameters() @@ -362,6 +364,7 @@ class ToolManager: tenant_id=tenant_id, invoke_from=invoke_from, tool_invoke_from=ToolInvokeFrom.WORKFLOW, + credential_id=workflow_tool.credential_id, ) parameters = tool_runtime.get_merged_runtime_parameters() @@ -391,6 +394,7 @@ class ToolManager: provider: str, tool_name: str, tool_parameters: dict[str, Any], + credential_id: Optional[str] = None, ) -> Tool: """ get tool runtime from plugin @@ -402,6 +406,7 @@ class ToolManager: tenant_id=tenant_id, invoke_from=InvokeFrom.SERVICE_API, tool_invoke_from=ToolInvokeFrom.PLUGIN, + credential_id=credential_id, ) runtime_parameters = {} parameters = tool_entity.get_merged_runtime_parameters() @@ -551,6 +556,22 @@ class ToolManager: return cls._builtin_tools_labels[tool_name] + @classmethod + def list_default_builtin_providers(cls, tenant_id: str) -> list[BuiltinToolProvider]: + """ + list all the builtin providers + """ + # according to multi credentials, select the one with is_default=True first, then created_at oldest + # for compatibility with old version + sql = """ + SELECT DISTINCT ON (tenant_id, provider) id + FROM tool_builtin_providers + WHERE tenant_id = :tenant_id + ORDER BY tenant_id, provider, is_default DESC, created_at DESC + """ + ids = [row.id for row in db.session.execute(db.text(sql), {"tenant_id": tenant_id}).all()] + return db.session.query(BuiltinToolProvider).filter(BuiltinToolProvider.id.in_(ids)).all() + @classmethod def list_providers_from_api( cls, user_id: str, tenant_id: str, typ: ToolProviderTypeApiLiteral @@ -565,21 +586,13 @@ class ToolManager: with db.session.no_autoflush: if "builtin" in filters: - # get builtin providers builtin_providers = cls.list_builtin_providers(tenant_id) - # get db builtin providers - db_builtin_providers: list[BuiltinToolProvider] = ( - db.session.query(BuiltinToolProvider).filter(BuiltinToolProvider.tenant_id == tenant_id).all() - ) - - # rewrite db_builtin_providers - for db_provider in db_builtin_providers: - tool_provider_id = str(ToolProviderID(db_provider.provider)) - db_provider.provider = tool_provider_id - - def find_db_builtin_provider(provider): - return next((x for x in db_builtin_providers if x.provider == provider), None) + # key: provider name, value: provider + db_builtin_providers = { + str(ToolProviderID(provider.provider)): provider + for provider in cls.list_default_builtin_providers(tenant_id) + } # append builtin providers for provider in builtin_providers: @@ -591,10 +604,9 @@ class ToolManager: name_func=lambda x: x.identity.name, ): continue - user_provider = ToolTransformService.builtin_provider_to_user_provider( provider_controller=provider, - db_provider=find_db_builtin_provider(provider.entity.identity.name), + db_provider=db_builtin_providers.get(provider.entity.identity.name), decrypt_credentials=False, ) @@ -604,7 +616,6 @@ class ToolManager: result_providers[f"builtin_provider.{user_provider.name}"] = user_provider # get db api providers - if "api" in filters: db_api_providers: list[ApiToolProvider] = ( db.session.query(ApiToolProvider).filter(ApiToolProvider.tenant_id == tenant_id).all() @@ -764,15 +775,12 @@ class ToolManager: auth_type, ) # init tool configuration - tool_configuration = ProviderConfigEncrypter( + encrypter, _ = create_tool_provider_encrypter( tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in controller.get_credentials_schema()], - provider_type=controller.provider_type.value, - provider_identity=controller.entity.identity.name, + controller=controller, ) - decrypted_credentials = tool_configuration.decrypt(credentials) - masked_credentials = tool_configuration.mask_tool_credentials(decrypted_credentials) + masked_credentials = encrypter.mask_tool_credentials(encrypter.decrypt(credentials)) try: icon = json.loads(provider_obj.icon) diff --git a/api/core/tools/utils/configuration.py b/api/core/tools/utils/configuration.py index 251fedf56e..aceba6e69f 100644 --- a/api/core/tools/utils/configuration.py +++ b/api/core/tools/utils/configuration.py @@ -1,12 +1,8 @@ from copy import deepcopy from typing import Any -from pydantic import BaseModel - -from core.entities.provider_entities import BasicProviderConfig from core.helper import encrypter from core.helper.tool_parameter_cache import ToolParameterCache, ToolParameterCacheType -from core.helper.tool_provider_cache import ToolProviderCredentialsCache, ToolProviderCredentialsCacheType from core.tools.__base.tool import Tool from core.tools.entities.tool_entities import ( ToolParameter, @@ -14,110 +10,6 @@ from core.tools.entities.tool_entities import ( ) -class ProviderConfigEncrypter(BaseModel): - tenant_id: str - config: list[BasicProviderConfig] - provider_type: str - provider_identity: str - - def _deep_copy(self, data: dict[str, str]) -> dict[str, str]: - """ - deep copy data - """ - return deepcopy(data) - - def encrypt(self, data: dict[str, str]) -> dict[str, str]: - """ - encrypt tool credentials with tenant id - - return a deep copy of credentials with encrypted values - """ - data = self._deep_copy(data) - - # get fields need to be decrypted - fields = dict[str, BasicProviderConfig]() - for credential in self.config: - fields[credential.name] = credential - - for field_name, field in fields.items(): - if field.type == BasicProviderConfig.Type.SECRET_INPUT: - if field_name in data: - encrypted = encrypter.encrypt_token(self.tenant_id, data[field_name] or "") - data[field_name] = encrypted - - return data - - def mask_tool_credentials(self, data: dict[str, Any]) -> dict[str, Any]: - """ - mask tool credentials - - return a deep copy of credentials with masked values - """ - data = self._deep_copy(data) - - # get fields need to be decrypted - fields = dict[str, BasicProviderConfig]() - for credential in self.config: - fields[credential.name] = credential - - for field_name, field in fields.items(): - if field.type == BasicProviderConfig.Type.SECRET_INPUT: - if field_name in data: - if len(data[field_name]) > 6: - data[field_name] = ( - data[field_name][:2] + "*" * (len(data[field_name]) - 4) + data[field_name][-2:] - ) - else: - data[field_name] = "*" * len(data[field_name]) - - return data - - def decrypt(self, data: dict[str, str], use_cache: bool = True) -> dict[str, str]: - """ - decrypt tool credentials with tenant id - - return a deep copy of credentials with decrypted values - """ - if use_cache: - cache = ToolProviderCredentialsCache( - tenant_id=self.tenant_id, - identity_id=f"{self.provider_type}.{self.provider_identity}", - cache_type=ToolProviderCredentialsCacheType.PROVIDER, - ) - cached_credentials = cache.get() - if cached_credentials: - return cached_credentials - data = self._deep_copy(data) - # get fields need to be decrypted - fields = dict[str, BasicProviderConfig]() - for credential in self.config: - fields[credential.name] = credential - - for field_name, field in fields.items(): - if field.type == BasicProviderConfig.Type.SECRET_INPUT: - if field_name in data: - try: - # if the value is None or empty string, skip decrypt - if not data[field_name]: - continue - - data[field_name] = encrypter.decrypt_token(self.tenant_id, data[field_name]) - except Exception: - pass - - if use_cache: - cache.set(data) - return data - - def delete_tool_credentials_cache(self): - cache = ToolProviderCredentialsCache( - tenant_id=self.tenant_id, - identity_id=f"{self.provider_type}.{self.provider_identity}", - cache_type=ToolProviderCredentialsCacheType.PROVIDER, - ) - cache.delete() - - class ToolParameterConfigurationManager: """ Tool parameter configuration manager diff --git a/api/core/tools/utils/encryption.py b/api/core/tools/utils/encryption.py new file mode 100644 index 0000000000..4aa5412a5e --- /dev/null +++ b/api/core/tools/utils/encryption.py @@ -0,0 +1,141 @@ +from copy import deepcopy +from typing import Any, Optional, Protocol + +from core.entities.provider_entities import BasicProviderConfig +from core.helper import encrypter +from core.helper.provider_cache import SingletonProviderCredentialsCache +from core.tools.__base.tool_provider import ToolProviderController + + +class ProviderConfigCache(Protocol): + """ + Interface for provider configuration cache operations + """ + + def get(self) -> Optional[dict]: + """Get cached provider configuration""" + ... + + def set(self, config: dict[str, Any]) -> None: + """Cache provider configuration""" + ... + + def delete(self) -> None: + """Delete cached provider configuration""" + ... + + +class ProviderConfigEncrypter: + tenant_id: str + config: list[BasicProviderConfig] + provider_config_cache: ProviderConfigCache + + def __init__( + self, + tenant_id: str, + config: list[BasicProviderConfig], + provider_config_cache: ProviderConfigCache, + ): + self.tenant_id = tenant_id + self.config = config + self.provider_config_cache = provider_config_cache + + def _deep_copy(self, data: dict[str, str]) -> dict[str, str]: + """ + deep copy data + """ + return deepcopy(data) + + def encrypt(self, data: dict[str, str]) -> dict[str, str]: + """ + encrypt tool credentials with tenant id + + return a deep copy of credentials with encrypted values + """ + data = self._deep_copy(data) + + # get fields need to be decrypted + fields = dict[str, BasicProviderConfig]() + for credential in self.config: + fields[credential.name] = credential + + for field_name, field in fields.items(): + if field.type == BasicProviderConfig.Type.SECRET_INPUT: + if field_name in data: + encrypted = encrypter.encrypt_token(self.tenant_id, data[field_name] or "") + data[field_name] = encrypted + + return data + + def mask_tool_credentials(self, data: dict[str, Any]) -> dict[str, Any]: + """ + mask tool credentials + + return a deep copy of credentials with masked values + """ + data = self._deep_copy(data) + + # get fields need to be decrypted + fields = dict[str, BasicProviderConfig]() + for credential in self.config: + fields[credential.name] = credential + + for field_name, field in fields.items(): + if field.type == BasicProviderConfig.Type.SECRET_INPUT: + if field_name in data: + if len(data[field_name]) > 6: + data[field_name] = ( + data[field_name][:2] + "*" * (len(data[field_name]) - 4) + data[field_name][-2:] + ) + else: + data[field_name] = "*" * len(data[field_name]) + + return data + + def decrypt(self, data: dict[str, str]) -> dict[str, Any]: + """ + decrypt tool credentials with tenant id + + return a deep copy of credentials with decrypted values + """ + cached_credentials = self.provider_config_cache.get() + if cached_credentials: + return cached_credentials + + data = self._deep_copy(data) + # get fields need to be decrypted + fields = dict[str, BasicProviderConfig]() + for credential in self.config: + fields[credential.name] = credential + + for field_name, field in fields.items(): + if field.type == BasicProviderConfig.Type.SECRET_INPUT: + if field_name in data: + try: + # if the value is None or empty string, skip decrypt + if not data[field_name]: + continue + + data[field_name] = encrypter.decrypt_token(self.tenant_id, data[field_name]) + except Exception: + pass + + self.provider_config_cache.set(data) + return data + + +def create_provider_encrypter(tenant_id: str, config: list[BasicProviderConfig], cache: ProviderConfigCache): + return ProviderConfigEncrypter(tenant_id=tenant_id, config=config, provider_config_cache=cache), cache + +def create_tool_provider_encrypter(tenant_id: str, controller: ToolProviderController): + cache = SingletonProviderCredentialsCache( + tenant_id=tenant_id, + provider_type=controller.provider_type.value, + provider_identity=controller.entity.identity.name, + ) + encrypt = ProviderConfigEncrypter( + tenant_id=tenant_id, + config=[x.to_basic_provider_config() for x in controller.get_credentials_schema()], + provider_config_cache=cache, + ) + return encrypt, cache diff --git a/api/core/tools/utils/system_oauth_encryption.py b/api/core/tools/utils/system_oauth_encryption.py new file mode 100644 index 0000000000..f32b950619 --- /dev/null +++ b/api/core/tools/utils/system_oauth_encryption.py @@ -0,0 +1,192 @@ +import base64 +import hashlib +import json +import logging +from typing import Any, Optional + +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad, unpad + +from configs import dify_config + +logger = logging.getLogger(__name__) + + +class OAuthEncryptionError(Exception): + """OAuth encryption/decryption specific error""" + + pass + + +class SystemOAuthEncrypter: + """ + A simple OAuth parameters encrypter using AES-CBC encryption. + + This class provides methods to encrypt and decrypt OAuth parameters + using AES-CBC mode with a key derived from the application's SECRET_KEY. + """ + + def __init__(self, secret_key: Optional[str] = None): + """ + Initialize the OAuth encrypter. + + Args: + secret_key: Optional secret key. If not provided, uses dify_config.SECRET_KEY + + Raises: + ValueError: If SECRET_KEY is not configured or empty + """ + secret_key = secret_key or dify_config.SECRET_KEY or "" + + # Generate a fixed 256-bit key using SHA-256 + self.key = hashlib.sha256(secret_key.encode()).digest() + + def encrypt_oauth_params(self, oauth_params: str) -> str: + """ + Encrypt OAuth parameters. + + Args: + oauth_params: OAuth parameters dictionary, e.g., {"client_id": "xxx", "client_secret": "xxx"} + + Returns: + Base64-encoded encrypted string + + Raises: + OAuthEncryptionError: If encryption fails + ValueError: If oauth_params is invalid + """ + + if not oauth_params: + raise ValueError("oauth_params cannot be empty") + + try: + # Generate random IV (16 bytes) + iv = get_random_bytes(16) + + # Create AES cipher (CBC mode) + cipher = AES.new(self.key, AES.MODE_CBC, iv) + + # Encrypt data + padded_data = pad(oauth_params.encode("utf-8"), AES.block_size) + encrypted_data = cipher.encrypt(padded_data) + + # Combine IV and encrypted data + combined = iv + encrypted_data + + # Return base64 encoded string + return base64.b64encode(combined).decode() + + except Exception as e: + raise OAuthEncryptionError(f"Encryption failed: {str(e)}") from e + + def decrypt_oauth_params(self, encrypted_data: str) -> dict[str, Any]: + """ + Decrypt OAuth parameters. + + Args: + encrypted_data: Base64-encoded encrypted string + + Returns: + Decrypted OAuth parameters dictionary + + Raises: + OAuthEncryptionError: If decryption fails + ValueError: If encrypted_data is invalid + """ + if not isinstance(encrypted_data, str): + raise ValueError("encrypted_data must be a string") + + if not encrypted_data: + raise ValueError("encrypted_data cannot be empty") + + try: + # Base64 decode + combined = base64.b64decode(encrypted_data) + + # Check minimum length (IV + at least one AES block) + if len(combined) < 32: # 16 bytes IV + 16 bytes minimum encrypted data + raise ValueError("Invalid encrypted data format") + + # Separate IV and encrypted data + iv = combined[:16] + encrypted_data_bytes = combined[16:] + + # Create AES cipher + cipher = AES.new(self.key, AES.MODE_CBC, iv) + + # Decrypt data + decrypted_data = cipher.decrypt(encrypted_data_bytes) + unpadded_data = unpad(decrypted_data, AES.block_size) + + # Parse JSON + params_json = unpadded_data.decode("utf-8") + oauth_params = json.loads(params_json) + + if not isinstance(oauth_params, dict): + raise ValueError("Decrypted data is not a valid dictionary") + + return oauth_params + + except (ValueError, TypeError) as e: + raise OAuthEncryptionError(f"Decryption failed: {str(e)}") from e + except Exception as e: + raise OAuthEncryptionError(f"Decryption failed: {str(e)}") from e + + +# Factory function for creating encrypter instances +def create_system_oauth_encrypter(secret_key: Optional[str] = None) -> SystemOAuthEncrypter: + """ + Create an OAuth encrypter instance. + + Args: + secret_key: Optional secret key. If not provided, uses dify_config.SECRET_KEY + + Returns: + SystemOAuthEncrypter instance + """ + return SystemOAuthEncrypter(secret_key=secret_key) + + +# Global encrypter instance (for backward compatibility) +_oauth_encrypter: Optional[SystemOAuthEncrypter] = None + + +def get_system_oauth_encrypter() -> SystemOAuthEncrypter: + """ + Get the global OAuth encrypter instance. + + Returns: + SystemOAuthEncrypter instance + """ + global _oauth_encrypter + if _oauth_encrypter is None: + _oauth_encrypter = SystemOAuthEncrypter() + return _oauth_encrypter + + +# Convenience functions for backward compatibility +def encrypt_system_oauth_params(oauth_params: str) -> str: + """ + Encrypt OAuth parameters using the global encrypter. + + Args: + oauth_params: OAuth parameters dictionary + + Returns: + Base64-encoded encrypted string + """ + return get_system_oauth_encrypter().encrypt_oauth_params(oauth_params) + + +def decrypt_system_oauth_params(encrypted_data: str) -> dict[str, Any]: + """ + Decrypt OAuth parameters using the global encrypter. + + Args: + encrypted_data: Base64-encoded encrypted string + + Returns: + Decrypted OAuth parameters dictionary + """ + return get_system_oauth_encrypter().decrypt_oauth_params(encrypted_data) diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index 678b99d546..ce67197a58 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -4,6 +4,7 @@ from collections.abc import Generator, Mapping, Sequence from typing import Any, Optional, cast from packaging.version import Version +from pydantic import ValidationError from sqlalchemy import select from sqlalchemy.orm import Session @@ -13,10 +14,16 @@ from core.agent.strategy.plugin import PluginAgentStrategy from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance, ModelManager from core.model_runtime.entities.model_entities import AIModelEntity, ModelType +from core.plugin.entities.request import InvokeCredentials from core.plugin.impl.exc import PluginDaemonClientSideError from core.plugin.impl.plugin import PluginInstaller from core.provider_manager import ProviderManager -from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter, ToolProviderType +from core.tools.entities.tool_entities import ( + ToolIdentity, + ToolInvokeMessage, + ToolParameter, + ToolProviderType, +) from core.tools.tool_manager import ToolManager from core.variables.segments import StringSegment from core.workflow.entities.node_entities import NodeRunResult @@ -84,6 +91,7 @@ class AgentNode(ToolNode): for_log=True, strategy=strategy, ) + credentials = self._generate_credentials(parameters=parameters) # get conversation id conversation_id = self.graph_runtime_state.variable_pool.get(["sys", SystemVariableKey.CONVERSATION_ID]) @@ -94,6 +102,7 @@ class AgentNode(ToolNode): user_id=self.user_id, app_id=self.app_id, conversation_id=conversation_id.text if conversation_id else None, + credentials=credentials, ) except Exception as e: yield RunCompletedEvent( @@ -246,6 +255,7 @@ class AgentNode(ToolNode): tool_name=tool.get("tool_name", ""), tool_parameters=parameters, plugin_unique_identifier=tool.get("plugin_unique_identifier", None), + credential_id=tool.get("credential_id", None), ) extra = tool.get("extra", {}) @@ -276,6 +286,7 @@ class AgentNode(ToolNode): { **tool_runtime.entity.model_dump(mode="json"), "runtime_parameters": runtime_parameters, + "credential_id": tool.get("credential_id", None), "provider_type": provider_type.value, } ) @@ -305,6 +316,27 @@ class AgentNode(ToolNode): return result + def _generate_credentials( + self, + parameters: dict[str, Any], + ) -> InvokeCredentials: + """ + Generate credentials based on the given agent parameters. + """ + + credentials = InvokeCredentials() + + # generate credentials for tools selector + credentials.tool_credentials = {} + for tool in parameters.get("tools", []): + if tool.get("credential_id"): + try: + identity = ToolIdentity.model_validate(tool.get("identity", {})) + credentials.tool_credentials[identity.provider] = tool.get("credential_id", None) + except ValidationError: + continue + return credentials + @classmethod def _extract_variable_selector_to_variable_mapping( cls, diff --git a/api/core/workflow/nodes/tool/entities.py b/api/core/workflow/nodes/tool/entities.py index 691f6e0196..88c5160d14 100644 --- a/api/core/workflow/nodes/tool/entities.py +++ b/api/core/workflow/nodes/tool/entities.py @@ -14,6 +14,7 @@ class ToolEntity(BaseModel): tool_name: str tool_label: str # redundancy tool_configurations: dict[str, Any] + credential_id: str | None = None plugin_unique_identifier: str | None = None # redundancy @field_validator("tool_configurations", mode="before") diff --git a/api/docker/entrypoint.sh b/api/docker/entrypoint.sh index 18d4f4885d..59d652c11b 100755 --- a/api/docker/entrypoint.sh +++ b/api/docker/entrypoint.sh @@ -22,7 +22,7 @@ if [[ "${MODE}" == "worker" ]]; then exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION \ --max-tasks-per-child ${MAX_TASK_PRE_CHILD:-50} --loglevel ${LOG_LEVEL:-INFO} \ - -Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion} + -Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion,plugin} elif [[ "${MODE}" == "beat" ]]; then exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO} diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index 6279b1ad36..2c2846ba26 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -64,49 +64,62 @@ def init_app(app: DifyApp) -> Celery: celery_app.set_default() app.extensions["celery"] = celery_app - imports = [ - "schedule.clean_embedding_cache_task", - "schedule.clean_unused_datasets_task", - "schedule.create_tidb_serverless_task", - "schedule.update_tidb_serverless_status_task", - "schedule.clean_messages", - "schedule.mail_clean_document_notify_task", - "schedule.queue_monitor_task", - ] + imports = [] day = dify_config.CELERY_BEAT_SCHEDULER_TIME - beat_schedule = { - "clean_embedding_cache_task": { + + # if you add a new task, please add the switch to CeleryScheduleTasksConfig + beat_schedule = {} + if dify_config.ENABLE_CLEAN_EMBEDDING_CACHE_TASK: + imports.append("schedule.clean_embedding_cache_task") + beat_schedule["clean_embedding_cache_task"] = { "task": "schedule.clean_embedding_cache_task.clean_embedding_cache_task", "schedule": timedelta(days=day), - }, - "clean_unused_datasets_task": { + } + if dify_config.ENABLE_CLEAN_UNUSED_DATASETS_TASK: + imports.append("schedule.clean_unused_datasets_task") + beat_schedule["clean_unused_datasets_task"] = { "task": "schedule.clean_unused_datasets_task.clean_unused_datasets_task", "schedule": timedelta(days=day), - }, - "create_tidb_serverless_task": { + } + if dify_config.ENABLE_CREATE_TIDB_SERVERLESS_TASK: + imports.append("schedule.create_tidb_serverless_task") + beat_schedule["create_tidb_serverless_task"] = { "task": "schedule.create_tidb_serverless_task.create_tidb_serverless_task", "schedule": crontab(minute="0", hour="*"), - }, - "update_tidb_serverless_status_task": { + } + if dify_config.ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK: + imports.append("schedule.update_tidb_serverless_status_task") + beat_schedule["update_tidb_serverless_status_task"] = { "task": "schedule.update_tidb_serverless_status_task.update_tidb_serverless_status_task", "schedule": timedelta(minutes=10), - }, - "clean_messages": { + } + if dify_config.ENABLE_CLEAN_MESSAGES: + imports.append("schedule.clean_messages") + beat_schedule["clean_messages"] = { "task": "schedule.clean_messages.clean_messages", "schedule": timedelta(days=day), - }, - # every Monday - "mail_clean_document_notify_task": { + } + if dify_config.ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK: + imports.append("schedule.mail_clean_document_notify_task") + beat_schedule["mail_clean_document_notify_task"] = { "task": "schedule.mail_clean_document_notify_task.mail_clean_document_notify_task", "schedule": crontab(minute="0", hour="10", day_of_week="1"), - }, - "datasets-queue-monitor": { + } + if dify_config.ENABLE_DATASETS_QUEUE_MONITOR: + imports.append("schedule.queue_monitor_task") + beat_schedule["datasets-queue-monitor"] = { "task": "schedule.queue_monitor_task.queue_monitor_task", "schedule": timedelta( minutes=dify_config.QUEUE_MONITOR_INTERVAL if dify_config.QUEUE_MONITOR_INTERVAL else 30 ), - }, - } + } + if dify_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK: + imports.append("schedule.check_upgradable_plugin_task") + beat_schedule["check_upgradable_plugin_task"] = { + "task": "schedule.check_upgradable_plugin_task.check_upgradable_plugin_task", + "schedule": crontab(minute="*/15"), + } + celery_app.conf.update(beat_schedule=beat_schedule, imports=imports) return celery_app diff --git a/api/fields/workflow_run_fields.py b/api/fields/workflow_run_fields.py index a106728e9c..c23e2f494c 100644 --- a/api/fields/workflow_run_fields.py +++ b/api/fields/workflow_run_fields.py @@ -116,3 +116,9 @@ workflow_run_node_execution_fields = { workflow_run_node_execution_list_fields = { "data": fields.List(fields.Nested(workflow_run_node_execution_fields)), } + +workflow_node_ai_modify_fields = { + "message": fields.String, + "modified": fields.String, + "error": fields.String, +} diff --git a/api/libs/passport.py b/api/libs/passport.py index 8df4f529bc..fe8fc33b5f 100644 --- a/api/libs/passport.py +++ b/api/libs/passport.py @@ -14,9 +14,11 @@ class PassportService: def verify(self, token): try: return jwt.decode(token, self.sk, algorithms=["HS256"]) + except jwt.exceptions.ExpiredSignatureError: + raise Unauthorized("Token has expired.") except jwt.exceptions.InvalidSignatureError: raise Unauthorized("Invalid token signature.") except jwt.exceptions.DecodeError: raise Unauthorized("Invalid token.") - except jwt.exceptions.ExpiredSignatureError: - raise Unauthorized("Token has expired.") + except jwt.exceptions.PyJWTError: # Catch-all for other JWT errors + raise Unauthorized("Invalid token.") diff --git a/api/migrations/versions/2025_05_15_1635-16081485540c_.py b/api/migrations/versions/2025_05_15_1635-16081485540c_.py new file mode 100644 index 0000000000..f55730bfb2 --- /dev/null +++ b/api/migrations/versions/2025_05_15_1635-16081485540c_.py @@ -0,0 +1,41 @@ +"""empty message + +Revision ID: 16081485540c +Revises: d28f2004b072 +Create Date: 2025-05-15 16:35:39.113777 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '16081485540c' +down_revision = '2adcbe1f5dfb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tenant_plugin_auto_upgrade_strategies', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('strategy_setting', sa.String(length=16), server_default='fix_only', nullable=False), + sa.Column('upgrade_time_of_day', sa.Integer(), nullable=False), + sa.Column('upgrade_mode', sa.String(length=16), server_default='exclude', nullable=False), + sa.Column('exclude_plugins', sa.ARRAY(sa.String(length=255)), nullable=False), + sa.Column('include_plugins', sa.ARRAY(sa.String(length=255)), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='tenant_plugin_auto_upgrade_strategy_pkey'), + sa.UniqueConstraint('tenant_id', name='unique_tenant_plugin_auto_upgrade_strategy') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('tenant_plugin_auto_upgrade_strategies') + # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py b/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py index d7a5d116c9..47ac27511e 100644 --- a/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py +++ b/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py @@ -12,7 +12,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '4474872b0ee6' -down_revision = '2adcbe1f5dfb' +down_revision = '16081485540c' branch_labels = None depends_on = None diff --git a/api/migrations/versions/2025_07_04_1705-71f5020c6470_tool_oauth.py b/api/migrations/versions/2025_07_04_1705-71f5020c6470_tool_oauth.py new file mode 100644 index 0000000000..8520032f9c --- /dev/null +++ b/api/migrations/versions/2025_07_04_1705-71f5020c6470_tool_oauth.py @@ -0,0 +1,62 @@ +"""tool oauth + +Revision ID: 71f5020c6470 +Revises: 4474872b0ee6 +Create Date: 2025-06-24 17:05:43.118647 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '71f5020c6470' +down_revision = '58eb7bdb93fe' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tool_oauth_system_clients', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('plugin_id', sa.String(length=512), nullable=False), + sa.Column('provider', sa.String(length=255), nullable=False), + sa.Column('encrypted_oauth_params', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('id', name='tool_oauth_system_client_pkey'), + sa.UniqueConstraint('plugin_id', 'provider', name='tool_oauth_system_client_plugin_id_provider_idx') + ) + op.create_table('tool_oauth_tenant_clients', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('plugin_id', sa.String(length=512), nullable=False), + sa.Column('provider', sa.String(length=255), nullable=False), + sa.Column('enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False), + sa.Column('encrypted_oauth_params', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('id', name='tool_oauth_tenant_client_pkey'), + sa.UniqueConstraint('tenant_id', 'plugin_id', 'provider', name='unique_tool_oauth_tenant_client') + ) + + with op.batch_alter_table('tool_builtin_providers', schema=None) as batch_op: + batch_op.add_column(sa.Column('name', sa.String(length=256), server_default=sa.text("'API KEY 1'::character varying"), nullable=False)) + batch_op.add_column(sa.Column('is_default', sa.Boolean(), server_default=sa.text('false'), nullable=False)) + batch_op.add_column(sa.Column('credential_type', sa.String(length=32), server_default=sa.text("'api-key'::character varying"), nullable=False)) + batch_op.drop_constraint(batch_op.f('unique_builtin_tool_provider'), type_='unique') + batch_op.create_unique_constraint(batch_op.f('unique_builtin_tool_provider'), ['tenant_id', 'provider', 'name']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tool_builtin_providers', schema=None) as batch_op: + batch_op.drop_constraint(batch_op.f('unique_builtin_tool_provider'), type_='unique') + batch_op.create_unique_constraint(batch_op.f('unique_builtin_tool_provider'), ['tenant_id', 'provider']) + batch_op.drop_column('credential_type') + batch_op.drop_column('is_default') + batch_op.drop_column('name') + + op.drop_table('tool_oauth_tenant_clients') + op.drop_table('tool_oauth_system_clients') + # ### end Alembic commands ### diff --git a/api/models/account.py b/api/models/account.py index 7ffeefa980..1fa27f71dd 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -299,3 +299,35 @@ class TenantPluginPermission(Base): db.String(16), nullable=False, server_default="everyone" ) debug_permission: Mapped[DebugPermission] = mapped_column(db.String(16), nullable=False, server_default="noone") + + +class TenantPluginAutoUpgradeStrategy(Base): + class StrategySetting(enum.StrEnum): + DISABLED = "disabled" + FIX_ONLY = "fix_only" + LATEST = "latest" + + class UpgradeMode(enum.StrEnum): + ALL = "all" + PARTIAL = "partial" + EXCLUDE = "exclude" + + __tablename__ = "tenant_plugin_auto_upgrade_strategies" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"), + db.UniqueConstraint("tenant_id", name="unique_tenant_plugin_auto_upgrade_strategy"), + ) + + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + strategy_setting: Mapped[StrategySetting] = mapped_column(db.String(16), nullable=False, server_default="fix_only") + upgrade_time_of_day: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0) # seconds of the day + upgrade_mode: Mapped[UpgradeMode] = mapped_column(db.String(16), nullable=False, server_default="exclude") + exclude_plugins: Mapped[list[str]] = mapped_column( + db.ARRAY(db.String(255)), nullable=False + ) # plugin_id (author/name) + include_plugins: Mapped[list[str]] = mapped_column( + db.ARRAY(db.String(255)), nullable=False + ) # plugin_id (author/name) + created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) diff --git a/api/models/tools.py b/api/models/tools.py index 9d2c3baea5..d6a65885a7 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -21,6 +21,43 @@ from .model import Account, App, Tenant from .types import StringUUID +# system level tool oauth client params (client_id, client_secret, etc.) +class ToolOAuthSystemClient(Base): + __tablename__ = "tool_oauth_system_clients" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="tool_oauth_system_client_pkey"), + db.UniqueConstraint("plugin_id", "provider", name="tool_oauth_system_client_plugin_id_provider_idx"), + ) + + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + plugin_id: Mapped[str] = mapped_column(db.String(512), nullable=False) + provider: Mapped[str] = mapped_column(db.String(255), nullable=False) + # oauth params of the tool provider + encrypted_oauth_params: Mapped[str] = mapped_column(db.Text, nullable=False) + + +# tenant level tool oauth client params (client_id, client_secret, etc.) +class ToolOAuthTenantClient(Base): + __tablename__ = "tool_oauth_tenant_clients" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="tool_oauth_tenant_client_pkey"), + db.UniqueConstraint("tenant_id", "plugin_id", "provider", name="unique_tool_oauth_tenant_client"), + ) + + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + # tenant id + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + plugin_id: Mapped[str] = mapped_column(db.String(512), nullable=False) + provider: Mapped[str] = mapped_column(db.String(255), nullable=False) + enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) + # oauth params of the tool provider + encrypted_oauth_params: Mapped[str] = mapped_column(db.Text, nullable=False) + + @property + def oauth_params(self) -> dict: + return cast(dict, json.loads(self.encrypted_oauth_params or "{}")) + + class BuiltinToolProvider(Base): """ This table stores the tool provider information for built-in tools for each tenant. @@ -29,12 +66,14 @@ class BuiltinToolProvider(Base): __tablename__ = "tool_builtin_providers" __table_args__ = ( db.PrimaryKeyConstraint("id", name="tool_builtin_provider_pkey"), - # one tenant can only have one tool provider with the same name - db.UniqueConstraint("tenant_id", "provider", name="unique_builtin_tool_provider"), + db.UniqueConstraint("tenant_id", "provider", "name", name="unique_builtin_tool_provider"), ) # id of the tool provider id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + name: Mapped[str] = mapped_column( + db.String(256), nullable=False, server_default=db.text("'API KEY 1'::character varying") + ) # id of the tenant tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=True) # who created this tool provider @@ -49,6 +88,11 @@ class BuiltinToolProvider(Base): updated_at: Mapped[datetime] = mapped_column( db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") ) + is_default: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + # credential type, e.g., "api-key", "oauth2" + credential_type: Mapped[str] = mapped_column( + db.String(32), nullable=False, server_default=db.text("'api-key'::character varying") + ) @property def credentials(self) -> dict: @@ -68,7 +112,7 @@ class ApiToolProvider(Base): id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) # name of the api provider - name = db.Column(db.String(255), nullable=False) + name = db.Column(db.String(255), nullable=False, server_default=db.text("'API KEY 1'::character varying")) # icon icon = db.Column(db.String(255), nullable=False) # original schema @@ -281,18 +325,17 @@ class MCPToolProvider(Base): @property def decrypted_credentials(self) -> dict: + from core.helper.provider_cache import NoOpProviderCredentialCache from core.tools.mcp_tool.provider import MCPToolProviderController - from core.tools.utils.configuration import ProviderConfigEncrypter + from core.tools.utils.encryption import create_provider_encrypter provider_controller = MCPToolProviderController._from_db(self) - tool_configuration = ProviderConfigEncrypter( + return create_provider_encrypter( tenant_id=self.tenant_id, - config=list(provider_controller.get_credentials_schema()), - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.provider_id, - ) - return tool_configuration.decrypt(self.credentials, use_cache=False) + config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], + cache=NoOpProviderCredentialCache(), + )[0].decrypt(self.credentials) class ToolModelInvoke(Base): diff --git a/api/pyproject.toml b/api/pyproject.toml index 420bc771b6..7f1efa671f 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -108,7 +108,7 @@ dev = [ "faker~=32.1.0", "lxml-stubs~=0.5.1", "mypy~=1.16.0", - "ruff~=0.11.5", + "ruff~=0.12.3", "pytest~=8.3.2", "pytest-benchmark~=4.0.0", "pytest-cov~=4.1.0", diff --git a/api/schedule/check_upgradable_plugin_task.py b/api/schedule/check_upgradable_plugin_task.py new file mode 100644 index 0000000000..c1d6018827 --- /dev/null +++ b/api/schedule/check_upgradable_plugin_task.py @@ -0,0 +1,49 @@ +import time + +import click + +import app +from extensions.ext_database import db +from models.account import TenantPluginAutoUpgradeStrategy +from tasks.process_tenant_plugin_autoupgrade_check_task import process_tenant_plugin_autoupgrade_check_task + +AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL = 15 * 60 # 15 minutes + + +@app.celery.task(queue="plugin") +def check_upgradable_plugin_task(): + click.echo(click.style("Start check upgradable plugin.", fg="green")) + start_at = time.perf_counter() + + now_seconds_of_day = time.time() % 86400 - 30 # we assume the tz is UTC + click.echo(click.style("Now seconds of day: {}".format(now_seconds_of_day), fg="green")) + + strategies = ( + db.session.query(TenantPluginAutoUpgradeStrategy) + .filter( + TenantPluginAutoUpgradeStrategy.upgrade_time_of_day >= now_seconds_of_day, + TenantPluginAutoUpgradeStrategy.upgrade_time_of_day + < now_seconds_of_day + AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL, + TenantPluginAutoUpgradeStrategy.strategy_setting + != TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED, + ) + .all() + ) + + for strategy in strategies: + process_tenant_plugin_autoupgrade_check_task.delay( + strategy.tenant_id, + strategy.strategy_setting, + strategy.upgrade_time_of_day, + strategy.upgrade_mode, + strategy.exclude_plugins, + strategy.include_plugins, + ) + + end_at = time.perf_counter() + click.echo( + click.style( + "Checked upgradable plugin success latency: {}".format(end_at - start_at), + fg="green", + ) + ) diff --git a/api/services/account_service.py b/api/services/account_service.py index 2d540ea5b4..fd796e6cb0 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -28,6 +28,7 @@ from models.account import ( Tenant, TenantAccountJoin, TenantAccountRole, + TenantPluginAutoUpgradeStrategy, TenantStatus, ) from models.model import DifySetup @@ -829,6 +830,17 @@ class TenantService: db.session.add(tenant) db.session.commit() + plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy( + tenant_id=tenant.id, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + exclude_plugins=[], + include_plugins=[], + ) + db.session.add(plugin_upgrade_strategy) + db.session.commit() + tenant.encrypt_public_key = generate_key_pair(tenant.id) db.session.commit() return tenant diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 20257fa345..08e13c588e 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -575,13 +575,26 @@ class AppDslService: raise ValueError("Missing draft workflow configuration, please check.") workflow_dict = workflow.to_dict(include_secret=include_secret) + # TODO: refactor: we need a better way to filter workspace related data from nodes for node in workflow_dict.get("graph", {}).get("nodes", []): - if node.get("data", {}).get("type", "") == NodeType.KNOWLEDGE_RETRIEVAL.value: - dataset_ids = node["data"].get("dataset_ids", []) - node["data"]["dataset_ids"] = [ + node_data = node.get("data", {}) + if not node_data: + continue + data_type = node_data.get("type", "") + if data_type == NodeType.KNOWLEDGE_RETRIEVAL.value: + dataset_ids = node_data.get("dataset_ids", []) + node_data["dataset_ids"] = [ cls.encrypt_dataset_id(dataset_id=dataset_id, tenant_id=app_model.tenant_id) for dataset_id in dataset_ids ] + # filter credential id from tool node + if not include_secret and data_type == NodeType.TOOL.value: + node_data.pop("credential_id", None) + # filter credential id from agent node + if not include_secret and data_type == NodeType.AGENT.value: + for tool in node_data.get("agent_parameters", {}).get("tools", {}).get("value", []): + tool.pop("credential_id", None) + export_data["workflow"] = workflow_dict dependencies = cls._extract_dependencies_from_workflow(workflow) export_data["dependencies"] = [ @@ -602,7 +615,15 @@ class AppDslService: if not app_model_config: raise ValueError("Missing app configuration, please check.") - export_data["model_config"] = app_model_config.to_dict() + model_config = app_model_config.to_dict() + + # TODO: refactor: we need a better way to filter workspace related data from model config + # filter credential id from model config + for tool in model_config.get("agent_mode", {}).get("tools", []): + tool.pop("credential_id", None) + + export_data["model_config"] = model_config + dependencies = cls._extract_dependencies_from_model_config(app_model_config.to_dict()) export_data["dependencies"] = [ jsonable_encoder(d.model_dump()) diff --git a/api/services/enterprise/enterprise_service.py b/api/services/enterprise/enterprise_service.py index 8c06ee9386..54d45f45ea 100644 --- a/api/services/enterprise/enterprise_service.py +++ b/api/services/enterprise/enterprise_service.py @@ -29,7 +29,7 @@ class EnterpriseService: raise ValueError("No data found.") try: # parse the UTC timestamp from the response - return datetime.fromisoformat(data.replace("Z", "+00:00")) + return datetime.fromisoformat(data) except ValueError as e: raise ValueError(f"Invalid date format: {data}") from e @@ -40,7 +40,7 @@ class EnterpriseService: raise ValueError("No data found.") try: # parse the UTC timestamp from the response - return datetime.fromisoformat(data.replace("Z", "+00:00")) + return datetime.fromisoformat(data) except ValueError as e: raise ValueError(f"Invalid date format: {data}") from e diff --git a/api/services/entities/knowledge_entities/knowledge_entities.py b/api/services/entities/knowledge_entities/knowledge_entities.py index 603064ca07..88d4224e97 100644 --- a/api/services/entities/knowledge_entities/knowledge_entities.py +++ b/api/services/entities/knowledge_entities/knowledge_entities.py @@ -95,7 +95,7 @@ class WeightKeywordSetting(BaseModel): class WeightModel(BaseModel): - weight_type: Optional[str] = None + weight_type: Optional[Literal["semantic_first", "keyword_first", "customized"]] = None vector_setting: Optional[WeightVectorSetting] = None keyword_setting: Optional[WeightKeywordSetting] = None diff --git a/api/services/plugin/plugin_auto_upgrade_service.py b/api/services/plugin/plugin_auto_upgrade_service.py new file mode 100644 index 0000000000..3774050445 --- /dev/null +++ b/api/services/plugin/plugin_auto_upgrade_service.py @@ -0,0 +1,87 @@ +from sqlalchemy.orm import Session + +from extensions.ext_database import db +from models.account import TenantPluginAutoUpgradeStrategy + + +class PluginAutoUpgradeService: + @staticmethod + def get_strategy(tenant_id: str) -> TenantPluginAutoUpgradeStrategy | None: + with Session(db.engine) as session: + return ( + session.query(TenantPluginAutoUpgradeStrategy) + .filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) + .first() + ) + + @staticmethod + def change_strategy( + tenant_id: str, + strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting, + upgrade_time_of_day: int, + upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode, + exclude_plugins: list[str], + include_plugins: list[str], + ) -> bool: + with Session(db.engine) as session: + exist_strategy = ( + session.query(TenantPluginAutoUpgradeStrategy) + .filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) + .first() + ) + if not exist_strategy: + strategy = TenantPluginAutoUpgradeStrategy( + tenant_id=tenant_id, + strategy_setting=strategy_setting, + upgrade_time_of_day=upgrade_time_of_day, + upgrade_mode=upgrade_mode, + exclude_plugins=exclude_plugins, + include_plugins=include_plugins, + ) + session.add(strategy) + else: + exist_strategy.strategy_setting = strategy_setting + exist_strategy.upgrade_time_of_day = upgrade_time_of_day + exist_strategy.upgrade_mode = upgrade_mode + exist_strategy.exclude_plugins = exclude_plugins + exist_strategy.include_plugins = include_plugins + + session.commit() + return True + + @staticmethod + def exclude_plugin(tenant_id: str, plugin_id: str) -> bool: + with Session(db.engine) as session: + exist_strategy = ( + session.query(TenantPluginAutoUpgradeStrategy) + .filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) + .first() + ) + if not exist_strategy: + # create for this tenant + PluginAutoUpgradeService.change_strategy( + tenant_id, + TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + 0, + TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + [plugin_id], + [], + ) + return True + else: + if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE: + if plugin_id not in exist_strategy.exclude_plugins: + new_exclude_plugins = exist_strategy.exclude_plugins.copy() + new_exclude_plugins.append(plugin_id) + exist_strategy.exclude_plugins = new_exclude_plugins + elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL: + if plugin_id in exist_strategy.include_plugins: + new_include_plugins = exist_strategy.include_plugins.copy() + new_include_plugins.remove(plugin_id) + exist_strategy.include_plugins = new_include_plugins + elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL: + exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE + exist_strategy.exclude_plugins = [plugin_id] + + session.commit() + return True diff --git a/api/services/plugin/plugin_parameter_service.py b/api/services/plugin/plugin_parameter_service.py index 393213c0e2..a1c5639e00 100644 --- a/api/services/plugin/plugin_parameter_service.py +++ b/api/services/plugin/plugin_parameter_service.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session from core.plugin.entities.parameters import PluginParameterOption from core.plugin.impl.dynamic_select import DynamicSelectClient from core.tools.tool_manager import ToolManager -from core.tools.utils.configuration import ProviderConfigEncrypter +from core.tools.utils.encryption import create_tool_provider_encrypter from extensions.ext_database import db from models.tools import BuiltinToolProvider @@ -38,11 +38,9 @@ class PluginParameterService: case "tool": provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) # init tool configuration - tool_configuration = ProviderConfigEncrypter( + encrypter, _ = create_tool_provider_encrypter( tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + controller=provider_controller, ) # check if credentials are required @@ -63,7 +61,7 @@ class PluginParameterService: if db_record is None: raise ValueError(f"Builtin provider {provider} not found when fetching credentials") - credentials = tool_configuration.decrypt(db_record.credentials) + credentials = encrypter.decrypt(db_record.credentials) case _: raise ValueError(f"Invalid provider type: {provider_type}") diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index d7fb4a7c1b..0f22afd8dd 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -427,6 +427,9 @@ class PluginService: manager = PluginInstaller() + # collect actual plugin_unique_identifiers + actual_plugin_unique_identifiers = [] + metas = [] features = FeatureService.get_system_features() # check if already downloaded @@ -437,6 +440,8 @@ class PluginService: # check if the plugin is available to install PluginService._check_plugin_installation_scope(plugin_decode_response.verification) # already downloaded, skip + actual_plugin_unique_identifiers.append(plugin_unique_identifier) + metas.append({"plugin_unique_identifier": plugin_unique_identifier}) except Exception: # plugin not installed, download and upload pkg pkg = download_plugin_pkg(plugin_unique_identifier) @@ -447,17 +452,15 @@ class PluginService: ) # check if the plugin is available to install PluginService._check_plugin_installation_scope(response.verification) + # use response plugin_unique_identifier + actual_plugin_unique_identifiers.append(response.unique_identifier) + metas.append({"plugin_unique_identifier": response.unique_identifier}) return manager.install_from_identifiers( tenant_id, - plugin_unique_identifiers, + actual_plugin_unique_identifiers, PluginInstallationSource.Marketplace, - [ - { - "plugin_unique_identifier": plugin_unique_identifier, - } - for plugin_unique_identifier in plugin_unique_identifiers - ], + metas, ) @staticmethod diff --git a/api/services/tools/api_tools_manage_service.py b/api/services/tools/api_tools_manage_service.py index 6f848d49c4..80badf2335 100644 --- a/api/services/tools/api_tools_manage_service.py +++ b/api/services/tools/api_tools_manage_service.py @@ -18,7 +18,7 @@ from core.tools.entities.tool_entities import ( ) from core.tools.tool_label_manager import ToolLabelManager from core.tools.tool_manager import ToolManager -from core.tools.utils.configuration import ProviderConfigEncrypter +from core.tools.utils.encryption import create_tool_provider_encrypter from core.tools.utils.parser import ApiBasedToolSchemaParser from extensions.ext_database import db from models.tools import ApiToolProvider @@ -164,15 +164,11 @@ class ApiToolManageService: provider_controller.load_bundled_tools(tool_bundles) # encrypt credentials - tool_configuration = ProviderConfigEncrypter( + encrypter, _ = create_tool_provider_encrypter( tenant_id=tenant_id, - config=list(provider_controller.get_credentials_schema()), - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + controller=provider_controller, ) - - encrypted_credentials = tool_configuration.encrypt(credentials) - db_provider.credentials_str = json.dumps(encrypted_credentials) + db_provider.credentials_str = json.dumps(encrypter.encrypt(credentials)) db.session.add(db_provider) db.session.commit() @@ -297,28 +293,26 @@ class ApiToolManageService: provider_controller.load_bundled_tools(tool_bundles) # get original credentials if exists - tool_configuration = ProviderConfigEncrypter( + encrypter, cache = create_tool_provider_encrypter( tenant_id=tenant_id, - config=list(provider_controller.get_credentials_schema()), - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + controller=provider_controller, ) - original_credentials = tool_configuration.decrypt(provider.credentials) - masked_credentials = tool_configuration.mask_tool_credentials(original_credentials) + original_credentials = encrypter.decrypt(provider.credentials) + masked_credentials = encrypter.mask_tool_credentials(original_credentials) # check if the credential has changed, save the original credential for name, value in credentials.items(): if name in masked_credentials and value == masked_credentials[name]: credentials[name] = original_credentials[name] - credentials = tool_configuration.encrypt(credentials) + credentials = encrypter.encrypt(credentials) provider.credentials_str = json.dumps(credentials) db.session.add(provider) db.session.commit() # delete cache - tool_configuration.delete_tool_credentials_cache() + cache.delete() # update labels ToolLabelManager.update_tool_labels(provider_controller, labels) @@ -416,15 +410,13 @@ class ApiToolManageService: # decrypt credentials if db_provider.id: - tool_configuration = ProviderConfigEncrypter( + encrypter, _ = create_tool_provider_encrypter( tenant_id=tenant_id, - config=list(provider_controller.get_credentials_schema()), - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + controller=provider_controller, ) - decrypted_credentials = tool_configuration.decrypt(credentials) + decrypted_credentials = encrypter.decrypt(credentials) # check if the credential has changed, save the original credential - masked_credentials = tool_configuration.mask_tool_credentials(decrypted_credentials) + masked_credentials = encrypter.mask_tool_credentials(decrypted_credentials) for name, value in credentials.items(): if name in masked_credentials and value == masked_credentials[name]: credentials[name] = decrypted_credentials[name] @@ -446,7 +438,7 @@ class ApiToolManageService: return {"result": result or "empty response"} @staticmethod - def list_api_tools(user_id: str, tenant_id: str) -> list[ToolProviderApiEntity]: + def list_api_tools(tenant_id: str) -> list[ToolProviderApiEntity]: """ list api tools """ @@ -474,7 +466,7 @@ class ApiToolManageService: for tool in tools or []: user_provider.tools.append( ToolTransformService.convert_tool_entity_to_api_entity( - tenant_id=tenant_id, tool=tool, credentials=user_provider.original_credentials, labels=labels + tenant_id=tenant_id, tool=tool, labels=labels ) ) diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 58a4b2f179..92b7b49336 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -1,28 +1,63 @@ import json import logging +import re from pathlib import Path +from typing import Any, Optional from sqlalchemy.orm import Session from configs import dify_config +from constants import HIDDEN_VALUE, UNKNOWN_VALUE from core.helper.position_helper import is_filtered -from core.model_runtime.utils.encoders import jsonable_encoder +from core.helper.provider_cache import NoOpProviderCredentialCache, ToolProviderCredentialsCache from core.plugin.entities.plugin import ToolProviderID from core.plugin.impl.exc import PluginDaemonClientSideError +from core.tools.builtin_tool.provider import BuiltinToolProviderController from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort -from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity +from core.tools.entities.api_entities import ( + ToolApiEntity, + ToolProviderApiEntity, + ToolProviderCredentialApiEntity, + ToolProviderCredentialInfoApiEntity, +) +from core.tools.entities.tool_entities import CredentialType from core.tools.errors import ToolNotFoundError, ToolProviderCredentialValidationError, ToolProviderNotFoundError +from core.tools.plugin_tool.provider import PluginToolProviderController from core.tools.tool_label_manager import ToolLabelManager from core.tools.tool_manager import ToolManager -from core.tools.utils.configuration import ProviderConfigEncrypter +from core.tools.utils.encryption import create_provider_encrypter +from core.tools.utils.system_oauth_encryption import decrypt_system_oauth_params from extensions.ext_database import db -from models.tools import BuiltinToolProvider +from extensions.ext_redis import redis_client +from models.tools import BuiltinToolProvider, ToolOAuthSystemClient, ToolOAuthTenantClient from services.tools.tools_transform_service import ToolTransformService logger = logging.getLogger(__name__) class BuiltinToolManageService: + __MAX_BUILTIN_TOOL_PROVIDER_COUNT__ = 100 + + @staticmethod + def get_builtin_tool_provider_oauth_client_schema(tenant_id: str, provider_name: str): + """ + get builtin tool provider oauth client schema + """ + provider = ToolManager.get_builtin_provider(provider_name, tenant_id) + + is_oauth_custom_client_enabled = BuiltinToolManageService.is_oauth_custom_client_enabled( + tenant_id, provider_name + ) + is_system_oauth_params_exists = BuiltinToolManageService.is_oauth_system_client_exists(provider_name) + result = { + "schema": provider.get_oauth_client_schema(), + "is_oauth_custom_client_enabled": is_oauth_custom_client_enabled, + "is_system_oauth_params_exists": is_system_oauth_params_exists, + "client_params": BuiltinToolManageService.get_custom_oauth_client_params(tenant_id, provider_name), + "redirect_uri": f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider_name}/tool/callback", + } + return result + @staticmethod def list_builtin_tool_provider_tools(tenant_id: str, provider: str) -> list[ToolApiEntity]: """ @@ -36,27 +71,11 @@ class BuiltinToolManageService: provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) tools = provider_controller.get_tools() - tool_provider_configurations = ProviderConfigEncrypter( - tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, - ) - # check if user has added the provider - builtin_provider = BuiltinToolManageService._fetch_builtin_provider(provider, tenant_id) - - credentials = {} - if builtin_provider is not None: - # get credentials - credentials = builtin_provider.credentials - credentials = tool_provider_configurations.decrypt(credentials) - result: list[ToolApiEntity] = [] for tool in tools or []: result.append( ToolTransformService.convert_tool_entity_to_api_entity( tool=tool, - credentials=credentials, tenant_id=tenant_id, labels=ToolLabelManager.get_tool_labels(provider_controller), ) @@ -65,25 +84,15 @@ class BuiltinToolManageService: return result @staticmethod - def get_builtin_tool_provider_info(user_id: str, tenant_id: str, provider: str): + def get_builtin_tool_provider_info(tenant_id: str, provider: str): """ get builtin tool provider info """ provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) - tool_provider_configurations = ProviderConfigEncrypter( - tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, - ) # check if user has added the provider - builtin_provider = BuiltinToolManageService._fetch_builtin_provider(provider, tenant_id) - - credentials = {} - if builtin_provider is not None: - # get credentials - credentials = builtin_provider.credentials - credentials = tool_provider_configurations.decrypt(credentials) + builtin_provider = BuiltinToolManageService.get_builtin_provider(provider, tenant_id) + if builtin_provider is None: + raise ValueError(f"you have not added provider {provider}") entity = ToolTransformService.builtin_provider_to_user_provider( provider_controller=provider_controller, @@ -92,127 +101,410 @@ class BuiltinToolManageService: ) entity.original_credentials = {} - return entity @staticmethod - def list_builtin_provider_credentials_schema(provider_name: str, tenant_id: str): + def list_builtin_provider_credentials_schema(provider_name: str, credential_type: CredentialType, tenant_id: str): """ list builtin provider credentials schema + :param credential_type: credential type :param provider_name: the name of the provider :param tenant_id: the id of the tenant :return: the list of tool providers """ provider = ToolManager.get_builtin_provider(provider_name, tenant_id) - return jsonable_encoder(provider.get_credentials_schema()) + return provider.get_credentials_schema_by_type(credential_type) @staticmethod def update_builtin_tool_provider( - session: Session, user_id: str, tenant_id: str, provider_name: str, credentials: dict + user_id: str, + tenant_id: str, + provider: str, + credential_id: str, + credentials: dict | None = None, + name: str | None = None, ): """ update builtin tool provider """ - # get if the provider exists - provider = BuiltinToolManageService._fetch_builtin_provider(provider_name, tenant_id) + 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() and credentials: + 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 + ) + + original_credentials = encrypter.decrypt(db_provider.credentials) + new_credentials: dict = { + key: value if value != HIDDEN_VALUE else original_credentials.get(key, UNKNOWN_VALUE) + for key, value in credentials.items() + } + + if CredentialType.of(db_provider.credential_type).is_validate_allowed(): + provider_controller.validate_credentials(user_id, new_credentials) + + # encrypt credentials + db_provider.encrypted_credentials = json.dumps(encrypter.encrypt(new_credentials)) + + cache.delete() + + # update name if provided + if name and name != db_provider.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") + + db_provider.name = name + + session.commit() + except ( + PluginDaemonClientSideError, + ToolProviderNotFoundError, + ToolNotFoundError, + ToolProviderCredentialValidationError, + ) as e: + session.rollback() + raise ValueError(str(e)) + + return {"result": "success"} + @staticmethod + def add_builtin_tool_provider( + user_id: str, + api_type: CredentialType, + tenant_id: str, + provider: str, + credentials: dict, + name: str | None = None, + ): + """ + add builtin tool provider + """ try: - # get provider - provider_controller = ToolManager.get_builtin_provider(provider_name, tenant_id) - if not provider_controller.need_credentials: - raise ValueError(f"provider {provider_name} does not need credentials") - tool_configuration = ProviderConfigEncrypter( - tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, - ) + 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() + ) - # get original credentials if exists - if provider is not None: - original_credentials = tool_configuration.decrypt(provider.credentials) - masked_credentials = tool_configuration.mask_tool_credentials(original_credentials) - # check if the credential has changed, save the original credential - for name, value in credentials.items(): - if name in masked_credentials and value == masked_credentials[name]: - credentials[name] = original_credentials[name] - # validate credentials - provider_controller.validate_credentials(user_id, credentials) - # encrypt credentials - credentials = tool_configuration.encrypt(credentials) + # 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) + + # generate name if not provided + if name is None or name == "": + 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(), + ) + + 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, + ) + + session.add(db_provider) + session.commit() except ( PluginDaemonClientSideError, ToolProviderNotFoundError, ToolNotFoundError, ToolProviderCredentialValidationError, ) as e: + session.rollback() raise ValueError(str(e)) + return {"result": "success"} - if provider is None: - # create provider - provider = BuiltinToolProvider( - tenant_id=tenant_id, - user_id=user_id, - provider=provider_name, - encrypted_credentials=json.dumps(credentials), + @staticmethod + def create_tool_encrypter( + tenant_id: str, + db_provider: BuiltinToolProvider, + provider: str, + provider_controller: BuiltinToolProviderController, + ): + encrypter, cache = create_provider_encrypter( + tenant_id=tenant_id, + config=[ + x.to_basic_provider_config() + for x in provider_controller.get_credentials_schema_by_type(db_provider.credential_type) + ], + cache=ToolProviderCredentialsCache(tenant_id=tenant_id, provider=provider, credential_id=db_provider.id), + ) + return encrypter, cache + + @staticmethod + def generate_builtin_tool_provider_name( + session: Session, tenant_id: str, provider: str, credential_type: CredentialType + ) -> str: + try: + db_providers = ( + session.query(BuiltinToolProvider) + .filter_by( + tenant_id=tenant_id, + provider=provider, + credential_type=credential_type.value, + ) + .order_by(BuiltinToolProvider.created_at.desc()) + .all() ) - db.session.add(provider) - else: - provider.encrypted_credentials = json.dumps(credentials) + # Get the default name pattern + default_pattern = f"{credential_type.get_name()}" - # delete cache - tool_configuration.delete_tool_credentials_cache() + # Find all names that match the default pattern: "{default_pattern} {number}" + pattern = rf"^{re.escape(default_pattern)}\s+(\d+)$" + numbers = [] - db.session.commit() - return {"result": "success"} + for db_provider in db_providers: + if db_provider.name: + match = re.match(pattern, db_provider.name.strip()) + if match: + numbers.append(int(match.group(1))) + + # If no default pattern names found, start with 1 + if not numbers: + return f"{default_pattern} 1" + + # Find the next number + max_number = max(numbers) + return f"{default_pattern} {max_number + 1}" + except Exception as e: + logger.warning(f"Error generating next provider name for {provider}: {str(e)}") + # fallback + return f"{credential_type.get_name()} 1" @staticmethod - def get_builtin_tool_provider_credentials(tenant_id: str, provider_name: str): + def get_builtin_tool_provider_credentials( + tenant_id: str, provider_name: str + ) -> list[ToolProviderCredentialApiEntity]: """ get builtin tool provider credentials """ - provider_obj = BuiltinToolManageService._fetch_builtin_provider(provider_name, tenant_id) + with db.session.no_autoflush: + providers = ( + db.session.query(BuiltinToolProvider) + .filter_by(tenant_id=tenant_id, provider=provider_name) + .order_by(BuiltinToolProvider.is_default.desc(), BuiltinToolProvider.created_at.asc()) + .all() + ) - if provider_obj is None: - return {} + if len(providers) == 0: + return [] + + default_provider = providers[0] + default_provider.is_default = True + provider_controller = ToolManager.get_builtin_provider(default_provider.provider, tenant_id) + + credentials: list[ToolProviderCredentialApiEntity] = [] + encrypters = {} + for provider in providers: + credential_type = provider.credential_type + if credential_type not in encrypters: + encrypters[credential_type] = BuiltinToolManageService.create_tool_encrypter( + tenant_id, provider, provider.provider, provider_controller + )[0] + encrypter = encrypters[credential_type] + decrypt_credential = encrypter.mask_tool_credentials(encrypter.decrypt(provider.credentials)) + credential_entity = ToolTransformService.convert_builtin_provider_to_credential_entity( + provider=provider, + credentials=decrypt_credential, + ) + credentials.append(credential_entity) + return credentials - provider_controller = ToolManager.get_builtin_provider(provider_obj.provider, tenant_id) - tool_configuration = ProviderConfigEncrypter( - tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + @staticmethod + def get_builtin_tool_provider_credential_info(tenant_id: str, provider: str) -> ToolProviderCredentialInfoApiEntity: + """ + get builtin tool provider credential info + """ + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + supported_credential_types = provider_controller.get_supported_credential_types() + credentials = BuiltinToolManageService.get_builtin_tool_provider_credentials(tenant_id, provider) + credential_info = ToolProviderCredentialInfoApiEntity( + supported_credential_types=supported_credential_types, + is_oauth_custom_client_enabled=BuiltinToolManageService.is_oauth_custom_client_enabled(tenant_id, provider), + credentials=credentials, ) - credentials = tool_configuration.decrypt(provider_obj.credentials) - credentials = tool_configuration.mask_tool_credentials(credentials) - return credentials + + return credential_info @staticmethod - def delete_builtin_tool_provider(user_id: str, tenant_id: str, provider_name: str): + def delete_builtin_tool_provider(tenant_id: str, provider: str, credential_id: str): """ delete tool provider """ - provider_obj = BuiltinToolManageService._fetch_builtin_provider(provider_name, tenant_id) + with Session(db.engine) as session: + 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}") + + session.delete(db_provider) + session.commit() + + # 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"} + + @staticmethod + def set_default_provider(tenant_id: str, user_id: str, provider: str, id: str): + """ + set default provider + """ + with Session(db.engine) as session: + # get provider + target_provider = session.query(BuiltinToolProvider).filter_by(id=id).first() + if target_provider is None: + raise ValueError("provider not found") + + # clear default provider + session.query(BuiltinToolProvider).filter_by( + 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 + session.commit() + return {"result": "success"} - if provider_obj is None: - raise ValueError(f"you have not added provider {provider_name}") + @staticmethod + def is_oauth_system_client_exists(provider_name: str) -> bool: + """ + check if oauth system client exists + """ + tool_provider = ToolProviderID(provider_name) + with Session(db.engine).no_autoflush as session: + system_client: ToolOAuthSystemClient | None = ( + session.query(ToolOAuthSystemClient) + .filter_by(plugin_id=tool_provider.plugin_id, provider=tool_provider.provider_name) + .first() + ) + return system_client is not None - db.session.delete(provider_obj) - db.session.commit() + @staticmethod + def is_oauth_custom_client_enabled(tenant_id: str, provider: str) -> bool: + """ + check if oauth custom client is enabled + """ + tool_provider = ToolProviderID(provider) + with Session(db.engine).no_autoflush as session: + user_client: ToolOAuthTenantClient | None = ( + session.query(ToolOAuthTenantClient) + .filter_by( + tenant_id=tenant_id, + provider=tool_provider.provider_name, + plugin_id=tool_provider.plugin_id, + enabled=True, + ) + .first() + ) + return user_client is not None and user_client.enabled - # delete cache - provider_controller = ToolManager.get_builtin_provider(provider_name, tenant_id) - tool_configuration = ProviderConfigEncrypter( + @staticmethod + def get_oauth_client(tenant_id: str, provider: str) -> dict[str, Any] | None: + """ + get builtin tool provider + """ + tool_provider = ToolProviderID(provider) + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + encrypter, _ = create_provider_encrypter( tenant_id=tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + config=[x.to_basic_provider_config() for x in provider_controller.get_oauth_client_schema()], + cache=NoOpProviderCredentialCache(), ) - tool_configuration.delete_tool_credentials_cache() + with Session(db.engine).no_autoflush as session: + user_client: ToolOAuthTenantClient | None = ( + session.query(ToolOAuthTenantClient) + .filter_by( + tenant_id=tenant_id, + provider=tool_provider.provider_name, + plugin_id=tool_provider.plugin_id, + enabled=True, + ) + .first() + ) + oauth_params: dict[str, Any] | None = None + if user_client: + oauth_params = encrypter.decrypt(user_client.oauth_params) + return oauth_params + + system_client: ToolOAuthSystemClient | None = ( + session.query(ToolOAuthSystemClient) + .filter_by(plugin_id=tool_provider.plugin_id, provider=tool_provider.provider_name) + .first() + ) + if system_client: + try: + oauth_params = decrypt_system_oauth_params(system_client.encrypted_oauth_params) + except Exception as e: + raise ValueError(f"Error decrypting system oauth params: {e}") - return {"result": "success"} + return oauth_params @staticmethod def get_builtin_tool_provider_icon(provider: str): @@ -234,9 +526,7 @@ class BuiltinToolManageService: with db.session.no_autoflush: # get all user added providers - db_providers: list[BuiltinToolProvider] = ( - db.session.query(BuiltinToolProvider).filter(BuiltinToolProvider.tenant_id == tenant_id).all() or [] - ) + db_providers: list[BuiltinToolProvider] = ToolManager.list_default_builtin_providers(tenant_id) # rewrite db_providers for db_provider in db_providers: @@ -275,7 +565,6 @@ class BuiltinToolManageService: ToolTransformService.convert_tool_entity_to_api_entity( tenant_id=tenant_id, tool=tool, - credentials=user_builtin_provider.original_credentials, labels=ToolLabelManager.get_tool_labels(provider_controller), ) ) @@ -287,43 +576,153 @@ class BuiltinToolManageService: return BuiltinToolProviderSort.sort(result) @staticmethod - def _fetch_builtin_provider(provider_name: str, tenant_id: str) -> BuiltinToolProvider | None: - try: - full_provider_name = provider_name - provider_id_entity = ToolProviderID(provider_name) - provider_name = provider_id_entity.provider_name - if provider_id_entity.organization != "langgenius": - provider_obj = ( - db.session.query(BuiltinToolProvider) - .filter( - BuiltinToolProvider.tenant_id == tenant_id, - BuiltinToolProvider.provider == full_provider_name, + def get_builtin_provider(provider_name: str, tenant_id: str) -> Optional[BuiltinToolProvider]: + """ + This method is used to fetch the builtin provider from the database + 1.if the default provider exists, return the default provider + 2.if the default provider does not exist, return the oldest provider + """ + with Session(db.engine) as session: + try: + full_provider_name = provider_name + provider_id_entity = ToolProviderID(provider_name) + provider_name = provider_id_entity.provider_name + + if provider_id_entity.organization != "langgenius": + provider = ( + session.query(BuiltinToolProvider) + .filter( + BuiltinToolProvider.tenant_id == tenant_id, + BuiltinToolProvider.provider == full_provider_name, + ) + .order_by( + BuiltinToolProvider.is_default.desc(), # default=True first + BuiltinToolProvider.created_at.asc(), # oldest first + ) + .first() ) - .first() - ) - else: - provider_obj = ( - db.session.query(BuiltinToolProvider) - .filter( - BuiltinToolProvider.tenant_id == tenant_id, - (BuiltinToolProvider.provider == provider_name) - | (BuiltinToolProvider.provider == full_provider_name), + else: + provider = ( + session.query(BuiltinToolProvider) + .filter( + BuiltinToolProvider.tenant_id == tenant_id, + (BuiltinToolProvider.provider == provider_name) + | (BuiltinToolProvider.provider == full_provider_name), + ) + .order_by( + BuiltinToolProvider.is_default.desc(), # default=True first + BuiltinToolProvider.created_at.asc(), # oldest first + ) + .first() + ) + + if provider is None: + return None + + provider.provider = ToolProviderID(provider.provider).to_string() + return provider + except Exception: + # it's an old provider without organization + return ( + session.query(BuiltinToolProvider) + .filter(BuiltinToolProvider.tenant_id == tenant_id, BuiltinToolProvider.provider == provider_name) + .order_by( + BuiltinToolProvider.is_default.desc(), # default=True first + BuiltinToolProvider.created_at.asc(), # oldest first ) .first() ) - if provider_obj is None: - return None + @staticmethod + def save_custom_oauth_client_params( + tenant_id: str, + provider: str, + client_params: Optional[dict] = None, + enable_oauth_custom_client: Optional[bool] = None, + ): + """ + setup oauth custom client + """ + if client_params is None and enable_oauth_custom_client is None: + return {"result": "success"} - provider_obj.provider = ToolProviderID(provider_obj.provider).to_string() - return provider_obj - except Exception: - # it's an old provider without organization - return ( - db.session.query(BuiltinToolProvider) - .filter( - BuiltinToolProvider.tenant_id == tenant_id, - (BuiltinToolProvider.provider == provider_name), + tool_provider = ToolProviderID(provider) + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + if not provider_controller: + raise ToolProviderNotFoundError(f"Provider {provider} not found") + + if not isinstance(provider_controller, (BuiltinToolProviderController, PluginToolProviderController)): + raise ValueError(f"Provider {provider} is not a builtin or plugin provider") + + with Session(db.engine) as session: + custom_client_params = ( + session.query(ToolOAuthTenantClient) + .filter_by( + tenant_id=tenant_id, + plugin_id=tool_provider.plugin_id, + provider=tool_provider.provider_name, ) .first() ) + + # if the record does not exist, create a basic record + if custom_client_params is None: + custom_client_params = ToolOAuthTenantClient( + tenant_id=tenant_id, + plugin_id=tool_provider.plugin_id, + provider=tool_provider.provider_name, + ) + session.add(custom_client_params) + + if client_params is not None: + encrypter, _ = create_provider_encrypter( + tenant_id=tenant_id, + config=[x.to_basic_provider_config() for x in provider_controller.get_oauth_client_schema()], + cache=NoOpProviderCredentialCache(), + ) + original_params = encrypter.decrypt(custom_client_params.oauth_params) + new_params: dict = { + key: value if value != HIDDEN_VALUE else original_params.get(key, UNKNOWN_VALUE) + for key, value in client_params.items() + } + custom_client_params.encrypted_oauth_params = json.dumps(encrypter.encrypt(new_params)) + + if enable_oauth_custom_client is not None: + custom_client_params.enabled = enable_oauth_custom_client + + session.commit() + return {"result": "success"} + + @staticmethod + def get_custom_oauth_client_params(tenant_id: str, provider: str): + """ + get custom oauth client params + """ + with Session(db.engine) as session: + tool_provider = ToolProviderID(provider) + custom_oauth_client_params: ToolOAuthTenantClient | None = ( + session.query(ToolOAuthTenantClient) + .filter_by( + tenant_id=tenant_id, + plugin_id=tool_provider.plugin_id, + provider=tool_provider.provider_name, + ) + .first() + ) + if custom_oauth_client_params is None: + return {} + + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + if not provider_controller: + raise ToolProviderNotFoundError(f"Provider {provider} not found") + + if not isinstance(provider_controller, BuiltinToolProviderController): + raise ValueError(f"Provider {provider} is not a builtin or plugin provider") + + encrypter, _ = create_provider_encrypter( + tenant_id=tenant_id, + config=[x.to_basic_provider_config() for x in provider_controller.get_oauth_client_schema()], + cache=NoOpProviderCredentialCache(), + ) + + return encrypter.mask_tool_credentials(encrypter.decrypt(custom_oauth_client_params.oauth_params)) diff --git a/api/services/tools/mcp_tools_mange_service.py b/api/services/tools/mcp_tools_mange_service.py index 7c23abda4b..fda6da5983 100644 --- a/api/services/tools/mcp_tools_mange_service.py +++ b/api/services/tools/mcp_tools_mange_service.py @@ -7,13 +7,14 @@ from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from core.helper import encrypter +from core.helper.provider_cache import NoOpProviderCredentialCache from core.mcp.error import MCPAuthError, MCPError from core.mcp.mcp_client import MCPClient from core.tools.entities.api_entities import ToolProviderApiEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderType from core.tools.mcp_tool.provider import MCPToolProviderController -from core.tools.utils.configuration import ProviderConfigEncrypter +from core.tools.utils.encryption import ProviderConfigEncrypter from extensions.ext_database import db from models.tools import MCPToolProvider from services.tools.tools_transform_service import ToolTransformService @@ -69,6 +70,7 @@ class MCPToolManageService: MCPToolProvider.server_url_hash == server_url_hash, MCPToolProvider.server_identifier == server_identifier, ), + MCPToolProvider.tenant_id == tenant_id, ) .first() ) @@ -197,8 +199,7 @@ class MCPToolManageService: tool_configuration = ProviderConfigEncrypter( tenant_id=mcp_provider.tenant_id, config=list(provider_controller.get_credentials_schema()), - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.provider_id, + provider_config_cache=NoOpProviderCredentialCache(), ) credentials = tool_configuration.encrypt(credentials) mcp_provider.updated_at = datetime.now() diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index 3d0c35cd9b..36b892e205 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -5,21 +5,23 @@ from typing import Any, Optional, Union, cast from yarl import URL from configs import dify_config +from core.helper.provider_cache import ToolProviderCredentialsCache from core.mcp.types import Tool as MCPTool from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.builtin_tool.provider import BuiltinToolProviderController from core.tools.custom_tool.provider import ApiToolProviderController -from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity +from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity, ToolProviderCredentialApiEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_bundle import ApiToolBundle from core.tools.entities.tool_entities import ( ApiProviderAuthType, + CredentialType, ToolParameter, ToolProviderType, ) from core.tools.plugin_tool.provider import PluginToolProviderController -from core.tools.utils.configuration import ProviderConfigEncrypter +from core.tools.utils.encryption import create_provider_encrypter, create_tool_provider_encrypter from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from core.tools.workflow_as_tool.tool import WorkflowTool from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider @@ -119,7 +121,12 @@ class ToolTransformService: result.plugin_unique_identifier = provider_controller.plugin_unique_identifier # get credentials schema - schema = {x.to_basic_provider_config().name: x for x in provider_controller.get_credentials_schema()} + schema = { + x.to_basic_provider_config().name: x + for x in provider_controller.get_credentials_schema_by_type( + CredentialType.of(db_provider.credential_type) if db_provider else CredentialType.API_KEY + ) + } for name, value in schema.items(): if result.masked_credentials: @@ -136,15 +143,23 @@ class ToolTransformService: credentials = db_provider.credentials # init tool configuration - tool_configuration = ProviderConfigEncrypter( + encrypter, _ = create_provider_encrypter( tenant_id=db_provider.tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + config=[ + x.to_basic_provider_config() + for x in provider_controller.get_credentials_schema_by_type( + CredentialType.of(db_provider.credential_type) + ) + ], + cache=ToolProviderCredentialsCache( + tenant_id=db_provider.tenant_id, + provider=db_provider.provider, + credential_id=db_provider.id, + ), ) # decrypt the credentials and mask the credentials - decrypted_credentials = tool_configuration.decrypt(data=credentials) - masked_credentials = tool_configuration.mask_tool_credentials(data=decrypted_credentials) + decrypted_credentials = encrypter.decrypt(data=credentials) + masked_credentials = encrypter.mask_tool_credentials(data=decrypted_credentials) result.masked_credentials = masked_credentials result.original_credentials = decrypted_credentials @@ -287,16 +302,14 @@ class ToolTransformService: if decrypt_credentials: # init tool configuration - tool_configuration = ProviderConfigEncrypter( + encrypter, _ = create_tool_provider_encrypter( tenant_id=db_provider.tenant_id, - config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], - provider_type=provider_controller.provider_type.value, - provider_identity=provider_controller.entity.identity.name, + controller=provider_controller, ) # decrypt the credentials and mask the credentials - decrypted_credentials = tool_configuration.decrypt(data=credentials) - masked_credentials = tool_configuration.mask_tool_credentials(data=decrypted_credentials) + decrypted_credentials = encrypter.decrypt(data=credentials) + masked_credentials = encrypter.mask_tool_credentials(data=decrypted_credentials) result.masked_credentials = masked_credentials @@ -306,7 +319,6 @@ class ToolTransformService: def convert_tool_entity_to_api_entity( tool: Union[ApiToolBundle, WorkflowTool, Tool], tenant_id: str, - credentials: dict | None = None, labels: list[str] | None = None, ) -> ToolApiEntity: """ @@ -316,7 +328,7 @@ class ToolTransformService: # fork tool runtime tool = tool.fork_tool_runtime( runtime=ToolRuntime( - credentials=credentials or {}, + credentials={}, tenant_id=tenant_id, ) ) @@ -357,6 +369,19 @@ class ToolTransformService: labels=labels or [], ) + @staticmethod + def convert_builtin_provider_to_credential_entity( + provider: BuiltinToolProvider, credentials: dict + ) -> ToolProviderCredentialApiEntity: + return ToolProviderCredentialApiEntity( + id=provider.id, + name=provider.name, + provider=provider.provider, + credential_type=CredentialType.of(provider.credential_type), + is_default=provider.is_default, + credentials=credentials, + ) + @staticmethod def convert_mcp_schema_to_parameter(schema: dict) -> list["ToolParameter"]: """ diff --git a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py new file mode 100644 index 0000000000..42484814fe --- /dev/null +++ b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py @@ -0,0 +1,166 @@ +import traceback +import typing + +import click +from celery import shared_task # type: ignore + +from core.helper import marketplace +from core.helper.marketplace import MarketplacePluginDeclaration +from core.plugin.entities.plugin import PluginInstallationSource +from core.plugin.impl.plugin import PluginInstaller +from models.account import TenantPluginAutoUpgradeStrategy + +RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3 + + +cached_plugin_manifests: dict[str, typing.Union[MarketplacePluginDeclaration, None]] = {} + + +def marketplace_batch_fetch_plugin_manifests( + plugin_ids_plain_list: list[str], +) -> list[MarketplacePluginDeclaration]: + global cached_plugin_manifests + # return marketplace.batch_fetch_plugin_manifests(plugin_ids_plain_list) + not_included_plugin_ids = [ + plugin_id for plugin_id in plugin_ids_plain_list if plugin_id not in cached_plugin_manifests + ] + if not_included_plugin_ids: + manifests = marketplace.batch_fetch_plugin_manifests_ignore_deserialization_error(not_included_plugin_ids) + for manifest in manifests: + cached_plugin_manifests[manifest.plugin_id] = manifest + + if ( + len(manifests) == 0 + ): # this indicates that the plugin not found in marketplace, should set None in cache to prevent future check + for plugin_id in not_included_plugin_ids: + cached_plugin_manifests[plugin_id] = None + + result: list[MarketplacePluginDeclaration] = [] + for plugin_id in plugin_ids_plain_list: + final_manifest = cached_plugin_manifests.get(plugin_id) + if final_manifest is not None: + result.append(final_manifest) + + return result + + +@shared_task(queue="plugin") +def process_tenant_plugin_autoupgrade_check_task( + tenant_id: str, + strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting, + upgrade_time_of_day: int, + upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode, + exclude_plugins: list[str], + include_plugins: list[str], +): + try: + manager = PluginInstaller() + + click.echo( + click.style( + "Checking upgradable plugin for tenant: {}".format(tenant_id), + fg="green", + ) + ) + + if strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED: + return + + # 获取需要检查的插件 + plugin_ids: list[tuple[str, str, str]] = [] # plugin_id, version, unique_identifier + click.echo(click.style("Upgrade mode: {}".format(upgrade_mode), fg="green")) + + if upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL and include_plugins: + all_plugins = manager.list_plugins(tenant_id) + + for plugin in all_plugins: + if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id in include_plugins: + plugin_ids.append( + ( + plugin.plugin_id, + plugin.version, + plugin.plugin_unique_identifier, + ) + ) + + elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE: + # 获取所有插件并移除exclude的 + all_plugins = manager.list_plugins(tenant_id) + plugin_ids = [ + (plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier) + for plugin in all_plugins + if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id not in exclude_plugins + ] + elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL: + all_plugins = manager.list_plugins(tenant_id) + plugin_ids = [ + (plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier) + for plugin in all_plugins + if plugin.source == PluginInstallationSource.Marketplace + ] + + if not plugin_ids: + return + + plugin_ids_plain_list = [plugin_id for plugin_id, _, _ in plugin_ids] + + manifests = marketplace_batch_fetch_plugin_manifests(plugin_ids_plain_list) + + if not manifests: + return + + for manifest in manifests: + for plugin_id, version, original_unique_identifier in plugin_ids: + if manifest.plugin_id != plugin_id: + continue + + try: + current_version = version + latest_version = manifest.latest_version + + def fix_only_checker(latest_version, current_version): + latest_version_tuple = tuple(int(val) for val in latest_version.split(".")) + current_version_tuple = tuple(int(val) for val in current_version.split(".")) + + if ( + latest_version_tuple[0] == current_version_tuple[0] + and latest_version_tuple[1] == current_version_tuple[1] + ): + return latest_version_tuple[2] != current_version_tuple[2] + return False + + version_checker = { + TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST: lambda latest_version, + current_version: latest_version != current_version, + TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY: fix_only_checker, + } + + if version_checker[strategy_setting](latest_version, current_version): + # 执行升级 + new_unique_identifier = manifest.latest_package_identifier + + marketplace.record_install_plugin_event(new_unique_identifier) + click.echo( + click.style( + "Upgrade plugin: {} -> {}".format(original_unique_identifier, new_unique_identifier), + fg="green", + ) + ) + task_start_resp = manager.upgrade_plugin( + tenant_id, + original_unique_identifier, + new_unique_identifier, + PluginInstallationSource.Marketplace, + { + "plugin_unique_identifier": new_unique_identifier, + }, + ) + except Exception as e: + click.echo(click.style("Error when upgrading plugin: {}".format(e), fg="red")) + traceback.print_exc() + break + + except Exception as e: + click.echo(click.style("Error when checking upgradable plugin: {}".format(e), fg="red")) + traceback.print_exc() + return diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index 638323f850..8acaa54b9c 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -116,11 +116,11 @@ def test_execute_llm(flask_req_ctx): mock_usage = LLMUsage( prompt_tokens=30, prompt_unit_price=Decimal("0.001"), - prompt_price_unit=Decimal("1000"), + prompt_price_unit=Decimal(1000), prompt_price=Decimal("0.00003"), completion_tokens=20, completion_unit_price=Decimal("0.002"), - completion_price_unit=Decimal("1000"), + completion_price_unit=Decimal(1000), completion_price=Decimal("0.00004"), total_tokens=50, total_price=Decimal("0.00007"), @@ -219,11 +219,11 @@ def test_execute_llm_with_jinja2(flask_req_ctx, setup_code_executor_mock): mock_usage = LLMUsage( prompt_tokens=30, prompt_unit_price=Decimal("0.001"), - prompt_price_unit=Decimal("1000"), + prompt_price_unit=Decimal(1000), prompt_price=Decimal("0.00003"), completion_tokens=20, completion_unit_price=Decimal("0.002"), - completion_price_unit=Decimal("1000"), + completion_price_unit=Decimal(1000), completion_price=Decimal("0.00004"), total_tokens=50, total_price=Decimal("0.00007"), diff --git a/api/tests/unit_tests/libs/test_login.py b/api/tests/unit_tests/libs/test_login.py new file mode 100644 index 0000000000..39671077d4 --- /dev/null +++ b/api/tests/unit_tests/libs/test_login.py @@ -0,0 +1,232 @@ +from unittest.mock import MagicMock, patch + +import pytest +from flask import Flask, g +from flask_login import LoginManager, UserMixin + +from libs.login import _get_user, current_user, login_required + + +class MockUser(UserMixin): + """Mock user class for testing.""" + + def __init__(self, id: str, is_authenticated: bool = True): + self.id = id + self._is_authenticated = is_authenticated + + @property + def is_authenticated(self): + return self._is_authenticated + + +class TestLoginRequired: + """Test cases for login_required decorator.""" + + @pytest.fixture + def setup_app(self, app: Flask): + """Set up Flask app with login manager.""" + # Initialize login manager + login_manager = LoginManager() + login_manager.init_app(app) + + # Mock unauthorized handler + login_manager.unauthorized = MagicMock(return_value="Unauthorized") + + # Add a dummy user loader to prevent exceptions + @login_manager.user_loader + def load_user(user_id): + return None + + return app + + def test_authenticated_user_can_access_protected_view(self, setup_app: Flask): + """Test that authenticated users can access protected views.""" + + @login_required + def protected_view(): + return "Protected content" + + with setup_app.test_request_context(): + # Mock authenticated user + mock_user = MockUser("test_user", is_authenticated=True) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Protected content" + + def test_unauthenticated_user_cannot_access_protected_view(self, setup_app: Flask): + """Test that unauthenticated users are redirected.""" + + @login_required + def protected_view(): + return "Protected content" + + with setup_app.test_request_context(): + # Mock unauthenticated user + mock_user = MockUser("test_user", is_authenticated=False) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Unauthorized" + setup_app.login_manager.unauthorized.assert_called_once() + + def test_login_disabled_allows_unauthenticated_access(self, setup_app: Flask): + """Test that LOGIN_DISABLED config bypasses authentication.""" + + @login_required + def protected_view(): + return "Protected content" + + with setup_app.test_request_context(): + # Mock unauthenticated user and LOGIN_DISABLED + mock_user = MockUser("test_user", is_authenticated=False) + with patch("libs.login._get_user", return_value=mock_user): + with patch("libs.login.dify_config") as mock_config: + mock_config.LOGIN_DISABLED = True + + result = protected_view() + assert result == "Protected content" + # Ensure unauthorized was not called + setup_app.login_manager.unauthorized.assert_not_called() + + def test_options_request_bypasses_authentication(self, setup_app: Flask): + """Test that OPTIONS requests are exempt from authentication.""" + + @login_required + def protected_view(): + return "Protected content" + + with setup_app.test_request_context(method="OPTIONS"): + # Mock unauthenticated user + mock_user = MockUser("test_user", is_authenticated=False) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Protected content" + # Ensure unauthorized was not called + setup_app.login_manager.unauthorized.assert_not_called() + + def test_flask_2_compatibility(self, setup_app: Flask): + """Test Flask 2.x compatibility with ensure_sync.""" + + @login_required + def protected_view(): + return "Protected content" + + # Mock Flask 2.x ensure_sync + setup_app.ensure_sync = MagicMock(return_value=lambda: "Synced content") + + with setup_app.test_request_context(): + mock_user = MockUser("test_user", is_authenticated=True) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Synced content" + setup_app.ensure_sync.assert_called_once() + + def test_flask_1_compatibility(self, setup_app: Flask): + """Test Flask 1.x compatibility without ensure_sync.""" + + @login_required + def protected_view(): + return "Protected content" + + # Remove ensure_sync to simulate Flask 1.x + if hasattr(setup_app, "ensure_sync"): + delattr(setup_app, "ensure_sync") + + with setup_app.test_request_context(): + mock_user = MockUser("test_user", is_authenticated=True) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Protected content" + + +class TestGetUser: + """Test cases for _get_user function.""" + + def test_get_user_returns_user_from_g(self, app: Flask): + """Test that _get_user returns user from g._login_user.""" + mock_user = MockUser("test_user") + + with app.test_request_context(): + g._login_user = mock_user + user = _get_user() + assert user == mock_user + assert user.id == "test_user" + + def test_get_user_loads_user_if_not_in_g(self, app: Flask): + """Test that _get_user loads user if not already in g.""" + mock_user = MockUser("test_user") + + # Mock login manager + login_manager = MagicMock() + login_manager._load_user = MagicMock() + app.login_manager = login_manager + + with app.test_request_context(): + # Simulate _load_user setting g._login_user + def side_effect(): + g._login_user = mock_user + + login_manager._load_user.side_effect = side_effect + + user = _get_user() + assert user == mock_user + login_manager._load_user.assert_called_once() + + def test_get_user_returns_none_without_request_context(self, app: Flask): + """Test that _get_user returns None outside request context.""" + # Outside of request context + user = _get_user() + assert user is None + + +class TestCurrentUser: + """Test cases for current_user proxy.""" + + def test_current_user_proxy_returns_authenticated_user(self, app: Flask): + """Test that current_user proxy returns authenticated user.""" + mock_user = MockUser("test_user", is_authenticated=True) + + with app.test_request_context(): + with patch("libs.login._get_user", return_value=mock_user): + assert current_user.id == "test_user" + assert current_user.is_authenticated is True + + def test_current_user_proxy_returns_none_when_no_user(self, app: Flask): + """Test that current_user proxy handles None user.""" + with app.test_request_context(): + with patch("libs.login._get_user", return_value=None): + # When _get_user returns None, accessing attributes should fail + # or current_user should evaluate to falsy + try: + # Try to access an attribute that would exist on a real user + _ = current_user.id + pytest.fail("Should have raised AttributeError") + except AttributeError: + # This is expected when current_user is None + pass + + def test_current_user_proxy_thread_safety(self, app: Flask): + """Test that current_user proxy is thread-safe.""" + import threading + + results = {} + + def check_user_in_thread(user_id: str, index: int): + with app.test_request_context(): + mock_user = MockUser(user_id) + with patch("libs.login._get_user", return_value=mock_user): + results[index] = current_user.id + + # Create multiple threads with different users + threads = [] + for i in range(5): + thread = threading.Thread(target=check_user_in_thread, args=(f"user_{i}", i)) + threads.append(thread) + thread.start() + + # Wait for all threads to complete + for thread in threads: + thread.join() + + # Verify each thread got its own user + for i in range(5): + assert results[i] == f"user_{i}" diff --git a/api/tests/unit_tests/libs/test_passport.py b/api/tests/unit_tests/libs/test_passport.py new file mode 100644 index 0000000000..f33484c18d --- /dev/null +++ b/api/tests/unit_tests/libs/test_passport.py @@ -0,0 +1,205 @@ +from datetime import UTC, datetime, timedelta +from unittest.mock import patch + +import jwt +import pytest +from werkzeug.exceptions import Unauthorized + +from libs.passport import PassportService + + +class TestPassportService: + """Test PassportService JWT operations""" + + @pytest.fixture + def passport_service(self): + """Create PassportService instance with test secret key""" + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "test-secret-key-for-testing" + return PassportService() + + @pytest.fixture + def another_passport_service(self): + """Create another PassportService instance with different secret key""" + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "another-secret-key-for-testing" + return PassportService() + + # Core functionality tests + def test_should_issue_and_verify_token(self, passport_service): + """Test complete JWT lifecycle: issue and verify""" + payload = {"user_id": "123", "app_code": "test-app"} + token = passport_service.issue(payload) + + # Verify token format + assert isinstance(token, str) + assert len(token.split(".")) == 3 # JWT format: header.payload.signature + + # Verify token content + decoded = passport_service.verify(token) + assert decoded == payload + + def test_should_handle_different_payload_types(self, passport_service): + """Test issuing and verifying tokens with different payload types""" + test_cases = [ + {"string": "value"}, + {"number": 42}, + {"float": 3.14}, + {"boolean": True}, + {"null": None}, + {"array": [1, 2, 3]}, + {"nested": {"key": "value"}}, + {"unicode": "中文测试"}, + {"emoji": "🔐"}, + {}, # Empty payload + ] + + for payload in test_cases: + token = passport_service.issue(payload) + decoded = passport_service.verify(token) + assert decoded == payload + + # Security tests + def test_should_reject_modified_token(self, passport_service): + """Test that any modification to token invalidates it""" + token = passport_service.issue({"user": "test"}) + + # Test multiple modification points + test_positions = [0, len(token) // 3, len(token) // 2, len(token) - 1] + + for pos in test_positions: + if pos < len(token) and token[pos] != ".": + # Change one character + tampered = token[:pos] + ("X" if token[pos] != "X" else "Y") + token[pos + 1 :] + with pytest.raises(Unauthorized): + passport_service.verify(tampered) + + def test_should_reject_token_with_different_secret_key(self, passport_service, another_passport_service): + """Test key isolation - token from one service should not work with another""" + payload = {"user_id": "123", "app_code": "test-app"} + token = passport_service.issue(payload) + + with pytest.raises(Unauthorized) as exc_info: + another_passport_service.verify(token) + assert str(exc_info.value) == "401 Unauthorized: Invalid token signature." + + def test_should_use_hs256_algorithm(self, passport_service): + """Test that HS256 algorithm is used for signing""" + payload = {"test": "data"} + token = passport_service.issue(payload) + + # Decode header without relying on JWT internals + # Use jwt.get_unverified_header which is a public API + header = jwt.get_unverified_header(token) + assert header["alg"] == "HS256" + + def test_should_reject_token_with_wrong_algorithm(self, passport_service): + """Test rejection of token signed with different algorithm""" + payload = {"user_id": "123"} + + # Create token with different algorithm + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "test-secret-key-for-testing" + # Create token with HS512 instead of HS256 + wrong_alg_token = jwt.encode(payload, mock_config.SECRET_KEY, algorithm="HS512") + + # Should fail because service expects HS256 + # InvalidAlgorithmError is now caught by PyJWTError handler + with pytest.raises(Unauthorized) as exc_info: + passport_service.verify(wrong_alg_token) + assert str(exc_info.value) == "401 Unauthorized: Invalid token." + + # Exception handling tests + def test_should_handle_invalid_tokens(self, passport_service): + """Test handling of various invalid token formats""" + invalid_tokens = [ + ("not.a.token", "Invalid token."), + ("invalid-jwt-format", "Invalid token."), + ("xxx.yyy.zzz", "Invalid token."), + ("a.b", "Invalid token."), # Missing signature + ("", "Invalid token."), # Empty string + (" ", "Invalid token."), # Whitespace + (None, "Invalid token."), # None value + # Malformed base64 + ("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.INVALID_BASE64!@#$.signature", "Invalid token."), + ] + + for invalid_token, expected_message in invalid_tokens: + with pytest.raises(Unauthorized) as exc_info: + passport_service.verify(invalid_token) + assert expected_message in str(exc_info.value) + + def test_should_reject_expired_token(self, passport_service): + """Test rejection of expired token""" + past_time = datetime.now(UTC) - timedelta(hours=1) + payload = {"user_id": "123", "exp": past_time.timestamp()} + + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "test-secret-key-for-testing" + token = jwt.encode(payload, mock_config.SECRET_KEY, algorithm="HS256") + + with pytest.raises(Unauthorized) as exc_info: + passport_service.verify(token) + assert str(exc_info.value) == "401 Unauthorized: Token has expired." + + # Configuration tests + def test_should_handle_empty_secret_key(self): + """Test behavior when SECRET_KEY is empty""" + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "" + service = PassportService() + + # Empty secret key should still work but is insecure + payload = {"test": "data"} + token = service.issue(payload) + decoded = service.verify(token) + assert decoded == payload + + def test_should_handle_none_secret_key(self): + """Test behavior when SECRET_KEY is None""" + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = None + service = PassportService() + + payload = {"test": "data"} + # JWT library will raise TypeError when secret is None + with pytest.raises((TypeError, jwt.exceptions.InvalidKeyError)): + service.issue(payload) + + # Boundary condition tests + def test_should_handle_large_payload(self, passport_service): + """Test handling of large payload""" + # Test with 100KB instead of 1MB for faster tests + large_data = "x" * (100 * 1024) + payload = {"data": large_data} + + token = passport_service.issue(payload) + decoded = passport_service.verify(token) + + assert decoded["data"] == large_data + + def test_should_handle_special_characters_in_payload(self, passport_service): + """Test handling of special characters in payload""" + special_payloads = [ + {"special": "!@#$%^&*()"}, + {"quotes": 'He said "Hello"'}, + {"backslash": "path\\to\\file"}, + {"newline": "line1\nline2"}, + {"unicode": "🔐🔑🛡️"}, + {"mixed": "Test123!@#中文🔐"}, + ] + + for payload in special_payloads: + token = passport_service.issue(payload) + decoded = passport_service.verify(token) + assert decoded == payload + + def test_should_catch_generic_pyjwt_errors(self, passport_service): + """Test that generic PyJWTError exceptions are caught and converted to Unauthorized""" + # Mock jwt.decode to raise a generic PyJWTError + with patch("libs.passport.jwt.decode") as mock_decode: + mock_decode.side_effect = jwt.exceptions.PyJWTError("Generic JWT error") + + with pytest.raises(Unauthorized) as exc_info: + passport_service.verify("some-token") + assert str(exc_info.value) == "401 Unauthorized: Invalid token." diff --git a/api/tests/unit_tests/services/services_test_help.py b/api/tests/unit_tests/services/services_test_help.py new file mode 100644 index 0000000000..c6b962f7fc --- /dev/null +++ b/api/tests/unit_tests/services/services_test_help.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock + + +class ServiceDbTestHelper: + """ + Helper class for service database query tests. + """ + + @staticmethod + def setup_db_query_filter_by_mock(mock_db, query_results): + """ + Smart database query mock that responds based on model type and query parameters. + + Args: + mock_db: Mock database session + query_results: Dict mapping (model_name, filter_key, filter_value) to return value + Example: {('Account', 'email', 'test@example.com'): mock_account} + """ + + def query_side_effect(model): + mock_query = MagicMock() + + def filter_by_side_effect(**kwargs): + mock_filter_result = MagicMock() + + def first_side_effect(): + # Find matching result based on model and filter parameters + for (model_name, filter_key, filter_value), result in query_results.items(): + if model.__name__ == model_name and filter_key in kwargs and kwargs[filter_key] == filter_value: + return result + return None + + mock_filter_result.first.side_effect = first_side_effect + + # Handle order_by calls for complex queries + def order_by_side_effect(*args, **kwargs): + mock_order_result = MagicMock() + + def order_first_side_effect(): + # Look for order_by results in the same query_results dict + for (model_name, filter_key, filter_value), result in query_results.items(): + if ( + model.__name__ == model_name + and filter_key == "order_by" + and filter_value == "first_available" + ): + return result + return None + + mock_order_result.first.side_effect = order_first_side_effect + return mock_order_result + + mock_filter_result.order_by.side_effect = order_by_side_effect + return mock_filter_result + + mock_query.filter_by.side_effect = filter_by_side_effect + return mock_query + + mock_db.session.query.side_effect = query_side_effect diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py new file mode 100644 index 0000000000..13900ab6d1 --- /dev/null +++ b/api/tests/unit_tests/services/test_account_service.py @@ -0,0 +1,1545 @@ +import json +from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch + +import pytest + +from configs import dify_config +from models.account import Account +from services.account_service import AccountService, RegisterService, TenantService +from services.errors.account import ( + AccountAlreadyInTenantError, + AccountLoginError, + AccountNotFoundError, + AccountPasswordError, + AccountRegisterError, + CurrentPasswordIncorrectError, +) +from tests.unit_tests.services.services_test_help import ServiceDbTestHelper + + +class TestAccountAssociatedDataFactory: + """Factory class for creating test data and mock objects for account service tests.""" + + @staticmethod + def create_account_mock( + account_id: str = "user-123", + email: str = "test@example.com", + name: str = "Test User", + status: str = "active", + password: str = "hashed_password", + password_salt: str = "salt", + interface_language: str = "en-US", + interface_theme: str = "light", + timezone: str = "UTC", + **kwargs, + ) -> MagicMock: + """Create a mock account with specified attributes.""" + account = MagicMock(spec=Account) + account.id = account_id + account.email = email + account.name = name + account.status = status + account.password = password + account.password_salt = password_salt + account.interface_language = interface_language + account.interface_theme = interface_theme + account.timezone = timezone + # Set last_active_at to a datetime object that's older than 10 minutes + account.last_active_at = datetime.now() - timedelta(minutes=15) + account.initialized_at = None + for key, value in kwargs.items(): + setattr(account, key, value) + return account + + @staticmethod + def create_tenant_join_mock( + tenant_id: str = "tenant-456", + account_id: str = "user-123", + current: bool = True, + role: str = "normal", + **kwargs, + ) -> MagicMock: + """Create a mock tenant account join record.""" + tenant_join = MagicMock() + tenant_join.tenant_id = tenant_id + tenant_join.account_id = account_id + tenant_join.current = current + tenant_join.role = role + for key, value in kwargs.items(): + setattr(tenant_join, key, value) + return tenant_join + + @staticmethod + def create_feature_service_mock(allow_register: bool = True): + """Create a mock feature service.""" + mock_service = MagicMock() + mock_service.get_system_features.return_value.is_allow_register = allow_register + return mock_service + + @staticmethod + def create_billing_service_mock(email_frozen: bool = False): + """Create a mock billing service.""" + mock_service = MagicMock() + mock_service.is_email_in_freeze.return_value = email_frozen + return mock_service + + +class TestAccountService: + """ + Comprehensive unit tests for AccountService methods. + + This test suite covers all account-related operations including: + - Authentication and login + - Account creation and registration + - Password management + - JWT token generation + - User loading and tenant management + - Error conditions and edge cases + """ + + @pytest.fixture + def mock_db_dependencies(self): + """Common mock setup for database dependencies.""" + with patch("services.account_service.db") as mock_db: + mock_db.session.add = MagicMock() + mock_db.session.commit = MagicMock() + yield { + "db": mock_db, + } + + @pytest.fixture + def mock_password_dependencies(self): + """Mock setup for password-related functions.""" + with ( + patch("services.account_service.compare_password") as mock_compare_password, + patch("services.account_service.hash_password") as mock_hash_password, + patch("services.account_service.valid_password") as mock_valid_password, + ): + yield { + "compare_password": mock_compare_password, + "hash_password": mock_hash_password, + "valid_password": mock_valid_password, + } + + @pytest.fixture + def mock_external_service_dependencies(self): + """Mock setup for external service dependencies.""" + with ( + patch("services.account_service.FeatureService") as mock_feature_service, + patch("services.account_service.BillingService") as mock_billing_service, + patch("services.account_service.PassportService") as mock_passport_service, + ): + yield { + "feature_service": mock_feature_service, + "billing_service": mock_billing_service, + "passport_service": mock_passport_service, + } + + @pytest.fixture + def mock_db_with_autospec(self): + """ + Mock database with autospec for more realistic behavior. + This approach preserves the actual method signatures and behavior. + """ + with patch("services.account_service.db", autospec=True) as mock_db: + # Create a more realistic session mock + mock_session = MagicMock() + mock_db.session = mock_session + + # Setup basic session methods + mock_session.add = MagicMock() + mock_session.commit = MagicMock() + mock_session.query = MagicMock() + + yield mock_db + + def _assert_database_operations_called(self, mock_db): + """Helper method to verify database operations were called.""" + mock_db.session.commit.assert_called() + + def _assert_database_operations_not_called(self, mock_db): + """Helper method to verify database operations were not called.""" + mock_db.session.commit.assert_not_called() + + def _assert_exception_raised(self, exception_type, callable_func, *args, **kwargs): + """Helper method to verify that specific exception is raised.""" + with pytest.raises(exception_type): + callable_func(*args, **kwargs) + + # ==================== Authentication Tests ==================== + + def test_authenticate_success(self, mock_db_dependencies, mock_password_dependencies): + """Test successful authentication with correct email and password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock + query_results = {("Account", "email", "test@example.com"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + mock_password_dependencies["compare_password"].return_value = True + + # Execute test + result = AccountService.authenticate("test@example.com", "password") + + # Verify results + assert result == mock_account + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_authenticate_account_not_found(self, mock_db_dependencies): + """Test authentication when account does not exist.""" + # Setup smart database query mock - no matching results + query_results = {("Account", "email", "notfound@example.com"): None} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test and verify exception + self._assert_exception_raised( + AccountNotFoundError, AccountService.authenticate, "notfound@example.com", "password" + ) + + def test_authenticate_account_banned(self, mock_db_dependencies): + """Test authentication when account is banned.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned") + + # Setup smart database query mock + query_results = {("Account", "email", "banned@example.com"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test and verify exception + self._assert_exception_raised(AccountLoginError, AccountService.authenticate, "banned@example.com", "password") + + def test_authenticate_password_error(self, mock_db_dependencies, mock_password_dependencies): + """Test authentication with wrong password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock + query_results = {("Account", "email", "test@example.com"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + mock_password_dependencies["compare_password"].return_value = False + + # Execute test and verify exception + self._assert_exception_raised( + AccountPasswordError, AccountService.authenticate, "test@example.com", "wrongpassword" + ) + + def test_authenticate_pending_account_activates(self, mock_db_dependencies, mock_password_dependencies): + """Test authentication for a pending account, which should activate on login.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="pending") + + # Setup smart database query mock + query_results = {("Account", "email", "pending@example.com"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + mock_password_dependencies["compare_password"].return_value = True + + # Execute test + result = AccountService.authenticate("pending@example.com", "password") + + # Verify results + assert result == mock_account + assert mock_account.status == "active" + self._assert_database_operations_called(mock_db_dependencies["db"]) + + # ==================== Account Creation Tests ==================== + + def test_create_account_success( + self, mock_db_dependencies, mock_password_dependencies, mock_external_service_dependencies + ): + """Test successful account creation with all required parameters.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + mock_password_dependencies["hash_password"].return_value = b"hashed_password" + + # Execute test + result = AccountService.create_account( + email="test@example.com", + name="Test User", + interface_language="en-US", + password="password123", + interface_theme="light", + ) + + # Verify results + assert result.email == "test@example.com" + assert result.name == "Test User" + assert result.interface_language == "en-US" + assert result.interface_theme == "light" + assert result.password is not None + assert result.password_salt is not None + assert result.timezone is not None + + # Verify database operations + mock_db_dependencies["db"].session.add.assert_called_once() + added_account = mock_db_dependencies["db"].session.add.call_args[0][0] + assert added_account.email == "test@example.com" + assert added_account.name == "Test User" + assert added_account.interface_language == "en-US" + assert added_account.interface_theme == "light" + assert added_account.password is not None + assert added_account.password_salt is not None + assert added_account.timezone is not None + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_create_account_registration_disabled(self, mock_external_service_dependencies): + """Test account creation when registration is disabled.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = False + + # Execute test and verify exception + self._assert_exception_raised( + Exception, # AccountNotFound + AccountService.create_account, + email="test@example.com", + name="Test User", + interface_language="en-US", + ) + + def test_create_account_email_frozen(self, mock_db_dependencies, mock_external_service_dependencies): + """Test account creation with frozen email address.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = True + dify_config.BILLING_ENABLED = True + + # Execute test and verify exception + self._assert_exception_raised( + AccountRegisterError, + AccountService.create_account, + email="frozen@example.com", + name="Test User", + interface_language="en-US", + ) + dify_config.BILLING_ENABLED = False + + def test_create_account_without_password(self, mock_db_dependencies, mock_external_service_dependencies): + """Test account creation without password (for invite-based registration).""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Execute test + result = AccountService.create_account( + email="test@example.com", + name="Test User", + interface_language="zh-CN", + password=None, + interface_theme="dark", + ) + + # Verify results + assert result.email == "test@example.com" + assert result.name == "Test User" + assert result.interface_language == "zh-CN" + assert result.interface_theme == "dark" + assert result.password is None + assert result.password_salt is None + assert result.timezone is not None + + # Verify database operations + mock_db_dependencies["db"].session.add.assert_called_once() + added_account = mock_db_dependencies["db"].session.add.call_args[0][0] + assert added_account.email == "test@example.com" + assert added_account.name == "Test User" + assert added_account.interface_language == "zh-CN" + assert added_account.interface_theme == "dark" + assert added_account.password is None + assert added_account.password_salt is None + assert added_account.timezone is not None + self._assert_database_operations_called(mock_db_dependencies["db"]) + + # ==================== Password Management Tests ==================== + + def test_update_account_password_success(self, mock_db_dependencies, mock_password_dependencies): + """Test successful password update with correct current password and valid new password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_password_dependencies["compare_password"].return_value = True + mock_password_dependencies["valid_password"].return_value = None + mock_password_dependencies["hash_password"].return_value = b"new_hashed_password" + + # Execute test + result = AccountService.update_account_password(mock_account, "old_password", "new_password123") + + # Verify results + assert result == mock_account + assert mock_account.password is not None + assert mock_account.password_salt is not None + + # Verify password validation was called + mock_password_dependencies["compare_password"].assert_called_once_with( + "old_password", "hashed_password", "salt" + ) + mock_password_dependencies["valid_password"].assert_called_once_with("new_password123") + + # Verify database operations + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_update_account_password_current_password_incorrect(self, mock_password_dependencies): + """Test password update with incorrect current password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_password_dependencies["compare_password"].return_value = False + + # Execute test and verify exception + self._assert_exception_raised( + CurrentPasswordIncorrectError, + AccountService.update_account_password, + mock_account, + "wrong_password", + "new_password123", + ) + + # Verify password comparison was called + mock_password_dependencies["compare_password"].assert_called_once_with( + "wrong_password", "hashed_password", "salt" + ) + + def test_update_account_password_invalid_new_password(self, mock_password_dependencies): + """Test password update with invalid new password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_password_dependencies["compare_password"].return_value = True + mock_password_dependencies["valid_password"].side_effect = ValueError("Password too short") + + # Execute test and verify exception + self._assert_exception_raised( + ValueError, AccountService.update_account_password, mock_account, "old_password", "short" + ) + + # Verify password validation was called + mock_password_dependencies["valid_password"].assert_called_once_with("short") + + # ==================== User Loading Tests ==================== + + def test_load_user_success(self, mock_db_dependencies): + """Test successful user loading with current tenant.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_tenant_join = TestAccountAssociatedDataFactory.create_tenant_join_mock() + + # Setup smart database query mock + query_results = { + ("Account", "id", "user-123"): mock_account, + ("TenantAccountJoin", "account_id", "user-123"): mock_tenant_join, + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock datetime + with patch("services.account_service.datetime") as mock_datetime: + mock_now = datetime.now() + mock_datetime.now.return_value = mock_now + mock_datetime.UTC = "UTC" + + # Execute test + result = AccountService.load_user("user-123") + + # Verify results + assert result == mock_account + assert mock_account.set_tenant_id.called + + def test_load_user_not_found(self, mock_db_dependencies): + """Test user loading when user does not exist.""" + # Setup smart database query mock - no matching results + query_results = {("Account", "id", "non-existent-user"): None} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test + result = AccountService.load_user("non-existent-user") + + # Verify results + assert result is None + + def test_load_user_banned(self, mock_db_dependencies): + """Test user loading when user is banned.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned") + + # Setup smart database query mock + query_results = {("Account", "id", "user-123"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test and verify exception + self._assert_exception_raised( + Exception, # Unauthorized + AccountService.load_user, + "user-123", + ) + + def test_load_user_no_current_tenant(self, mock_db_dependencies): + """Test user loading when user has no current tenant but has available tenants.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_available_tenant = TestAccountAssociatedDataFactory.create_tenant_join_mock(current=False) + + # Setup smart database query mock for complex scenario + query_results = { + ("Account", "id", "user-123"): mock_account, + ("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant + ("TenantAccountJoin", "order_by", "first_available"): mock_available_tenant, # First available tenant + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock datetime + with patch("services.account_service.datetime") as mock_datetime: + mock_now = datetime.now() + mock_datetime.now.return_value = mock_now + mock_datetime.UTC = "UTC" + + # Execute test + result = AccountService.load_user("user-123") + + # Verify results + assert result == mock_account + assert mock_available_tenant.current is True + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_load_user_no_tenants(self, mock_db_dependencies): + """Test user loading when user has no tenants at all.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock for no tenants scenario + query_results = { + ("Account", "id", "user-123"): mock_account, + ("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant + ("TenantAccountJoin", "order_by", "first_available"): None, # No available tenants + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock datetime + with patch("services.account_service.datetime") as mock_datetime: + mock_now = datetime.now() + mock_datetime.now.return_value = mock_now + mock_datetime.UTC = "UTC" + + # Execute test + result = AccountService.load_user("user-123") + + # Verify results + assert result is None + + +class TestTenantService: + """ + Comprehensive unit tests for TenantService methods. + + This test suite covers all tenant-related operations including: + - Tenant creation and management + - Member management and permissions + - Tenant switching + - Role updates and permission checks + - Error conditions and edge cases + """ + + @pytest.fixture + def mock_db_dependencies(self): + """Common mock setup for database dependencies.""" + with patch("services.account_service.db") as mock_db: + mock_db.session.add = MagicMock() + mock_db.session.commit = MagicMock() + yield { + "db": mock_db, + } + + @pytest.fixture + def mock_rsa_dependencies(self): + """Mock setup for RSA-related functions.""" + with patch("services.account_service.generate_key_pair") as mock_generate_key_pair: + yield mock_generate_key_pair + + @pytest.fixture + def mock_external_service_dependencies(self): + """Mock setup for external service dependencies.""" + with ( + patch("services.account_service.FeatureService") as mock_feature_service, + patch("services.account_service.BillingService") as mock_billing_service, + ): + yield { + "feature_service": mock_feature_service, + "billing_service": mock_billing_service, + } + + def _assert_database_operations_called(self, mock_db): + """Helper method to verify database operations were called.""" + mock_db.session.commit.assert_called() + + def _assert_exception_raised(self, exception_type, callable_func, *args, **kwargs): + """Helper method to verify that specific exception is raised.""" + with pytest.raises(exception_type): + callable_func(*args, **kwargs) + + # ==================== Tenant Creation Tests ==================== + + def test_create_owner_tenant_if_not_exist_new_user( + self, mock_db_dependencies, mock_rsa_dependencies, mock_external_service_dependencies + ): + """Test creating owner tenant for new user without existing tenants.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock - no existing tenant joins + query_results = { + ("TenantAccountJoin", "account_id", "user-123"): None, + ("TenantAccountJoin", "tenant_id", "tenant-456"): None, # For has_roles check + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Setup external service mocks + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + + # Mock tenant creation + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.name = "Test User's Workspace" + + # Mock database operations + mock_db_dependencies["db"].session.add = MagicMock() + + # Mock RSA key generation + mock_rsa_dependencies.return_value = "mock_public_key" + + # Mock has_roles method to return False (no existing owner) + with patch("services.account_service.TenantService.has_roles") as mock_has_roles: + mock_has_roles.return_value = False + + # Mock Tenant creation to set proper ID + with patch("services.account_service.Tenant") as mock_tenant_class: + mock_tenant_instance = MagicMock() + mock_tenant_instance.id = "tenant-456" + mock_tenant_instance.name = "Test User's Workspace" + mock_tenant_class.return_value = mock_tenant_instance + + # Execute test + TenantService.create_owner_tenant_if_not_exist(mock_account) + + # Verify tenant was created with correct parameters + mock_db_dependencies["db"].session.add.assert_called() + + # Get all calls to session.add + add_calls = mock_db_dependencies["db"].session.add.call_args_list + + # Should have at least 2 calls: one for Tenant, one for TenantAccountJoin + assert len(add_calls) >= 2 + + # Verify Tenant was added with correct name + tenant_added = False + tenant_account_join_added = False + + for call in add_calls: + added_object = call[0][0] # First argument of the call + + # Check if it's a Tenant object + if hasattr(added_object, "name") and hasattr(added_object, "id"): + # This should be a Tenant object + assert added_object.name == "Test User's Workspace" + tenant_added = True + + # Check if it's a TenantAccountJoin object + elif ( + hasattr(added_object, "tenant_id") + and hasattr(added_object, "account_id") + and hasattr(added_object, "role") + ): + # This should be a TenantAccountJoin object + assert added_object.tenant_id is not None + assert added_object.account_id == "user-123" + assert added_object.role == "owner" + tenant_account_join_added = True + + assert tenant_added, "Tenant object was not added to database" + assert tenant_account_join_added, "TenantAccountJoin object was not added to database" + + self._assert_database_operations_called(mock_db_dependencies["db"]) + assert mock_rsa_dependencies.called, "RSA key generation was not called" + + # ==================== Member Management Tests ==================== + + def test_create_tenant_member_success(self, mock_db_dependencies): + """Test successful tenant member creation.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock - no existing member + query_results = {("TenantAccountJoin", "tenant_id", "tenant-456"): None} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock database operations + mock_db_dependencies["db"].session.add = MagicMock() + + # Execute test + result = TenantService.create_tenant_member(mock_tenant, mock_account, "normal") + + # Verify member was created with correct parameters + assert result is not None + mock_db_dependencies["db"].session.add.assert_called_once() + + # Verify the TenantAccountJoin object was added with correct parameters + added_tenant_account_join = mock_db_dependencies["db"].session.add.call_args[0][0] + assert added_tenant_account_join.tenant_id == "tenant-456" + assert added_tenant_account_join.account_id == "user-123" + assert added_tenant_account_join.role == "normal" + + self._assert_database_operations_called(mock_db_dependencies["db"]) + + # ==================== Tenant Switching Tests ==================== + + def test_switch_tenant_success(self): + """Test successful tenant switching.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_tenant_join = TestAccountAssociatedDataFactory.create_tenant_join_mock( + tenant_id="tenant-456", account_id="user-123", current=False + ) + + # Mock the complex query in switch_tenant method + with patch("services.account_service.db") as mock_db: + # Mock the join query that returns the tenant_account_join + mock_query = MagicMock() + mock_filter = MagicMock() + mock_filter.first.return_value = mock_tenant_join + mock_query.filter.return_value = mock_filter + mock_query.join.return_value = mock_query + mock_db.session.query.return_value = mock_query + + # Execute test + TenantService.switch_tenant(mock_account, "tenant-456") + + # Verify tenant was switched + assert mock_tenant_join.current is True + self._assert_database_operations_called(mock_db) + + def test_switch_tenant_no_tenant_id(self): + """Test tenant switching without providing tenant ID.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Execute test and verify exception + self._assert_exception_raised(ValueError, TenantService.switch_tenant, mock_account, None) + + # ==================== Role Management Tests ==================== + + def test_update_member_role_success(self): + """Test successful member role update.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_member = TestAccountAssociatedDataFactory.create_account_mock(account_id="member-789") + mock_operator = TestAccountAssociatedDataFactory.create_account_mock(account_id="operator-123") + mock_target_join = TestAccountAssociatedDataFactory.create_tenant_join_mock( + tenant_id="tenant-456", account_id="member-789", role="normal" + ) + mock_operator_join = TestAccountAssociatedDataFactory.create_tenant_join_mock( + tenant_id="tenant-456", account_id="operator-123", role="owner" + ) + + # Mock the database queries in update_member_role method + with patch("services.account_service.db") as mock_db: + # Mock the first query for operator permission check + mock_query1 = MagicMock() + mock_filter1 = MagicMock() + mock_filter1.first.return_value = mock_operator_join + mock_query1.filter_by.return_value = mock_filter1 + + # Mock the second query for target member + mock_query2 = MagicMock() + mock_filter2 = MagicMock() + mock_filter2.first.return_value = mock_target_join + mock_query2.filter_by.return_value = mock_filter2 + + # Make the query method return different mocks for different calls + mock_db.session.query.side_effect = [mock_query1, mock_query2] + + # Execute test + TenantService.update_member_role(mock_tenant, mock_member, "admin", mock_operator) + + # Verify role was updated + assert mock_target_join.role == "admin" + self._assert_database_operations_called(mock_db) + + # ==================== Permission Check Tests ==================== + + def test_check_member_permission_success(self, mock_db_dependencies): + """Test successful member permission check.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_operator = TestAccountAssociatedDataFactory.create_account_mock(account_id="operator-123") + mock_member = TestAccountAssociatedDataFactory.create_account_mock(account_id="member-789") + mock_operator_join = TestAccountAssociatedDataFactory.create_tenant_join_mock( + tenant_id="tenant-456", account_id="operator-123", role="owner" + ) + + # Setup smart database query mock + query_results = {("TenantAccountJoin", "tenant_id", "tenant-456"): mock_operator_join} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test - should not raise exception + TenantService.check_member_permission(mock_tenant, mock_operator, mock_member, "add") + + def test_check_member_permission_operate_self(self): + """Test member permission check when operator tries to operate self.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_operator = TestAccountAssociatedDataFactory.create_account_mock(account_id="operator-123") + + # Execute test and verify exception + from services.errors.account import CannotOperateSelfError + + self._assert_exception_raised( + CannotOperateSelfError, + TenantService.check_member_permission, + mock_tenant, + mock_operator, + mock_operator, # Same as operator + "add", + ) + + +class TestRegisterService: + """ + Comprehensive unit tests for RegisterService methods. + + This test suite covers all registration-related operations including: + - System setup + - Account registration + - Member invitation + - Token management + - Invitation validation + - Error conditions and edge cases + """ + + @pytest.fixture + def mock_db_dependencies(self): + """Common mock setup for database dependencies.""" + with patch("services.account_service.db") as mock_db: + mock_db.session.add = MagicMock() + mock_db.session.commit = MagicMock() + mock_db.session.begin_nested = MagicMock() + mock_db.session.rollback = MagicMock() + yield { + "db": mock_db, + } + + @pytest.fixture + def mock_redis_dependencies(self): + """Mock setup for Redis-related functions.""" + with patch("services.account_service.redis_client") as mock_redis: + yield mock_redis + + @pytest.fixture + def mock_external_service_dependencies(self): + """Mock setup for external service dependencies.""" + with ( + patch("services.account_service.FeatureService") as mock_feature_service, + patch("services.account_service.BillingService") as mock_billing_service, + patch("services.account_service.PassportService") as mock_passport_service, + ): + yield { + "feature_service": mock_feature_service, + "billing_service": mock_billing_service, + "passport_service": mock_passport_service, + } + + @pytest.fixture + def mock_task_dependencies(self): + """Mock setup for task dependencies.""" + with patch("services.account_service.send_invite_member_mail_task") as mock_send_mail: + yield mock_send_mail + + def _assert_database_operations_called(self, mock_db): + """Helper method to verify database operations were called.""" + mock_db.session.commit.assert_called() + + def _assert_database_operations_not_called(self, mock_db): + """Helper method to verify database operations were not called.""" + mock_db.session.commit.assert_not_called() + + def _assert_exception_raised(self, exception_type, callable_func, *args, **kwargs): + """Helper method to verify that specific exception is raised.""" + with pytest.raises(exception_type): + callable_func(*args, **kwargs) + + # ==================== Setup Tests ==================== + + def test_setup_success(self, mock_db_dependencies, mock_external_service_dependencies): + """Test successful system setup.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.return_value = mock_account + + # Mock TenantService.create_owner_tenant_if_not_exist + with patch("services.account_service.TenantService.create_owner_tenant_if_not_exist") as mock_create_tenant: + # Mock DifySetup + with patch("services.account_service.DifySetup") as mock_dify_setup: + mock_dify_setup_instance = MagicMock() + mock_dify_setup.return_value = mock_dify_setup_instance + + # Execute test + RegisterService.setup("admin@example.com", "Admin User", "password123", "192.168.1.1") + + # Verify results + mock_create_account.assert_called_once_with( + email="admin@example.com", + name="Admin User", + interface_language="en-US", + password="password123", + is_setup=True, + ) + mock_create_tenant.assert_called_once_with(account=mock_account, is_setup=True) + mock_dify_setup.assert_called_once() + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_setup_failure_rollback(self, mock_db_dependencies, mock_external_service_dependencies): + """Test setup failure with proper rollback.""" + # Setup mocks to simulate failure + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account to raise exception + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.side_effect = Exception("Database error") + + # Execute test and verify exception + self._assert_exception_raised( + ValueError, + RegisterService.setup, + "admin@example.com", + "Admin User", + "password123", + "192.168.1.1", + ) + + # Verify rollback operations were called + mock_db_dependencies["db"].session.query.assert_called() + + # ==================== Registration Tests ==================== + + def test_register_success(self, mock_db_dependencies, mock_external_service_dependencies): + """Test successful account registration.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.return_value = mock_account + + # Mock TenantService.create_tenant and create_tenant_member + with ( + patch("services.account_service.TenantService.create_tenant") as mock_create_tenant, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.tenant_was_created") as mock_event, + ): + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_create_tenant.return_value = mock_tenant + + # Execute test + result = RegisterService.register( + email="test@example.com", + name="Test User", + password="password123", + language="en-US", + ) + + # Verify results + assert result == mock_account + assert result.status == "active" + assert result.initialized_at is not None + mock_create_account.assert_called_once_with( + email="test@example.com", + name="Test User", + interface_language="en-US", + password="password123", + is_setup=False, + ) + mock_create_tenant.assert_called_once_with("Test User's Workspace") + mock_create_member.assert_called_once_with(mock_tenant, mock_account, role="owner") + mock_event.send.assert_called_once_with(mock_tenant) + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_register_with_oauth(self, mock_db_dependencies, mock_external_service_dependencies): + """Test account registration with OAuth integration.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account and link_account_integrate + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with ( + patch("services.account_service.AccountService.create_account") as mock_create_account, + patch("services.account_service.AccountService.link_account_integrate") as mock_link_account, + ): + mock_create_account.return_value = mock_account + + # Mock TenantService methods + with ( + patch("services.account_service.TenantService.create_tenant") as mock_create_tenant, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.tenant_was_created") as mock_event, + ): + mock_tenant = MagicMock() + mock_create_tenant.return_value = mock_tenant + + # Execute test + result = RegisterService.register( + email="test@example.com", + name="Test User", + password=None, + open_id="oauth123", + provider="google", + language="en-US", + ) + + # Verify results + assert result == mock_account + mock_link_account.assert_called_once_with("google", "oauth123", mock_account) + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_register_with_pending_status(self, mock_db_dependencies, mock_external_service_dependencies): + """Test account registration with pending status.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.return_value = mock_account + + # Mock TenantService methods + with ( + patch("services.account_service.TenantService.create_tenant") as mock_create_tenant, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.tenant_was_created") as mock_event, + ): + mock_tenant = MagicMock() + mock_create_tenant.return_value = mock_tenant + + # Execute test with pending status + from models.account import AccountStatus + + result = RegisterService.register( + email="test@example.com", + name="Test User", + password="password123", + language="en-US", + status=AccountStatus.PENDING, + ) + + # Verify results + assert result == mock_account + assert result.status == "pending" + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_register_workspace_not_allowed(self, mock_db_dependencies, mock_external_service_dependencies): + """Test registration when workspace creation is not allowed.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.return_value = mock_account + + # Execute test and verify exception + from services.errors.workspace import WorkSpaceNotAllowedCreateError + + with patch("services.account_service.TenantService.create_tenant") as mock_create_tenant: + mock_create_tenant.side_effect = WorkSpaceNotAllowedCreateError() + + self._assert_exception_raised( + AccountRegisterError, + RegisterService.register, + email="test@example.com", + name="Test User", + password="password123", + language="en-US", + ) + + # Verify rollback was called + mock_db_dependencies["db"].session.rollback.assert_called() + + def test_register_general_exception(self, mock_db_dependencies, mock_external_service_dependencies): + """Test registration with general exception handling.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account to raise exception + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.side_effect = Exception("Unexpected error") + + # Execute test and verify exception + self._assert_exception_raised( + AccountRegisterError, + RegisterService.register, + email="test@example.com", + name="Test User", + password="password123", + language="en-US", + ) + + # Verify rollback was called + mock_db_dependencies["db"].session.rollback.assert_called() + + # ==================== Member Invitation Tests ==================== + + def test_invite_new_member_new_account(self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies): + """Test inviting a new member who doesn't have an account.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.name = "Test Workspace" + mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") + + # Mock database queries - need to mock the Session query + mock_session = MagicMock() + mock_session.query.return_value.filter_by.return_value.first.return_value = None # No existing account + + with patch("services.account_service.Session") as mock_session_class: + mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.__exit__.return_value = None + + # Mock RegisterService.register + mock_new_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="new-user-456", email="newuser@example.com", name="newuser", status="pending" + ) + with patch("services.account_service.RegisterService.register") as mock_register: + mock_register.return_value = mock_new_account + + # Mock TenantService methods + with ( + patch("services.account_service.TenantService.check_member_permission") as mock_check_permission, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.TenantService.switch_tenant") as mock_switch_tenant, + patch("services.account_service.RegisterService.generate_invite_token") as mock_generate_token, + ): + mock_generate_token.return_value = "invite-token-123" + + # Execute test + result = RegisterService.invite_new_member( + tenant=mock_tenant, + email="newuser@example.com", + language="en-US", + role="normal", + inviter=mock_inviter, + ) + + # Verify results + assert result == "invite-token-123" + mock_register.assert_called_once_with( + email="newuser@example.com", + name="newuser", + language="en-US", + status="pending", + is_setup=True, + ) + mock_create_member.assert_called_once_with(mock_tenant, mock_new_account, "normal") + mock_switch_tenant.assert_called_once_with(mock_new_account, mock_tenant.id) + mock_generate_token.assert_called_once_with(mock_tenant, mock_new_account) + mock_task_dependencies.delay.assert_called_once() + + def test_invite_new_member_existing_account( + self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies + ): + """Test inviting a new member who already has an account.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.name = "Test Workspace" + mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") + mock_existing_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="existing-user-456", email="existing@example.com", status="pending" + ) + + # Mock database queries - need to mock the Session query + mock_session = MagicMock() + mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing_account + + with patch("services.account_service.Session") as mock_session_class: + mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.__exit__.return_value = None + + # Mock the db.session.query for TenantAccountJoin + mock_db_query = MagicMock() + mock_db_query.filter_by.return_value.first.return_value = None # No existing member + mock_db_dependencies["db"].session.query.return_value = mock_db_query + + # Mock TenantService methods + with ( + patch("services.account_service.TenantService.check_member_permission") as mock_check_permission, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.RegisterService.generate_invite_token") as mock_generate_token, + ): + mock_generate_token.return_value = "invite-token-123" + + # Execute test + result = RegisterService.invite_new_member( + tenant=mock_tenant, + email="existing@example.com", + language="en-US", + role="normal", + inviter=mock_inviter, + ) + + # Verify results + assert result == "invite-token-123" + mock_create_member.assert_called_once_with(mock_tenant, mock_existing_account, "normal") + mock_generate_token.assert_called_once_with(mock_tenant, mock_existing_account) + mock_task_dependencies.delay.assert_called_once() + + def test_invite_new_member_already_in_tenant(self, mock_db_dependencies, mock_redis_dependencies): + """Test inviting a member who is already in the tenant.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") + mock_existing_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="existing-user-456", email="existing@example.com", status="active" + ) + + # Mock database queries + query_results = { + ("Account", "email", "existing@example.com"): mock_existing_account, + ( + "TenantAccountJoin", + "tenant_id", + "tenant-456", + ): TestAccountAssociatedDataFactory.create_tenant_join_mock(), + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock TenantService methods + with patch("services.account_service.TenantService.check_member_permission") as mock_check_permission: + # Execute test and verify exception + self._assert_exception_raised( + AccountAlreadyInTenantError, + RegisterService.invite_new_member, + tenant=mock_tenant, + email="existing@example.com", + language="en-US", + role="normal", + inviter=mock_inviter, + ) + + def test_invite_new_member_no_inviter(self): + """Test inviting a member without providing an inviter.""" + # Setup test data + mock_tenant = MagicMock() + + # Execute test and verify exception + self._assert_exception_raised( + ValueError, + RegisterService.invite_new_member, + tenant=mock_tenant, + email="test@example.com", + language="en-US", + role="normal", + inviter=None, + ) + + # ==================== Token Management Tests ==================== + + def test_generate_invite_token_success(self, mock_redis_dependencies): + """Test successful invite token generation.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="user-123", email="test@example.com" + ) + + # Mock uuid generation + with patch("services.account_service.uuid.uuid4") as mock_uuid: + mock_uuid.return_value = "test-uuid-123" + + # Execute test + result = RegisterService.generate_invite_token(mock_tenant, mock_account) + + # Verify results + assert result == "test-uuid-123" + mock_redis_dependencies.setex.assert_called_once() + + # Verify the stored data + call_args = mock_redis_dependencies.setex.call_args + assert call_args[0][0] == "member_invite:token:test-uuid-123" + stored_data = json.loads(call_args[0][2]) + assert stored_data["account_id"] == "user-123" + assert stored_data["email"] == "test@example.com" + assert stored_data["workspace_id"] == "tenant-456" + + def test_is_valid_invite_token_valid(self, mock_redis_dependencies): + """Test checking valid invite token.""" + # Setup mock + mock_redis_dependencies.get.return_value = b'{"test": "data"}' + + # Execute test + result = RegisterService.is_valid_invite_token("valid-token") + + # Verify results + assert result is True + mock_redis_dependencies.get.assert_called_once_with("member_invite:token:valid-token") + + def test_is_valid_invite_token_invalid(self, mock_redis_dependencies): + """Test checking invalid invite token.""" + # Setup mock + mock_redis_dependencies.get.return_value = None + + # Execute test + result = RegisterService.is_valid_invite_token("invalid-token") + + # Verify results + assert result is False + mock_redis_dependencies.get.assert_called_once_with("member_invite:token:invalid-token") + + def test_revoke_token_with_workspace_and_email(self, mock_redis_dependencies): + """Test revoking token with workspace ID and email.""" + # Execute test + RegisterService.revoke_token("workspace-123", "test@example.com", "token-123") + + # Verify results + mock_redis_dependencies.delete.assert_called_once() + call_args = mock_redis_dependencies.delete.call_args + assert "workspace-123" in call_args[0][0] + # The email is hashed, so we check for the hash pattern instead + assert "member_invite_token:" in call_args[0][0] + + def test_revoke_token_without_workspace_and_email(self, mock_redis_dependencies): + """Test revoking token without workspace ID and email.""" + # Execute test + RegisterService.revoke_token("", "", "token-123") + + # Verify results + mock_redis_dependencies.delete.assert_called_once_with("member_invite:token:token-123") + + # ==================== Invitation Validation Tests ==================== + + def test_get_invitation_if_token_valid_success(self, mock_db_dependencies, mock_redis_dependencies): + """Test successful invitation validation.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.status = "normal" + mock_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="user-123", email="test@example.com" + ) + + with patch("services.account_service.RegisterService._get_invitation_by_token") as mock_get_invitation_by_token: + # Mock the invitation data returned by _get_invitation_by_token + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_get_invitation_by_token.return_value = invitation_data + + # Mock database queries - complex query mocking + mock_query1 = MagicMock() + mock_query1.filter.return_value.first.return_value = mock_tenant + + mock_query2 = MagicMock() + mock_query2.join.return_value.filter.return_value.first.return_value = (mock_account, "normal") + + mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is not None + assert result["account"] == mock_account + assert result["tenant"] == mock_tenant + assert result["data"] == invitation_data + + def test_get_invitation_if_token_valid_no_token_data(self, mock_redis_dependencies): + """Test invitation validation with no token data.""" + # Setup mock + mock_redis_dependencies.get.return_value = None + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is None + + def test_get_invitation_if_token_valid_tenant_not_found(self, mock_db_dependencies, mock_redis_dependencies): + """Test invitation validation when tenant is not found.""" + # Setup mock Redis data + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() + + # Mock database queries - no tenant found + mock_query = MagicMock() + mock_query.filter.return_value.first.return_value = None + mock_db_dependencies["db"].session.query.return_value = mock_query + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is None + + def test_get_invitation_if_token_valid_account_not_found(self, mock_db_dependencies, mock_redis_dependencies): + """Test invitation validation when account is not found.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.status = "normal" + + # Mock Redis data + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() + + # Mock database queries + mock_query1 = MagicMock() + mock_query1.filter.return_value.first.return_value = mock_tenant + + mock_query2 = MagicMock() + mock_query2.join.return_value.filter.return_value.first.return_value = None # No account found + + mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is None + + def test_get_invitation_if_token_valid_account_id_mismatch(self, mock_db_dependencies, mock_redis_dependencies): + """Test invitation validation when account ID doesn't match.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.status = "normal" + mock_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="different-user-456", email="test@example.com" + ) + + # Mock Redis data with different account ID + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() + + # Mock database queries + mock_query1 = MagicMock() + mock_query1.filter.return_value.first.return_value = mock_tenant + + mock_query2 = MagicMock() + mock_query2.join.return_value.filter.return_value.first.return_value = (mock_account, "normal") + + mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is None + + # ==================== Helper Method Tests ==================== + + def test_get_invitation_token_key(self): + """Test the _get_invitation_token_key helper method.""" + # Execute test + result = RegisterService._get_invitation_token_key("test-token") + + # Verify results + assert result == "member_invite:token:test-token" + + def test_get_invitation_by_token_with_workspace_and_email(self, mock_redis_dependencies): + """Test _get_invitation_by_token with workspace ID and email.""" + # Setup mock + mock_redis_dependencies.get.return_value = b"user-123" + + # Execute test + result = RegisterService._get_invitation_by_token("token-123", "workspace-456", "test@example.com") + + # Verify results + assert result is not None + assert result["account_id"] == "user-123" + assert result["email"] == "test@example.com" + assert result["workspace_id"] == "workspace-456" + + def test_get_invitation_by_token_without_workspace_and_email(self, mock_redis_dependencies): + """Test _get_invitation_by_token without workspace ID and email.""" + # Setup mock + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() + + # Execute test + result = RegisterService._get_invitation_by_token("token-123") + + # Verify results + assert result is not None + assert result == invitation_data + + def test_get_invitation_by_token_no_data(self, mock_redis_dependencies): + """Test _get_invitation_by_token with no data.""" + # Setup mock + mock_redis_dependencies.get.return_value = None + + # Execute test + result = RegisterService._get_invitation_by_token("token-123") + + # Verify results + assert result is None diff --git a/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py b/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py index 728c58fc5b..93284eed4b 100644 --- a/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py +++ b/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py @@ -27,11 +27,11 @@ def create_mock_usage(prompt_tokens: int = 10, completion_tokens: int = 5) -> LL return LLMUsage( prompt_tokens=prompt_tokens, prompt_unit_price=Decimal("0.001"), - prompt_price_unit=Decimal("1"), + prompt_price_unit=Decimal(1), prompt_price=Decimal(str(prompt_tokens)) * Decimal("0.001"), completion_tokens=completion_tokens, completion_unit_price=Decimal("0.002"), - completion_price_unit=Decimal("1"), + completion_price_unit=Decimal(1), completion_price=Decimal(str(completion_tokens)) * Decimal("0.002"), total_tokens=prompt_tokens + completion_tokens, total_price=Decimal(str(prompt_tokens)) * Decimal("0.001") + Decimal(str(completion_tokens)) * Decimal("0.002"), diff --git a/api/uv.lock b/api/uv.lock index e108e0c445..21b6b20f53 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1498,7 +1498,7 @@ dev = [ { name = "pytest-cov", specifier = "~=4.1.0" }, { name = "pytest-env", specifier = "~=1.1.3" }, { name = "pytest-mock", specifier = "~=3.14.0" }, - { name = "ruff", specifier = "~=0.11.5" }, + { name = "ruff", specifier = "~=0.12.3" }, { name = "scipy-stubs", specifier = ">=1.15.3.0" }, { name = "types-aiofiles", specifier = "~=24.1.0" }, { name = "types-beautifulsoup4", specifier = "~=4.12.0" }, @@ -5088,27 +5088,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, - { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, - { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, - { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, - { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, - { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, - { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +version = "0.12.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" }, + { url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" }, + { url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" }, + { url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" }, + { url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" }, + { url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" }, + { url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" }, ] [[package]] diff --git a/docker/.env.example b/docker/.env.example index e2d7436067..03d9b6ebb8 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1145,3 +1145,13 @@ QUEUE_MONITOR_THRESHOLD=200 QUEUE_MONITOR_ALERT_EMAILS= # Monitor interval in minutes, default is 30 minutes QUEUE_MONITOR_INTERVAL=30 + +# Celery schedule tasks configuration +ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false +ENABLE_CLEAN_UNUSED_DATASETS_TASK=false +ENABLE_CREATE_TIDB_SERVERLESS_TASK=false +ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false +ENABLE_CLEAN_MESSAGES=false +ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false +ENABLE_DATASETS_QUEUE_MONITOR=false +ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 7c1544acb9..003038c539 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -55,6 +55,28 @@ services: - ssrf_proxy_network - default + # worker_beat service + # Celery beat for scheduling periodic tasks. + worker_beat: + image: langgenius/dify-api:1.5.0 + restart: always + environment: + # Use the shared environment variables. + <<: *shared-api-worker-env + # Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks. + MODE: beat + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + volumes: + # Mount the storage directory to the container, for storing user files. + - ./volumes/app/storage:/app/api/storage + networks: + - ssrf_proxy_network + - default + # Frontend web application. web: image: langgenius/dify-web:1.6.0 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 3803c26a33..9ab73ab53f 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -517,6 +517,14 @@ x-shared-env: &shared-api-worker-env QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200} QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-} QUEUE_MONITOR_INTERVAL: ${QUEUE_MONITOR_INTERVAL:-30} + ENABLE_CLEAN_EMBEDDING_CACHE_TASK: ${ENABLE_CLEAN_EMBEDDING_CACHE_TASK:-false} + ENABLE_CLEAN_UNUSED_DATASETS_TASK: ${ENABLE_CLEAN_UNUSED_DATASETS_TASK:-false} + ENABLE_CREATE_TIDB_SERVERLESS_TASK: ${ENABLE_CREATE_TIDB_SERVERLESS_TASK:-false} + ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK: ${ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK:-false} + ENABLE_CLEAN_MESSAGES: ${ENABLE_CLEAN_MESSAGES:-false} + ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK: ${ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK:-false} + ENABLE_DATASETS_QUEUE_MONITOR: ${ENABLE_DATASETS_QUEUE_MONITOR:-false} + ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK: ${ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK:-true} services: # API service @@ -574,6 +582,28 @@ services: - ssrf_proxy_network - default + # worker_beat service + # Celery beat for scheduling periodic tasks. + worker_beat: + image: langgenius/dify-api:1.5.0 + restart: always + environment: + # Use the shared environment variables. + <<: *shared-api-worker-env + # Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks. + MODE: beat + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + volumes: + # Mount the storage directory to the container, for storing user files. + - ./volumes/app/storage:/app/api/storage + networks: + - ssrf_proxy_network + - default + # Frontend web application. web: image: langgenius/dify-web:1.6.0 diff --git a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx index 938a03992b..e288c62b5d 100644 --- a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx +++ b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx @@ -1,16 +1,18 @@ -import type { FC } from 'react' import React from 'react' import Main from '@/app/components/explore/installed-app' export type IInstalledAppProps = { - params: Promise<{ + params: { appId: string - }> + } } -const InstalledApp: FC = async ({ params }) => { +// Using Next.js page convention for async server components +async function InstalledApp({ params }: IInstalledAppProps) { + const appId = (await params).appId return ( -
+
) } -export default React.memo(InstalledApp) + +export default InstalledApp diff --git a/web/app/(shareLayout)/chat/[token]/page.tsx b/web/app/(shareLayout)/chat/[token]/page.tsx index 640c40378f..8ce67585f0 100644 --- a/web/app/(shareLayout)/chat/[token]/page.tsx +++ b/web/app/(shareLayout)/chat/[token]/page.tsx @@ -1,10 +1,13 @@ 'use client' import React from 'react' import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' +import AuthenticatedLayout from '../../components/authenticated-layout' const Chat = () => { return ( - + + + ) } diff --git a/web/app/(shareLayout)/chatbot/[token]/page.tsx b/web/app/(shareLayout)/chatbot/[token]/page.tsx index 6196afecc4..5323d0dacc 100644 --- a/web/app/(shareLayout)/chatbot/[token]/page.tsx +++ b/web/app/(shareLayout)/chatbot/[token]/page.tsx @@ -1,10 +1,13 @@ 'use client' import React from 'react' import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' +import AuthenticatedLayout from '../../components/authenticated-layout' const Chatbot = () => { return ( - + + + ) } diff --git a/web/app/(shareLayout)/completion/[token]/page.tsx b/web/app/(shareLayout)/completion/[token]/page.tsx index e8bc9d79f5..ae91338b9a 100644 --- a/web/app/(shareLayout)/completion/[token]/page.tsx +++ b/web/app/(shareLayout)/completion/[token]/page.tsx @@ -1,9 +1,12 @@ import React from 'react' import Main from '@/app/components/share/text-generation' +import AuthenticatedLayout from '../../components/authenticated-layout' const Completion = () => { return ( -
+ +
+ ) } diff --git a/web/app/(shareLayout)/components/authenticated-layout.tsx b/web/app/(shareLayout)/components/authenticated-layout.tsx new file mode 100644 index 0000000000..e3cfc8e6a8 --- /dev/null +++ b/web/app/(shareLayout)/components/authenticated-layout.tsx @@ -0,0 +1,84 @@ +'use client' + +import AppUnavailable from '@/app/components/base/app-unavailable' +import Loading from '@/app/components/base/loading' +import { removeAccessToken } from '@/app/components/share/utils' +import { useWebAppStore } from '@/context/web-app-context' +import { useGetUserCanAccessApp } from '@/service/access-control' +import { useGetWebAppInfo, useGetWebAppMeta, useGetWebAppParams } from '@/service/use-share' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import React, { useCallback, useEffect } from 'react' +import { useTranslation } from 'react-i18next' + +const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { + const { t } = useTranslation() + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) + const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) + const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetWebAppParams() + const { isFetching: isFetchingAppInfo, data: appInfo, error: appInfoError } = useGetWebAppInfo() + const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetWebAppMeta() + const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false }) + + useEffect(() => { + if (appInfo) + updateAppInfo(appInfo) + if (appParams) + updateAppParams(appParams) + if (appMeta) + updateWebAppMeta(appMeta) + updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + }, [appInfo, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp]) + + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.set('redirect_url', pathname) + return `/webapp-signin?${params.toString()}` + }, [searchParams, pathname]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + + if (appInfoError) { + return
+ +
+ } + if (appParamsError) { + return
+ +
+ } + if (appMetaError) { + return
+ +
+ } + if (useCanAccessAppError) { + return
+ +
+ } + if (userCanAccessApp && !userCanAccessApp.result) { + return
+ + {t('common.userProfile.logout')} +
+ } + if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) { + return
+ +
+ } + return <>{children} +} + +export default React.memo(AuthenticatedLayout) diff --git a/web/app/(shareLayout)/components/splash.tsx b/web/app/(shareLayout)/components/splash.tsx new file mode 100644 index 0000000000..4fe9efe4dd --- /dev/null +++ b/web/app/(shareLayout)/components/splash.tsx @@ -0,0 +1,80 @@ +'use client' +import type { FC, PropsWithChildren } from 'react' +import { useEffect } from 'react' +import { useCallback } from 'react' +import { useWebAppStore } from '@/context/web-app-context' +import { useRouter, useSearchParams } from 'next/navigation' +import AppUnavailable from '@/app/components/base/app-unavailable' +import { checkOrSetAccessToken, removeAccessToken, setAccessToken } from '@/app/components/share/utils' +import { useTranslation } from 'react-i18next' +import { fetchAccessToken } from '@/service/share' +import Loading from '@/app/components/base/loading' +import { AccessMode } from '@/models/access-control' + +const Splash: FC = ({ children }) => { + const { t } = useTranslation() + const shareCode = useWebAppStore(s => s.shareCode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) + const searchParams = useSearchParams() + const router = useRouter() + const redirectUrl = searchParams.get('redirect_url') + const tokenFromUrl = searchParams.get('web_sso_token') + const message = searchParams.get('message') + const code = searchParams.get('code') + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.delete('code') + return `/webapp-signin?${params.toString()}` + }, [searchParams]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + + useEffect(() => { + (async () => { + if (message) + return + if (shareCode && tokenFromUrl && redirectUrl) { + localStorage.setItem('webapp_access_token', tokenFromUrl) + const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: tokenFromUrl }) + await setAccessToken(shareCode, tokenResp.access_token) + router.replace(decodeURIComponent(redirectUrl)) + return + } + if (shareCode && redirectUrl && localStorage.getItem('webapp_access_token')) { + const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) + await setAccessToken(shareCode, tokenResp.access_token) + router.replace(decodeURIComponent(redirectUrl)) + return + } + if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { + await checkOrSetAccessToken(shareCode) + router.replace(decodeURIComponent(redirectUrl)) + } + })() + }, [shareCode, redirectUrl, router, tokenFromUrl, message, webAppAccessMode]) + + if (message) { + return
+ + {code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')} +
+ } + if (tokenFromUrl) { + return
+ +
+ } + if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { + return
+ +
+ } + return <>{children} +} + +export default Splash diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx index d057ba7599..5af913cac9 100644 --- a/web/app/(shareLayout)/layout.tsx +++ b/web/app/(shareLayout)/layout.tsx @@ -1,54 +1,15 @@ -'use client' -import React, { useEffect, useState } from 'react' -import type { FC } from 'react' -import { usePathname, useSearchParams } from 'next/navigation' -import Loading from '../components/base/loading' -import { useGlobalPublicStore } from '@/context/global-public-context' -import { AccessMode } from '@/models/access-control' -import { getAppAccessModeByAppCode } from '@/service/share' +import type { FC, PropsWithChildren } from 'react' +import WebAppStoreProvider from '@/context/web-app-context' +import Splash from './components/splash' -const Layout: FC<{ - children: React.ReactNode -}> = ({ children }) => { - const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending) - const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode) - const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const pathname = usePathname() - const searchParams = useSearchParams() - const redirectUrl = searchParams.get('redirect_url') - const [isLoading, setIsLoading] = useState(true) - useEffect(() => { - (async () => { - if (!isGlobalPending && !systemFeatures.webapp_auth.enabled) { - setIsLoading(false) - return - } - - let appCode: string | null = null - if (redirectUrl) { - const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) - appCode = url.pathname.split('/').pop() || null - } - else { - appCode = pathname.split('/').pop() || null - } - - if (!appCode) - return - setIsLoading(true) - const ret = await getAppAccessModeByAppCode(appCode) - setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC) - setIsLoading(false) - })() - }, [pathname, redirectUrl, setWebAppAccessMode, isGlobalPending, systemFeatures.webapp_auth.enabled]) - if (isLoading || isGlobalPending) { - return
- -
- } +const Layout: FC = ({ children }) => { return (
- {children} + + + {children} + +
) } diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx index 9f9a8ad4e3..5e3f6fff1d 100644 --- a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx @@ -9,8 +9,7 @@ import Button from '@/app/components/base/button' import { changeWebAppPasswordWithToken } from '@/service/common' import Toast from '@/app/components/base/toast' import Input from '@/app/components/base/input' - -const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ +import { validPassword } from '@/config' const ChangePasswordForm = () => { const { t } = useTranslation() diff --git a/web/app/(shareLayout)/webapp-signin/layout.tsx b/web/app/(shareLayout)/webapp-signin/layout.tsx index a03364d326..7649982072 100644 --- a/web/app/(shareLayout)/webapp-signin/layout.tsx +++ b/web/app/(shareLayout)/webapp-signin/layout.tsx @@ -3,10 +3,13 @@ import cn from '@/utils/classnames' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' +import type { PropsWithChildren } from 'react' +import { useTranslation } from 'react-i18next' -export default function SignInLayout({ children }: any) { - const { systemFeatures } = useGlobalPublicStore() - useDocumentTitle('') +export default function SignInLayout({ children }: PropsWithChildren) { + const { t } = useTranslation() + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + useDocumentTitle(t('login.webapp.login')) return <>
diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx index d6bdf607ba..44006a9f1e 100644 --- a/web/app/(shareLayout)/webapp-signin/normalForm.tsx +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -1,3 +1,4 @@ +'use client' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Link from 'next/link' diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx index 967516c416..1c6209b902 100644 --- a/web/app/(shareLayout)/webapp-signin/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/page.tsx @@ -1,36 +1,30 @@ 'use client' import { useRouter, useSearchParams } from 'next/navigation' import type { FC } from 'react' -import React, { useCallback, useEffect } from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import Toast from '@/app/components/base/toast' -import { removeAccessToken, setAccessToken } from '@/app/components/share/utils' +import { removeAccessToken } from '@/app/components/share/utils' import { useGlobalPublicStore } from '@/context/global-public-context' -import Loading from '@/app/components/base/loading' import AppUnavailable from '@/app/components/base/app-unavailable' import NormalForm from './normalForm' import { AccessMode } from '@/models/access-control' import ExternalMemberSsoAuth from './components/external-member-sso-auth' -import { fetchAccessToken } from '@/service/share' +import { useWebAppStore } from '@/context/web-app-context' const WebSSOForm: FC = () => { const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) const searchParams = useSearchParams() const router = useRouter() const redirectUrl = searchParams.get('redirect_url') - const tokenFromUrl = searchParams.get('web_sso_token') - const message = searchParams.get('message') - const code = searchParams.get('code') const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.delete('code') + const params = new URLSearchParams() + params.append('redirect_url', redirectUrl || '') return `/webapp-signin?${params.toString()}` - }, [searchParams]) + }, [redirectUrl]) const backToHome = useCallback(() => { removeAccessToken() @@ -38,73 +32,12 @@ const WebSSOForm: FC = () => { router.replace(url) }, [getSigninUrl, router]) - const showErrorToast = (msg: string) => { - Toast.notify({ - type: 'error', - message: msg, - }) - } - - const getAppCodeFromRedirectUrl = useCallback(() => { - if (!redirectUrl) - return null - const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) - const appCode = url.pathname.split('/').pop() - if (!appCode) - return null - - return appCode - }, [redirectUrl]) - - useEffect(() => { - (async () => { - if (message) - return - - const appCode = getAppCodeFromRedirectUrl() - if (appCode && tokenFromUrl && redirectUrl) { - localStorage.setItem('webapp_access_token', tokenFromUrl) - const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl }) - await setAccessToken(appCode, tokenResp.access_token) - router.replace(decodeURIComponent(redirectUrl)) - return - } - if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) { - const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) - await setAccessToken(appCode, tokenResp.access_token) - router.replace(decodeURIComponent(redirectUrl)) - } - })() - }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) - - useEffect(() => { - if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) - router.replace(decodeURIComponent(redirectUrl)) - }, [webAppAccessMode, router, redirectUrl]) - - if (tokenFromUrl) { - return
- -
- } - - if (message) { - return
- - {code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')} -
- } if (!redirectUrl) { - showErrorToast('redirect url is invalid.') return
} - if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) { - return
- -
- } + if (!systemFeatures.webapp_auth.enabled) { return

{t('login.webapp.disabled')}

diff --git a/web/app/(shareLayout)/workflow/[token]/page.tsx b/web/app/(shareLayout)/workflow/[token]/page.tsx index e93bc8c1af..4f5923e91f 100644 --- a/web/app/(shareLayout)/workflow/[token]/page.tsx +++ b/web/app/(shareLayout)/workflow/[token]/page.tsx @@ -1,10 +1,13 @@ import React from 'react' import Main from '@/app/components/share/text-generation' +import AuthenticatedLayout from '../../components/authenticated-layout' const Workflow = () => { return ( -
+ +
+ ) } diff --git a/web/app/account/account-page/email-change-modal.tsx b/web/app/account/account-page/email-change-modal.tsx new file mode 100644 index 0000000000..22620c9a6a --- /dev/null +++ b/web/app/account/account-page/email-change-modal.tsx @@ -0,0 +1,374 @@ +import React, { useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { useRouter } from 'next/navigation' +import { useContext } from 'use-context-selector' +import { ToastContext } from '@/app/components/base/toast' +import { RiCloseLine } from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import { + checkEmailExisted, + logout, + resetEmail, + sendVerifyCode, + verifyEmail, +} from '@/service/common' +import { noop } from 'lodash-es' + +type Props = { + show: boolean + onClose: () => void + email: string +} + +enum STEP { + start = 'start', + verifyOrigin = 'verifyOrigin', + newEmail = 'newEmail', + verifyNew = 'verifyNew', +} + +const EmailChangeModal = ({ onClose, email, show }: Props) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const router = useRouter() + const [step, setStep] = useState(STEP.start) + const [code, setCode] = useState('') + const [mail, setMail] = useState('') + const [time, setTime] = useState(0) + const [stepToken, setStepToken] = useState('') + const [newEmailExited, setNewEmailExited] = useState(false) + + const startCount = () => { + setTime(60) + const timer = setInterval(() => { + setTime((prev) => { + if (prev <= 0) { + clearInterval(timer) + return 0 + } + return prev - 1 + }) + }, 1000) + } + + const sendEmail = async (email: string, isOrigin: boolean, token?: string) => { + try { + const res = await sendVerifyCode({ + email, + phase: isOrigin ? 'old_email' : 'new_email', + token, + }) + startCount() + if (res.data) + setStepToken(res.data) + } + catch (error) { + notify({ + type: 'error', + message: `Error sending verification code: ${error ? (error as any).message : ''}`, + }) + } + } + + const verifyEmailAddress = async (email: string, code: string, token: string, callback?: () => void) => { + try { + const res = await verifyEmail({ + email, + code, + token, + }) + if (res.is_valid) { + setStepToken(res.token) + callback?.() + } + else { + notify({ + type: 'error', + message: 'Verifying email failed', + }) + } + } + catch (error) { + notify({ + type: 'error', + message: `Error verifying email: ${error ? (error as any).message : ''}`, + }) + } + } + + const sendCodeToOriginEmail = async () => { + await sendEmail( + email, + true, + ) + setStep(STEP.verifyOrigin) + } + + const handleVerifyOriginEmail = async () => { + await verifyEmailAddress(email, code, stepToken, () => setStep(STEP.newEmail)) + setCode('') + } + + const isValidEmail = (email: string): boolean => { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ + return emailRegex.test(email) + } + + const checkNewEmailExisted = async (email: string) => { + try { + await checkEmailExisted({ + email, + }) + setNewEmailExited(false) + } + catch (error) { + setNewEmailExited(false) + if ((error as any)?.code === 'email_already_in_use') { + setNewEmailExited(true) + } + else { + notify({ + type: 'error', + message: `Error checking email existence: ${error ? (error as any).message : ''}`, + }) + } + } + } + + const handleNewEmailValueChange = (mailAddress: string) => { + setMail(mailAddress) + if (isValidEmail(mailAddress)) + checkNewEmailExisted(mailAddress) + } + + const sendCodeToNewEmail = async () => { + if (!isValidEmail(mail)) { + notify({ + type: 'error', + message: 'Invalid email format', + }) + return + } + await sendEmail( + mail, + false, + stepToken, + ) + setStep(STEP.verifyNew) + } + + const handleLogout = async () => { + await logout({ + url: '/logout', + params: {}, + }) + + localStorage.removeItem('setup_status') + localStorage.removeItem('console_token') + localStorage.removeItem('refresh_token') + + router.push('/signin') + } + + const updateEmail = async () => { + try { + await resetEmail({ + new_email: mail, + token: stepToken, + }) + handleLogout() + } + catch (error) { + notify({ + type: 'error', + message: `Error changing email: ${error ? (error as any).message : ''}`, + }) + } + } + + const submitNewEmail = async () => { + await verifyEmailAddress(email, code, stepToken, () => updateEmail()) + } + + return ( + +
+ +
+ {step === STEP.start && ( + <> +
{t('common.account.changeEmail.title')}
+
+
{t('common.account.changeEmail.authTip')}
+
+ }} + values={{ email }} + /> +
+
+
+
+ + +
+ + )} + {step === STEP.verifyOrigin && ( + <> +
{t('common.account.changeEmail.verifyEmail')}
+
+
+ }} + values={{ email }} + /> +
+
+
+
{t('common.account.changeEmail.codeLabel')}
+ setCode(e.target.value)} + maxLength={6} + /> +
+
+ + +
+
+ {t('common.account.changeEmail.resendTip')} + {time > 0 && ( + {t('common.account.changeEmail.resendCount', { count: time })} + )} + {!time && ( + {t('common.account.changeEmail.resend')} + )} +
+ + )} + {step === STEP.newEmail && ( + <> +
{t('common.account.changeEmail.newEmail')}
+
+
{t('common.account.changeEmail.content3')}
+
+
+
{t('common.account.changeEmail.emailLabel')}
+ handleNewEmailValueChange(e.target.value)} + destructive={newEmailExited} + /> + {newEmailExited && ( +
{t('common.account.changeEmail.existingEmail')}
+ )} +
+
+ + +
+ + )} + {step === STEP.verifyNew && ( + <> +
{t('common.account.changeEmail.verifyNew')}
+
+
+ }} + values={{ email: mail }} + /> +
+
+
+
{t('common.account.changeEmail.codeLabel')}
+ setCode(e.target.value)} + maxLength={6} + /> +
+
+ + +
+
+ {t('common.account.changeEmail.resendTip')} + {time > 0 && ( + {t('common.account.changeEmail.resendCount', { count: time })} + )} + {!time && ( + {t('common.account.changeEmail.resend')} + )} +
+ + )} +
+ ) +} + +export default EmailChangeModal diff --git a/web/app/account/account-page/index.module.css b/web/app/account/account-page/index.module.css deleted file mode 100644 index 949d1257e9..0000000000 --- a/web/app/account/account-page/index.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.modal { - padding: 24px 32px !important; - width: 400px !important; -} - -.bg { - background: linear-gradient(180deg, rgba(217, 45, 32, 0.05) 0%, rgba(217, 45, 32, 0.00) 24.02%), #F9FAFB; -} - diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index 19c1e44236..e3fa9bb04f 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -6,7 +6,6 @@ import { } from '@remixicon/react' import { useContext } from 'use-context-selector' import DeleteAccount from '../delete-account' -import s from './index.module.css' import AvatarWithEdit from './AvatarWithEdit' import Collapse from '@/app/components/header/account-setting/collapse' import type { IItem } from '@/app/components/header/account-setting/collapse' @@ -21,6 +20,8 @@ import { IS_CE_EDITION } from '@/config' import Input from '@/app/components/base/input' import PremiumBadge from '@/app/components/base/premium-badge' import { useGlobalPublicStore } from '@/context/global-public-context' +import { validPassword } from '@/config' +import EmailChangeModal from './email-change-modal' const titleClassName = ` system-sm-semibold text-text-secondary @@ -29,8 +30,6 @@ const descriptionClassName = ` mt-1 body-xs-regular text-text-tertiary ` -const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ - export default function AccountPage() { const { t } = useTranslation() const { systemFeatures } = useGlobalPublicStore() @@ -48,6 +47,7 @@ export default function AccountPage() { const [showCurrentPassword, setShowCurrentPassword] = useState(false) const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) + const [showUpdateEmail, setShowUpdateEmail] = useState(false) const handleEditName = () => { setEditNameModalVisible(true) @@ -123,10 +123,17 @@ export default function AccountPage() { } const renderAppItem = (item: IItem) => { + const { icon, icon_background, icon_type, icon_url } = item as any return (
- +
{item.name}
@@ -170,6 +177,11 @@ export default function AccountPage() {
{userProfile.email}
+ {systemFeatures.enable_change_email && ( +
setShowUpdateEmail(true)}> + {t('common.operation.change')} +
+ )}
{ @@ -190,7 +202,7 @@ export default function AccountPage() { {!!apps.length && ( ({ key: app.id, name: app.name }))} + items={apps.map(app => ({ ...app, key: app.id, name: app.name }))} renderItem={renderAppItem} wrapperClassName='mt-2' /> @@ -202,7 +214,7 @@ export default function AccountPage() { setEditNameModalVisible(false)} - className={s.modal} + className='!w-[420px] !p-6' >
{t('common.account.editName')}
{t('common.account.name')}
@@ -231,7 +243,7 @@ export default function AccountPage() { setEditPasswordModalVisible(false) resetPasswordForm() }} - className={s.modal} + className='!w-[420px] !p-6' >
{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}
{userProfile.is_password_set && ( @@ -316,6 +328,13 @@ export default function AccountPage() { /> ) } + {showUpdateEmail && ( + setShowUpdateEmail(false)} + email={userProfile.email} + /> + )} ) } diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index c28cc20df5..3817ebf5a4 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -308,13 +308,11 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx operations={operations} />
-
- -
+
{currTool?.label[language]}
{!!currTool?.description[language] && ( - + )} + { + collection.allow_delete && collection.type === CollectionType.builtIn && ( + + ) + } {/* form */}
diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 47f8c09e39..b04148d484 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -470,8 +470,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { className="py-4" id="scrollableDiv" style={{ - height: 1000, // Specify a value - overflow: 'auto', display: 'flex', flexDirection: 'column-reverse', }}> diff --git a/web/app/components/base/chat/chat-with-history/context.tsx b/web/app/components/base/chat/chat-with-history/context.tsx index 5bf1514774..3a5dc793d6 100644 --- a/web/app/components/base/chat/chat-with-history/context.tsx +++ b/web/app/components/base/chat/chat-with-history/context.tsx @@ -18,11 +18,8 @@ import type { import { noop } from 'lodash-es' export type ChatWithHistoryContextValue = { - appInfoError?: any - appInfoLoading?: boolean - appMeta?: AppMeta - appData?: AppData - userCanAccess?: boolean + appMeta?: AppMeta | null + appData?: AppData | null appParams?: ChatConfig appChatListDataLoading?: boolean currentConversationId: string @@ -62,7 +59,6 @@ export type ChatWithHistoryContextValue = { } export const ChatWithHistoryContext = createContext({ - userCanAccess: false, currentConversationId: '', appPrevChatTree: [], pinnedConversationList: [], diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 32f74e6457..be935a70ba 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -21,9 +21,6 @@ import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { delConversation, - fetchAppInfo, - fetchAppMeta, - fetchAppParams, fetchChatList, fetchConversations, generationConversationName, @@ -43,8 +40,7 @@ import { useAppFavicon } from '@/hooks/use-app-favicon' import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import { noop } from 'lodash-es' -import { useGetUserCanAccessApp } from '@/service/access-control' -import { useGlobalPublicStore } from '@/context/global-public-context' +import { useWebAppStore } from '@/context/web-app-context' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -74,13 +70,9 @@ function getFormattedChatList(messages: any[]) { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) - const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) - const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ - appId: installedAppInfo?.app.id || appInfo?.app_id, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) + const appInfo = useWebAppStore(s => s.appInfo) + const appParams = useWebAppStore(s => s.appParams) + const appMeta = useWebAppStore(s => s.appMeta) useAppFavicon({ enable: !installedAppInfo, @@ -107,6 +99,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { use_icon_as_answer_icon: app.use_icon_as_answer_icon, }, plan: 'basic', + custom_config: null, } as AppData } @@ -166,8 +159,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { return currentConversationId }, [currentConversationId, newConversationId]) - const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId)) - const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId)) const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100)) const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) @@ -485,9 +476,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [isInstalledApp, appId, t, notify]) return { - appInfoError, - appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), - userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, appId, currentConversationId, diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx index fe8e7b430d..cceb21b295 100644 --- a/web/app/components/base/chat/chat-with-history/index.tsx +++ b/web/app/components/base/chat/chat-with-history/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import { - useCallback, useEffect, useState, } from 'react' @@ -19,12 +18,10 @@ import ChatWrapper from './chat-wrapper' import type { InstalledApp } from '@/models/explore' import Loading from '@/app/components/base/loading' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' +import { checkOrSetAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import cn from '@/utils/classnames' import useDocumentTitle from '@/hooks/use-document-title' -import { useTranslation } from 'react-i18next' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' type ChatWithHistoryProps = { className?: string @@ -33,16 +30,12 @@ const ChatWithHistory: FC = ({ className, }) => { const { - userCanAccess, - appInfoError, appData, - appInfoLoading, appChatListDataLoading, chatShouldReloadKey, isMobile, themeBuilder, sidebarCollapseState, - isInstalledApp, } = useChatWithHistoryContext() const isSidebarCollapsed = sidebarCollapseState const customConfig = appData?.custom_config @@ -56,41 +49,6 @@ const ChatWithHistory: FC = ({ useDocumentTitle(site?.title || 'Chat') - const { t } = useTranslation() - const searchParams = useSearchParams() - const router = useRouter() - const pathname = usePathname() - const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.set('redirect_url', pathname) - return `/webapp-signin?${params.toString()}` - }, [searchParams, pathname]) - - const backToHome = useCallback(() => { - removeAccessToken() - const url = getSigninUrl() - router.replace(url) - }, [getSigninUrl, router]) - - if (appInfoLoading) { - return ( - - ) - } - if (!userCanAccess) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - - if (appInfoError) { - return ( - - ) - } - return (
= ({ const themeBuilder = useThemeContext() const { - appInfoError, - appInfoLoading, - userCanAccess, appData, appParams, appMeta, @@ -191,10 +146,7 @@ const ChatWithHistoryWrap: FC = ({ return ( item.isOpeningStatement) - if (index > -1) { ret[index] = { ...ret[index], content: getIntroduction(config.opening_statement), - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map(item => getIntroduction(item)), } } else { @@ -97,7 +96,7 @@ export const useChat = ( content: getIntroduction(config.opening_statement), isAnswer: true, isOpeningStatement: true, - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map(item => getIntroduction(item)), }) } } diff --git a/web/app/components/base/chat/chat/question.tsx b/web/app/components/base/chat/chat/question.tsx index d221587940..cae8e2b8ce 100644 --- a/web/app/components/base/chat/chat/question.tsx +++ b/web/app/components/base/chat/chat/question.tsx @@ -98,7 +98,7 @@ const Question: FC = ({ return (
-
+
= ({
{ diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx index 69cf7f163b..4c8c0a2455 100644 --- a/web/app/components/base/chat/embedded-chatbot/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/index.tsx @@ -1,10 +1,7 @@ 'use client' import { - useCallback, useEffect, - useState, } from 'react' -import { useAsyncEffect } from 'ahooks' import { useTranslation } from 'react-i18next' import { EmbeddedChatbotContext, @@ -14,8 +11,6 @@ import { useEmbeddedChatbot } from './hooks' import { isDify } from './utils' import { useThemeContext } from './theme/theme-context' import { CssTransform } from './theme/utils' -import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' -import AppUnavailable from '@/app/components/base/app-unavailable' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import Loading from '@/app/components/base/loading' import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header' @@ -25,21 +20,16 @@ import DifyLogo from '@/app/components/base/logo/dify-logo' import cn from '@/utils/classnames' import useDocumentTitle from '@/hooks/use-document-title' import { useGlobalPublicStore } from '@/context/global-public-context' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' const Chatbot = () => { const { - userCanAccess, isMobile, allowResetChat, - appInfoError, - appInfoLoading, appData, appChatListDataLoading, chatShouldReloadKey, handleNewConversation, themeBuilder, - isInstalledApp, } = useEmbeddedChatbotContext() const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -55,58 +45,6 @@ const Chatbot = () => { useDocumentTitle(site?.title || 'Chat') - const searchParams = useSearchParams() - const router = useRouter() - const pathname = usePathname() - const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.set('redirect_url', pathname) - return `/webapp-signin?${params.toString()}` - }, [searchParams, pathname]) - - const backToHome = useCallback(() => { - removeAccessToken() - const url = getSigninUrl() - router.replace(url) - }, [getSigninUrl, router]) - - if (appInfoLoading) { - return ( - <> - {!isMobile && } - {isMobile && ( -
-
- -
-
- )} - - ) - } - - if (!userCanAccess) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - - if (appInfoError) { - return ( - <> - {!isMobile && } - {isMobile && ( -
-
- -
-
- )} - - ) - } return (
{ const themeBuilder = useThemeContext() const { - appInfoError, - appInfoLoading, appData, userCanAccess, appParams, @@ -200,8 +136,6 @@ const EmbeddedChatbotWrapper = () => { return { } const EmbeddedChatbot = () => { - const [initialized, setInitialized] = useState(false) - const [appUnavailable, setAppUnavailable] = useState(false) - const [isUnknownReason, setIsUnknownReason] = useState(false) - - useAsyncEffect(async () => { - if (!initialized) { - try { - await checkOrSetAccessToken() - } - catch (e: any) { - if (e.status === 404) { - setAppUnavailable(true) - } - else { - setIsUnknownReason(true) - setAppUnavailable(true) - } - } - setInitialized(true) - } - }, []) - - if (!initialized) - return null - - if (appUnavailable) - return - return } diff --git a/web/app/components/base/chat/types.ts b/web/app/components/base/chat/types.ts index 91f9bc976b..c463879a53 100644 --- a/web/app/components/base/chat/types.ts +++ b/web/app/components/base/chat/types.ts @@ -49,6 +49,16 @@ export type ChatConfig = Omit & { questionEditEnable?: boolean supportFeedback?: boolean supportCitationHitInfo?: boolean + system_parameters: { + audio_file_size_limit: number + file_size_limit: number + image_file_size_limit: number + video_file_size_limit: number + workflow_file_upload_limit: number + } + more_like_this: { + enabled: boolean + } } export type WorkflowProcess = { diff --git a/web/app/components/base/date-and-time-picker/common/option-list-item.tsx b/web/app/components/base/date-and-time-picker/common/option-list-item.tsx index d11a6e9e86..8c2c8d82f1 100644 --- a/web/app/components/base/date-and-time-picker/common/option-list-item.tsx +++ b/web/app/components/base/date-and-time-picker/common/option-list-item.tsx @@ -4,18 +4,21 @@ import cn from '@/utils/classnames' type OptionListItemProps = { isSelected: boolean onClick: () => void + noAutoScroll?: boolean } & React.LiHTMLAttributes const OptionListItem: FC = ({ isSelected, onClick, + noAutoScroll, children, }) => { const listItemRef = useRef(null) useEffect(() => { - if (isSelected) + if (isSelected && !noAutoScroll) listItemRef.current?.scrollIntoView({ behavior: 'instant' }) + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/web/app/components/base/date-and-time-picker/time-picker/header.tsx b/web/app/components/base/date-and-time-picker/time-picker/header.tsx index 3d85b2ea40..dc6b56f744 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/header.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/header.tsx @@ -1,13 +1,18 @@ import React from 'react' import { useTranslation } from 'react-i18next' -const Header = () => { +type Props = { + title?: string +} +const Header = ({ + title, +}: Props) => { const { t } = useTranslation() return (
- {t('time.title.pickTime')} + {title || t('time.title.pickTime')}
) diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.tsx index a5e666d631..d493106667 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/index.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/index.tsx @@ -20,6 +20,9 @@ const TimePicker = ({ onChange, onClear, renderTrigger, + title, + minuteFilter, + popupClassName, }: TimePickerProps) => { const { t } = useTranslation() const [isOpen, setIsOpen] = useState(false) @@ -108,6 +111,15 @@ const TimePicker = ({ const displayValue = value?.format(timeFormat) || '' const placeholderDate = isOpen && selectedTime ? selectedTime.format(timeFormat) : (placeholder || t('time.defaultPlaceholder')) + const inputElem = ( + + ) return ( - {renderTrigger ? (renderTrigger()) : ( + {renderTrigger ? (renderTrigger({ + inputElem, + onClick: handleClickTrigger, + isOpen, + })) : (
- + {inputElem} )} - +
{/* Header */} -
+
{/* Time Options */} = ({ selectedTime, + minuteFilter, handleSelectHour, handleSelectMinute, handleSelectPeriod, @@ -33,7 +34,7 @@ const Options: FC = ({ {/* Minute */}
    { - minuteOptions.map((minute) => { + (minuteFilter ? minuteFilter(minuteOptions) : minuteOptions).map((minute) => { const isSelected = selectedTime?.format('mm') === minute return ( = ({ key={period} isSelected={isSelected} onClick={handleSelectPeriod.bind(null, period)} + noAutoScroll // if choose PM which would hide(scrolled) AM that may make user confused that there's no am. > {period} diff --git a/web/app/components/base/date-and-time-picker/types.ts b/web/app/components/base/date-and-time-picker/types.ts index 214c0f011b..4ac01c142a 100644 --- a/web/app/components/base/date-and-time-picker/types.ts +++ b/web/app/components/base/date-and-time-picker/types.ts @@ -28,6 +28,7 @@ export type DatePickerProps = { onClear: () => void triggerWrapClassName?: string renderTrigger?: (props: TriggerProps) => React.ReactNode + minuteFilter?: (minutes: string[]) => string[] popupZIndexClassname?: string } @@ -47,13 +48,21 @@ export type DatePickerFooterProps = { handleConfirmDate: () => void } +export type TriggerParams = { + isOpen: boolean + inputElem: React.ReactNode + onClick: (e: React.MouseEvent) => void +} export type TimePickerProps = { value: Dayjs | undefined timezone?: string placeholder?: string onChange: (date: Dayjs | undefined) => void onClear: () => void - renderTrigger?: () => React.ReactNode + renderTrigger?: (props: TriggerParams) => React.ReactNode + title?: string + minuteFilter?: (minutes: string[]) => string[] + popupClassName?: string } export type TimePickerFooterProps = { @@ -81,6 +90,7 @@ export type CalendarItemProps = { export type TimeOptionsProps = { selectedTime: Dayjs | undefined + minuteFilter?: (minutes: string[]) => string[] handleSelectHour: (hour: string) => void handleSelectMinute: (minute: string) => void handleSelectPeriod: (period: Period) => void diff --git a/web/app/components/base/date-and-time-picker/utils/dayjs.ts b/web/app/components/base/date-and-time-picker/utils/dayjs.ts index 0928fa5d58..4ba6af3f58 100644 --- a/web/app/components/base/date-and-time-picker/utils/dayjs.ts +++ b/web/app/components/base/date-and-time-picker/utils/dayjs.ts @@ -2,6 +2,7 @@ import dayjs, { type Dayjs } from 'dayjs' import type { Day } from '../types' import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' +import tz from '@/utils/timezone.json' dayjs.extend(utc) dayjs.extend(timezone) @@ -78,3 +79,14 @@ export const getHourIn12Hour = (date: Dayjs) => { export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => { return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone) } + +// Asia/Shanghai -> UTC+8 +const DEFAULT_OFFSET_STR = 'UTC+0' +export const convertTimezoneToOffsetStr = (timezone?: string) => { + if(!timezone) + return DEFAULT_OFFSET_STR + const tzItem = tz.find(item => item.value === timezone) + if(!tzItem) + return DEFAULT_OFFSET_STR + return `UTC${tzItem.name.charAt(0)}${tzItem.name.charAt(2)}` +} diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index 8d05967f33..6fc4b67181 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -101,7 +101,7 @@ const EmojiPickerInner: FC = ({
-
+
{isSearching && <>

Search

@@ -170,7 +170,7 @@ const EmojiPickerInner: FC = ({ 'flex h-8 w-8 items-center justify-center rounded-lg p-1', ) } style={{ background: color }}> - {selectedEmoji !== '' && } + {selectedEmoji !== '' && }
})} diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx index 117c8a5558..51e33c43d2 100644 --- a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx +++ b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx @@ -130,6 +130,7 @@ const OpeningSettingModal = ({ { const value = e.target.value setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => { diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx new file mode 100644 index 0000000000..70cd3a9f78 --- /dev/null +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -0,0 +1,177 @@ +import { + isValidElement, + memo, + useMemo, +} from 'react' +import type { AnyFieldApi } from '@tanstack/react-form' +import { useStore } from '@tanstack/react-form' +import cn from '@/utils/classnames' +import Input from '@/app/components/base/input' +import PureSelect from '@/app/components/base/select/pure' +import type { FormSchema } from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' +import { useRenderI18nObject } from '@/hooks/use-i18n' + +export type BaseFieldProps = { + fieldClassName?: string + labelClassName?: string + inputContainerClassName?: string + inputClassName?: string + formSchema: FormSchema + field: AnyFieldApi + disabled?: boolean +} +const BaseField = ({ + fieldClassName, + labelClassName, + inputContainerClassName, + inputClassName, + formSchema, + field, + disabled, +}: BaseFieldProps) => { + const renderI18nObject = useRenderI18nObject() + const { + label, + required, + placeholder, + options, + labelClassName: formLabelClassName, + show_on = [], + } = formSchema + + const memorizedLabel = useMemo(() => { + if (isValidElement(label)) + return label + + if (typeof label === 'string') + return label + + if (typeof label === 'object' && label !== null) + return renderI18nObject(label as Record) + }, [label, renderI18nObject]) + const memorizedPlaceholder = useMemo(() => { + if (typeof placeholder === 'string') + return placeholder + + if (typeof placeholder === 'object' && placeholder !== null) + return renderI18nObject(placeholder as Record) + }, [placeholder, renderI18nObject]) + const memorizedOptions = useMemo(() => { + return options?.map((option) => { + return { + label: typeof option.label === 'string' ? option.label : renderI18nObject(option.label), + value: option.value, + } + }) || [] + }, [options, renderI18nObject]) + const value = useStore(field.form.store, s => s.values[field.name]) + const values = useStore(field.form.store, (s) => { + return show_on.reduce((acc, condition) => { + acc[condition.variable] = s.values[condition.variable] + return acc + }, {} as Record) + }) + const show = useMemo(() => { + return show_on.every((condition) => { + const conditionValue = values[condition.variable] + return conditionValue === condition.value + }) + }, [values, show_on]) + + if (!show) + return null + + return ( +
+
+ {memorizedLabel} + { + required && ( + * + ) + } +
+
+ { + formSchema.type === FormTypeEnum.textInput && ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + disabled={disabled} + placeholder={memorizedPlaceholder} + /> + ) + } + { + formSchema.type === FormTypeEnum.secretInput && ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + disabled={disabled} + placeholder={memorizedPlaceholder} + /> + ) + } + { + formSchema.type === FormTypeEnum.textNumber && ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + disabled={disabled} + placeholder={memorizedPlaceholder} + /> + ) + } + { + formSchema.type === FormTypeEnum.select && ( + field.handleChange(v)} + disabled={disabled} + placeholder={memorizedPlaceholder} + options={memorizedOptions} + triggerPopupSameWidth + /> + ) + } + { + formSchema.type === FormTypeEnum.radio && ( +
+ { + memorizedOptions.map(option => ( +
field.handleChange(option.value)} + > + {option.label} +
+ )) + } +
+ ) + } +
+
+ ) +} + +export default memo(BaseField) diff --git a/web/app/components/base/form/components/base/base-form.tsx b/web/app/components/base/form/components/base/base-form.tsx new file mode 100644 index 0000000000..6911e4d95f --- /dev/null +++ b/web/app/components/base/form/components/base/base-form.tsx @@ -0,0 +1,104 @@ +import { + memo, + useCallback, + useImperativeHandle, +} from 'react' +import type { + AnyFieldApi, +} from '@tanstack/react-form' +import { useForm } from '@tanstack/react-form' +import type { + FormRef, + FormSchema, +} from '@/app/components/base/form/types' +import { + BaseField, +} from '.' +import type { + BaseFieldProps, +} from '.' +import cn from '@/utils/classnames' + +export type BaseFormProps = { + formSchemas?: FormSchema[] + defaultValues?: Record + formClassName?: string + ref?: FormRef + disabled?: boolean +} & Pick + +const BaseForm = ({ + formSchemas, + defaultValues, + formClassName, + fieldClassName, + labelClassName, + inputContainerClassName, + inputClassName, + ref, + disabled, +}: BaseFormProps) => { + const form = useForm({ + defaultValues, + }) + + useImperativeHandle(ref, () => { + return { + getForm() { + return form + }, + } + }, [form]) + + const renderField = useCallback((field: AnyFieldApi) => { + const formSchema = formSchemas?.find(schema => schema.name === field.name) + + if (formSchema) { + return ( + + ) + } + + return null + }, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled]) + + const renderFieldWrapper = useCallback((formSchema: FormSchema) => { + const { + name, + } = formSchema + + return ( + + {renderField} + + ) + }, [renderField, form]) + + if (!formSchemas?.length) + return null + + return ( +
{ + e.preventDefault() + form?.handleSubmit() + }} + > + {formSchemas.map(renderFieldWrapper)} +
+ ) +} + +export default memo(BaseForm) diff --git a/web/app/components/base/form/components/base/index.tsx b/web/app/components/base/form/components/base/index.tsx new file mode 100644 index 0000000000..0d6f0808ff --- /dev/null +++ b/web/app/components/base/form/components/base/index.tsx @@ -0,0 +1,2 @@ +export { default as BaseForm, type BaseFormProps } from './base-form' +export { default as BaseField, type BaseFieldProps } from './base-field' diff --git a/web/app/components/base/form/form-scenarios/auth/index.tsx b/web/app/components/base/form/form-scenarios/auth/index.tsx new file mode 100644 index 0000000000..5a88f94ac6 --- /dev/null +++ b/web/app/components/base/form/form-scenarios/auth/index.tsx @@ -0,0 +1,21 @@ +import { memo } from 'react' +import { BaseForm } from '../../components/base' +import type { BaseFormProps } from '../../components/base' + +const AuthForm = ({ + formSchemas = [], + defaultValues, + ref, +}: BaseFormProps) => { + return ( + + ) +} + +export default memo(AuthForm) diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts new file mode 100644 index 0000000000..02a93c3285 --- /dev/null +++ b/web/app/components/base/form/types.ts @@ -0,0 +1,66 @@ +import type { + ForwardedRef, + ReactNode, +} from 'react' +import type { + AnyFormApi, + FieldValidators, +} from '@tanstack/react-form' + +export type TypeWithI18N = { + en_US: T + zh_Hans: T + [key: string]: T +} + +export type FormShowOnObject = { + variable: string + value: string +} + +export enum FormTypeEnum { + textInput = 'text-input', + textNumber = 'number-input', + secretInput = 'secret-input', + select = 'select', + radio = 'radio', + boolean = 'boolean', + files = 'files', + file = 'file', + modelSelector = 'model-selector', + toolSelector = 'tool-selector', + multiToolSelector = 'array[tools]', + appSelector = 'app-selector', + dynamicSelect = 'dynamic-select', +} + +export type FormOption = { + label: TypeWithI18N | string + value: string + show_on?: FormShowOnObject[] + icon?: string +} + +export type FormSchema = { + type: FormTypeEnum + name: string + label: string | ReactNode | TypeWithI18N + required: boolean + default?: any + tooltip?: string | TypeWithI18N + show_on?: FormShowOnObject[] + url?: string + scope?: string + help?: string | TypeWithI18N + placeholder?: string | TypeWithI18N + options?: FormOption[] + labelClassName?: string + validators?: FieldValidators +} + +export type FormValues = Record + +export type FormRefObject = { + getForm: () => AnyFormApi +} +export type FormRef = ForwardedRef diff --git a/web/app/components/base/icons/assets/vender/line/general/search-menu.svg b/web/app/components/base/icons/assets/vender/line/general/search-menu.svg new file mode 100644 index 0000000000..f61f69f4ba --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/general/search-menu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/app/components/base/icons/assets/vender/system/auto-update-line.svg b/web/app/components/base/icons/assets/vender/system/auto-update-line.svg new file mode 100644 index 0000000000..c6bff78400 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/system/auto-update-line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/app/components/base/icons/script.mjs b/web/app/components/base/icons/script.mjs index 7f9d7b73a0..1b5994edef 100644 --- a/web/app/components/base/icons/script.mjs +++ b/web/app/components/base/icons/script.mjs @@ -75,7 +75,7 @@ Icon.displayName = '<%= svgName %>' export default Icon `.trim()) - await writeFile(path.resolve(currentPath, `${fileName}.json`), JSON.stringify(svgData, '', '\t')) + await writeFile(path.resolve(currentPath, `${fileName}.json`), `${JSON.stringify(svgData, '', '\t')}\n`) await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ svgName: fileName })}\n`) const indexingRender = template(` diff --git a/web/app/components/base/icons/src/public/tracing/AliyunIcon.json b/web/app/components/base/icons/src/public/tracing/AliyunIcon.json index 5cbb52c237..9a0b89f20a 100644 --- a/web/app/components/base/icons/src/public/tracing/AliyunIcon.json +++ b/web/app/components/base/icons/src/public/tracing/AliyunIcon.json @@ -1,118 +1,131 @@ { - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "xmlns": "http://www.w3.org/2000/svg", - "xmlns:xlink": "http://www.w3.org/1999/xlink", - "fill": "none", - "version": "1.1", - "width": "65", - "height": "16", - "viewBox": "0 0 65 16" - }, - "children": [ - { - "type": "element", - "name": "defs", - "children": [ - { - "type": "element", - "name": "clipPath", - "attributes": { - "id": "master_svg0_42_34281" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "x": "0", - "y": "0", - "width": "19", - "height": "16", - "rx": "0" - } - } - ] - } - ] - }, - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "g", - "attributes": { - "clip-path": "url(#master_svg0_42_34281)" - }, - "children": [ - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M4.06862,14.6667C3.79213,14.6667,3.45463,14.5688,3.05614,14.373C2.97908,14.3351,2.92692,14.3105,2.89968,14.2992C2.33193,14.0628,1.82911,13.7294,1.39123,13.2989C0.463742,12.3871,0,11.2874,0,10C0,8.71258,0.463742,7.61293,1.39123,6.70107C2.16172,5.94358,3.06404,5.50073,4.09819,5.37252C4.23172,3.98276,4.81755,2.77756,5.85569,1.75693C7.04708,0.585642,8.4857,0,10.1716,0C11.5256,0,12.743,0.396982,13.8239,1.19095C14.8847,1.97019,15.61,2.97855,16,4.21604L14.7045,4.61063C14.4016,3.64918,13.8374,2.86532,13.0121,2.25905C12.1719,1.64191,11.2251,1.33333,10.1716,1.33333C8.8602,1.33333,7.74124,1.7888,6.81467,2.69974C5.88811,3.61067,5.42483,4.71076,5.42483,6L5.42483,6.66667L4.74673,6.66667C3.81172,6.66667,3.01288,6.99242,2.35021,7.64393C1.68754,8.2954,1.35621,9.08076,1.35621,10C1.35621,10.9192,1.68754,11.7046,2.35021,12.3561C2.66354,12.6641,3.02298,12.9026,3.42852,13.0714C3.48193,13.0937,3.55988,13.13,3.66237,13.1803C3.87004,13.2823,4.00545,13.3333,4.06862,13.3333L4.06862,14.6667Z", - "fill-rule": "evenodd", - "fill": "#FF6A00", - "fill-opacity": "1" - } - } - ] - }, - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M13.458613505859375,7.779393492279053C12.975613505859375,7.717463492279053,12.484813505859375,7.686503492279053,11.993983505859376,7.686503492279053C11.152583505859376,7.686503492279053,10.303403505859375,7.779393492279053,9.493183505859374,7.941943492279053C8.682953505859375,8.104503492279052,7.903893505859375,8.359943492279053,7.155983505859375,8.654083492279053C6.657383505859375,8.870823492279053,6.158783505859375,9.128843492279053,5.660181505859375,9.428153492279053C5.332974751859375,9.621673492279053,5.239486705859375,10.070633492279054,5.434253505859375,10.395743492279053L7.413073505859375,13.298533492279052C7.639003505859375,13.623603492279052,8.090863505859375,13.716463492279052,8.418073505859375,13.523003492279052C8.547913505859375,13.435263492279052,8.763453505859374,13.326893492279053,9.064693505859374,13.197863492279053C9.516553505859374,13.004333492279052,9.976203505859374,12.872733492279053,10.459223505859375,12.779863492279052C10.942243505859375,12.679263492279052,11.433053505859375,12.617333492279052,11.955023505859375,12.617333492279052L13.380683505859375,7.810353492279052L13.458613505859375,7.779393492279053ZM15.273813505859374,8.135463492279053L15.016753505859375,5.333333492279053L13.458613505859375,7.787133492279053C13.817013505859375,7.818093492279052,14.144213505859375,7.880023492279053,14.494743505859375,7.949683492279053C14.494743505859375,7.944523492279053,14.754433505859375,8.006453492279054,15.273813505859374,8.135463492279053ZM12.064083505859376,12.648273492279053L11.378523505859375,14.970463492279054L12.515943505859376,16.00003349227905L14.074083505859376,15.643933492279054L14.525943505859376,13.027603492279052C14.198743505859374,12.934663492279054,13.879283505859375,12.834063492279054,13.552083505859375,12.772133492279053C13.069083505859375,12.717933492279052,12.578283505859375,12.648273492279053,12.064083505859376,12.648273492279053ZM18.327743505859374,9.428153492279053C17.829143505859374,9.128843492279053,17.330543505859374,8.870823492279053,16.831943505859375,8.654083492279053C16.348943505859374,8.460573492279053,15.826943505859376,8.267053492279054,15.305013505859375,8.135463492279053L15.305013505859375,8.267053492279054L14.463613505859374,13.043063492279053C14.596083505859376,13.105003492279053,14.759683505859375,13.135933492279053,14.884283505859376,13.205603492279053C15.185523505859376,13.334623492279052,15.401043505859375,13.443003492279052,15.530943505859375,13.530733492279053C15.858143505859376,13.724263492279054,16.341143505859375,13.623603492279052,16.535943505859375,13.306263492279053L18.514743505859375,10.403483492279053C18.779643505859376,10.039673492279054,18.686143505859377,9.621673492279053,18.327743505859374,9.428153492279053Z", - "fill": "#FF6A00", - "fill-opacity": "1" - } - } - ] - } - ] - } - ] - }, - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M25.044,2.668L34.676,2.668L34.676,4.04L25.044,4.04L25.044,2.668ZM29.958,7.82Q29.258,9.066,28.355,10.41Q27.451999999999998,11.754,26.92,12.3L32.506,11.782Q31.442,10.158,30.84,9.346L32.058,8.562000000000001Q32.786,9.5,33.843,11.012Q34.9,12.524,35.516,13.546L34.214,14.526Q33.891999999999996,13.966,33.346000000000004,13.098Q32.016,13.182,29.734,13.378Q27.451999999999998,13.574,25.87,13.742L25.31,13.812L24.834,13.882L24.414,12.468Q24.708,12.37,24.862000000000002,12.265Q25.016,12.16,25.121,12.069Q25.226,11.978,25.268,11.936Q25.912,11.32,26.724,10.165Q27.536,9.01,28.208,7.82L23.854,7.82L23.854,6.434L35.866,6.434L35.866,7.82L29.958,7.82ZM42.656,7.414L42.656,8.576L41.354,8.576L41.354,1.814L42.656,1.87L42.656,7.036Q43.314,5.846,43.888000000000005,4.369Q44.462,2.892,44.714,1.6600000000000001L46.086,1.981999Q45.96,2.612,45.722,3.41L49.6,3.41L49.6,4.74L45.274,4.74Q44.616,6.56,43.706,8.128L42.656,7.414ZM38.596000000000004,2.346L39.884,2.402L39.884,8.212L38.596000000000004,8.212L38.596000000000004,2.346ZM46.184,4.964Q46.688,5.356,47.5,6.175Q48.312,6.994,48.788,7.582L47.751999999999995,8.59Q47.346000000000004,8.072,46.576,7.274Q45.806,6.476,45.204,5.902L46.184,4.964ZM48.41,9.01L48.41,12.706L49.894,12.706L49.894,13.966L37.391999999999996,13.966L37.391999999999996,12.706L38.848,12.706L38.848,9.01L48.41,9.01ZM41.676,10.256L40.164,10.256L40.164,12.706L41.676,12.706L41.676,10.256ZM42.908,12.706L44.364000000000004,12.706L44.364000000000004,10.256L42.908,10.256L42.908,12.706ZM45.582,12.706L47.108000000000004,12.706L47.108000000000004,10.256L45.582,10.256L45.582,12.706ZM54.906,7.456L55.116,8.394L54.178,8.814L54.178,12.818Q54.178,13.434,54.031,13.735Q53.884,14.036,53.534,14.162Q53.184,14.288,52.456,14.358L51.867999999999995,14.414L51.476,13.084L52.162,13.028Q52.512,13,52.652,12.958Q52.792,12.916,52.841,12.797Q52.89,12.678,52.89,12.384L52.89,9.36Q51.980000000000004,9.724,51.322,9.948L51.013999999999996,8.576Q51.798,8.324,52.89,7.876L52.89,5.524L51.42,5.524L51.42,4.166L52.89,4.166L52.89,1.7579989999999999L54.178,1.814L54.178,4.166L55.214,4.166L55.214,5.524L54.178,5.524L54.178,7.316L54.808,7.022L54.906,7.456ZM56.894,4.5440000000000005L56.894,6.098L55.564,6.098L55.564,3.256L58.686,3.256Q58.42,2.346,58.266,1.9260000000000002L59.624,1.7579989999999999Q59.848,2.276,60.142,3.256L63.25,3.256L63.25,6.098L61.962,6.098L61.962,4.5440000000000005L56.894,4.5440000000000005ZM59.008,6.322Q58.392,6.938,57.685,7.512Q56.978,8.086,55.956,8.841999999999999L55.242,7.764Q56.824,6.728,58.126,5.37L59.008,6.322ZM60.422,5.37Q61.024,5.776,62.095,6.581Q63.166,7.386,63.656,7.806L62.942,8.982Q62.368,8.45,61.332,7.652Q60.296,6.854,59.666,6.434L60.422,5.37ZM62.592,10.256L60.044,10.256L60.044,12.566L63.572,12.566L63.572,13.826L55.144,13.826L55.144,12.566L58.63,12.566L58.63,10.256L56.054,10.256L56.054,8.982L62.592,8.982L62.592,10.256Z", - "fill": "#FF6A00", - "fill-opacity": "1" - } - } - ] - } - ] - } - ] - } - ] - }, - "name": "AliyunIcon" + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "fill": "none", + "version": "1.1", + "width": "106", + "height": "16", + "viewBox": "0 0 106 16" + }, + "children": [ + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "clipPath", + "attributes": { + "id": "master_svg0_36_00924" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "x": "0", + "y": "0", + "width": "19", + "height": "16", + "rx": "0" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "clip-path": "url(#master_svg0_36_00924)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.06862,14.6667C3.79213,14.6667,3.45463,14.5688,3.05614,14.373C2.97908,14.3351,2.92692,14.3105,2.89968,14.2992C2.33193,14.0628,1.82911,13.7294,1.39123,13.2989C0.463742,12.3871,0,11.2874,0,10C0,8.71258,0.463742,7.61293,1.39123,6.70107C2.16172,5.94358,3.06404,5.50073,4.09819,5.37252C4.23172,3.98276,4.81755,2.77756,5.85569,1.75693C7.04708,0.585642,8.4857,0,10.1716,0C11.5256,0,12.743,0.396982,13.8239,1.19095C14.8847,1.97019,15.61,2.97855,16,4.21604L14.7045,4.61063C14.4016,3.64918,13.8374,2.86532,13.0121,2.25905C12.1719,1.64191,11.2251,1.33333,10.1716,1.33333C8.8602,1.33333,7.74124,1.7888,6.81467,2.69974C5.88811,3.61067,5.42483,4.71076,5.42483,6L5.42483,6.66667L4.74673,6.66667C3.81172,6.66667,3.01288,6.99242,2.35021,7.64393C1.68754,8.2954,1.35621,9.08076,1.35621,10C1.35621,10.9192,1.68754,11.7046,2.35021,12.3561C2.66354,12.6641,3.02298,12.9026,3.42852,13.0714C3.48193,13.0937,3.55988,13.13,3.66237,13.1803C3.87004,13.2823,4.00545,13.3333,4.06862,13.3333L4.06862,14.6667Z", + "fill-rule": "evenodd", + "fill": "#000000", + "fill-opacity": "1", + "style": "mix-blend-mode:passthrough" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M13.458613505859375,7.779393492279053C12.975613505859375,7.717463492279053,12.484813505859375,7.686503492279053,11.993983505859376,7.686503492279053C11.152583505859376,7.686503492279053,10.303403505859375,7.779393492279053,9.493183505859374,7.941943492279053C8.682953505859375,8.104503492279052,7.903893505859375,8.359943492279053,7.155983505859375,8.654083492279053C6.657383505859375,8.870823492279053,6.158783505859375,9.128843492279053,5.660181505859375,9.428153492279053C5.332974751859375,9.621673492279053,5.239486705859375,10.070633492279054,5.434253505859375,10.395743492279053L7.413073505859375,13.298533492279052C7.639003505859375,13.623603492279052,8.090863505859375,13.716463492279052,8.418073505859375,13.523003492279052C8.547913505859375,13.435263492279052,8.763453505859374,13.326893492279053,9.064693505859374,13.197863492279053C9.516553505859374,13.004333492279052,9.976203505859374,12.872733492279053,10.459223505859375,12.779863492279052C10.942243505859375,12.679263492279052,11.433053505859375,12.617333492279052,11.955023505859375,12.617333492279052L13.380683505859375,7.810353492279052L13.458613505859375,7.779393492279053ZM15.273813505859374,8.135463492279053L15.016753505859375,5.333333492279053L13.458613505859375,7.787133492279053C13.817013505859375,7.818093492279052,14.144213505859375,7.880023492279053,14.494743505859375,7.949683492279053C14.494743505859375,7.944523492279053,14.754433505859375,8.006453492279054,15.273813505859374,8.135463492279053ZM12.064083505859376,12.648273492279053L11.378523505859375,14.970463492279054L12.515943505859376,16.00003349227905L14.074083505859376,15.643933492279054L14.525943505859376,13.027603492279052C14.198743505859374,12.934663492279054,13.879283505859375,12.834063492279054,13.552083505859375,12.772133492279053C13.069083505859375,12.717933492279052,12.578283505859375,12.648273492279053,12.064083505859376,12.648273492279053ZM18.327743505859374,9.428153492279053C17.829143505859374,9.128843492279053,17.330543505859374,8.870823492279053,16.831943505859375,8.654083492279053C16.348943505859374,8.460573492279053,15.826943505859376,8.267053492279054,15.305013505859375,8.135463492279053L15.305013505859375,8.267053492279054L14.463613505859374,13.043063492279053C14.596083505859376,13.105003492279053,14.759683505859375,13.135933492279053,14.884283505859376,13.205603492279053C15.185523505859376,13.334623492279052,15.401043505859375,13.443003492279052,15.530943505859375,13.530733492279053C15.858143505859376,13.724263492279054,16.341143505859375,13.623603492279052,16.535943505859375,13.306263492279053L18.514743505859375,10.403483492279053C18.779643505859376,10.039673492279054,18.686143505859377,9.621673492279053,18.327743505859374,9.428153492279053Z", + "fill": "#000000", + "fill-opacity": "1", + "style": "mix-blend-mode:passthrough" + }, + "children": [] + } + ] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M36.174,12.958L35.278,14.358Q31.344,11.964,29.958,8.884Q29.258,10.48,27.9,11.81Q26.542,13.14,24.624,14.372L23.644,12.986Q28.642,10.186,29.034,6.378L24.12,6.378L24.12,4.95L29.076,4.95L29.076,1.743999L30.616,1.7999990000000001L30.616,4.95L35.614000000000004,4.95L35.614000000000004,6.378L30.588,6.378L30.573999999999998,6.56Q31.078,8.646,32.408,10.144Q33.738,11.642,36.174,12.958ZM44.658,4.922000000000001L43.454,4.922000000000001L43.454,4.152L41.745999999999995,4.152L41.745999999999995,2.948L43.454,2.948L43.454,1.716L44.658,1.771999L44.658,2.948L46.492000000000004,2.948L46.492000000000004,1.716L47.682,1.771999L47.682,2.948L49.586,2.948L49.586,4.152L47.682,4.152L47.682,4.922000000000001L46.492000000000004,4.922000000000001L46.492000000000004,4.152L44.658,4.152L44.658,4.922000000000001ZM46.519999999999996,11.474Q47.010000000000005,12.146,47.870999999999995,12.615Q48.732,13.084,50.104,13.364L49.726,14.624Q46.884,13.924,45.61,12.286Q45.106,13.042,44.111999999999995,13.616Q43.117999999999995,14.19,41.507999999999996,14.638L41.004000000000005,13.42Q42.488,13.098,43.349000000000004,12.615Q44.21,12.132,44.574,11.474L41.522,11.474L41.522,10.368L44.896,10.368Q44.91,10.312,44.91,10.214Q44.924,10.032,44.924,9.542L42.152,9.542L42.152,8.492L41.284,9.108Q40.989999999999995,8.464,40.5,7.708L40.5,14.358L39.282,14.358L39.282,8.268Q38.61,9.99,37.952,11.082L36.944,10.074Q37.532,9.122,38.106,7.939Q38.68,6.756,39.058,5.664L37.658,5.664L37.658,4.390000000000001L39.282,4.390000000000001L39.282,1.7579989999999999L40.5,1.814L40.5,4.390000000000001L41.816,4.390000000000001L41.816,5.664L40.5,5.664L40.5,7.134L41.116,6.658Q41.704,7.386,42.152,8.24L42.152,5.244L48.97,5.244L48.97,9.542L46.226,9.542Q46.198,10.018,46.198,10.214L46.198,10.368L49.641999999999996,10.368L49.641999999999996,11.474L46.519999999999996,11.474ZM47.85,6.952L47.85,6.28L43.314,6.28L43.314,6.952L47.85,6.952ZM47.85,7.862L43.314,7.862L43.314,8.506L47.85,8.506L47.85,7.862ZM59.904,9.388L59.512,8.114L60.548,8.030000000000001Q61.066,7.988,61.234,7.855Q61.402,7.722,61.402,7.274L61.402,2.01L62.704,2.066L62.704,7.624Q62.704,8.268,62.55,8.604Q62.396,8.940000000000001,62.025,9.094Q61.654,9.248,60.94,9.304L59.904,9.388ZM51.546,9.276Q52.274,8.52,52.596000000000004,7.988Q52.918,7.456,53.016,6.784L51.518,6.784L51.518,5.566L53.1,5.566L53.1,5.188L53.1,3.718L51.867999999999995,3.718L51.867999999999995,2.458L58.448,2.458L58.448,3.718L57.244,3.718L57.244,5.566L58.728,5.566L58.728,6.784L57.244,6.784L57.244,9.206L55.928,9.206L55.928,6.784L54.332,6.784Q54.22,7.792,53.842,8.52Q53.464,9.248,52.61,10.102L51.546,9.276ZM59.092,2.724L60.366,2.7800000000000002L60.366,7.61L59.092,7.61L59.092,2.724ZM54.402,3.718L54.402,5.202L54.402,5.566L55.928,5.566L55.928,3.718L54.402,3.718ZM58.126,11.348L58.126,12.86L63.53,12.86L63.53,14.106L51.322,14.106L51.322,12.86L56.74,12.86L56.74,11.348L52.75,11.348L52.75,10.13L56.74,10.13L56.74,9.332L58.126,9.388L58.126,10.13L62.13,10.13L62.13,11.348L58.126,11.348ZM77.39,2.528L77.39,3.9L75.64,3.9L75.64,12.272Q75.64,13.098,75.465,13.49Q75.28999999999999,13.882,74.84899999999999,14.05Q74.408,14.218,73.47,14.302L72.56,14.386L72.126,13L73.19,12.916Q73.68,12.874,73.89699999999999,12.79Q74.114,12.706,74.184,12.51Q74.25399999999999,12.314,74.25399999999999,11.88L74.25399999999999,3.9L65.042,3.9L65.042,2.528L77.39,2.528ZM66.512,5.524L72.26599999999999,5.524L72.26599999999999,11.712L66.512,11.712L66.512,5.524ZM67.842,10.354L70.95,10.354L70.95,6.896L67.842,6.896L67.842,10.354ZM88.772,3.648L85.118,3.648L85.118,10.298L83.80199999999999,10.298L83.80199999999999,2.332L90.088,2.332L90.088,10.27L88.772,10.27L88.772,3.648ZM82.668,12.65Q82.23400000000001,11.712,81.632,10.522Q80.862,12.146,79.518,14.092L78.45400000000001,13.182Q80.036,11.068,80.89,9.024Q79.7,6.728,79,5.552L80.02199999999999,4.894Q80.48400000000001,5.622,81.47800000000001,7.386Q81.87,6.042,82.122,4.2780000000000005L79.02799999999999,4.2780000000000005L79.02799999999999,2.934L83.47999999999999,2.934L83.47999999999999,4.2780000000000005Q83.144,6.784,82.318,8.940000000000001Q83.158,10.508,83.774,11.782L82.668,12.65ZM91.166,11.264Q91.124,12.104,91.04,12.636Q90.956,13.28,90.802,13.602Q90.648,13.924,90.326,14.064Q90.004,14.204,89.374,14.204L88.142,14.204Q87.344,14.204,87.029,13.868Q86.714,13.532,86.714,12.636L86.714,11.11Q86.21000000000001,12.104,85.356,12.972Q84.50200000000001,13.84,83.2,14.708L82.332,13.56Q83.886,12.608,84.691,11.705Q85.49600000000001,10.802,85.804,9.745Q86.112,8.687999999999999,86.168,7.05L86.21000000000001,4.306L87.61,4.362L87.568,7.218Q87.526,8.366,87.344,9.276L88.016,9.304L88.016,12.16Q88.016,12.608,88.128,12.734Q88.24,12.86,88.632,12.86L89.108,12.86Q89.486,12.86,89.619,12.741Q89.752,12.622,89.808,12.174Q89.892,11.362,89.892,10.788L91.166,11.264ZM93.56,1.884Q94.036,2.206,94.68,2.759Q95.324,3.312,95.702,3.704L94.904,4.795999999999999Q94.596,4.418,93.938,3.809Q93.28,3.2,92.832,2.85L93.56,1.884ZM102.1,12.93Q102.478,12.888,102.653,12.832Q102.828,12.776,102.898,12.636Q102.968,12.496,102.968,12.188L102.968,1.981999L104.06,2.0380000000000003L104.06,12.608Q104.06,13.238,103.948,13.546Q103.836,13.854,103.521,13.994Q103.206,14.134,102.534,14.19L101.75,14.246L101.372,12.986L102.1,12.93ZM95.702,10.774L95.702,2.5140000000000002L100.168,2.5140000000000002L100.168,10.732L99.006,10.732L99.006,3.732L96.836,3.732L96.836,10.774L95.702,10.774ZM101.008,11.152L101.008,3.2L102.1,3.256L102.1,11.152L101.008,11.152ZM94.652,13.364Q95.856,12.482,96.43,11.789Q97.004,11.096,97.2,10.277Q97.396,9.458,97.396,8.058L97.396,4.362L98.488,4.418L98.488,8.058Q98.488,9.738,98.201,10.809Q97.914,11.88,97.277,12.664Q96.64,13.448,95.45,14.344L94.652,13.364ZM93.07,5.034Q93.546,5.37,94.197,5.937Q94.848,6.504,95.282,6.952L94.484,8.072Q94.078,7.61,93.427,7.015Q92.776,6.42,92.258,6.028L93.07,5.034ZM92.524,13.742Q92.748,13.126,93.266,11.278Q93.784,9.43,93.896,8.814L94.498,9.01L95.072,9.206Q94.89,10.032,94.421,11.733Q93.952,13.434,93.714,14.162L92.524,13.742ZM98.74,10.858Q99.888,11.908,100.714,12.958L99.888,13.868Q99.356,13.154,98.943,12.671Q98.53,12.188,97.984,11.684L98.74,10.858Z", + "fill": "#000000", + "fill-opacity": "1" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + }, + "name": "AliyunIcon" } diff --git a/web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx b/web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx index 5b062b8a86..c7f785d9fb 100644 --- a/web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx +++ b/web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx @@ -4,12 +4,16 @@ import * as React from 'react' import data from './AliyunIcon.json' import IconBase from '@/app/components/base/icons/IconBase' -import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' -const Icon = React.forwardRef, Omit>(( - props, - ref, -) => ) +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => Icon.displayName = 'AliyunIcon' diff --git a/web/app/components/base/icons/src/public/tracing/AliyunIconBig.json b/web/app/components/base/icons/src/public/tracing/AliyunIconBig.json index ea60744daf..c8093ba660 100644 --- a/web/app/components/base/icons/src/public/tracing/AliyunIconBig.json +++ b/web/app/components/base/icons/src/public/tracing/AliyunIconBig.json @@ -1,71 +1,117 @@ { - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "xmlns": "http://www.w3.org/2000/svg", - "xmlns:xlink": "http://www.w3.org/1999/xlink", - "fill": "none", - "version": "1.1", - "width": "96", - "height": "24", - "viewBox": "0 0 96 24" - }, - "children": [ - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M6.10294,22C5.68819,22,5.18195,21.8532,4.58421,21.5595C4.46861,21.5027,4.39038,21.4658,4.34951,21.4488C3.49789,21.0943,2.74367,20.5941,2.08684,19.9484C0.695613,18.5806,0,16.9311,0,15C0,13.0689,0.695612,11.4194,2.08684,10.0516C3.24259,8.91537,4.59607,8.2511,6.14728,8.05878C6.34758,5.97414,7.22633,4.16634,8.78354,2.63539C10.5706,0.878463,12.7286,0,15.2573,0C17.2884,0,19.1146,0.595472,20.7358,1.78642C22.327,2.95528,23.4151,4.46783,24,6.32406L22.0568,6.91594C21.6024,5.47377,20.7561,4.29798,19.5181,3.38858C18.2579,2.46286,16.8377,2,15.2573,2C13.2903,2,11.6119,2.6832,10.222,4.04961C8.83217,5.41601,8.13725,7.06614,8.13725,9L8.13725,10L7.12009,10C5.71758,10,4.51932,10.4886,3.52532,11.4659C2.53132,12.4431,2.03431,13.6211,2.03431,15C2.03431,16.3789,2.53132,17.5569,3.52532,18.5341C3.99531,18.9962,4.53447,19.3538,5.14278,19.6071C5.2229,19.6405,5.33983,19.695,5.49356,19.7705C5.80505,19.9235,6.00818,20,6.10294,20L6.10294,22Z", - "fill-rule": "evenodd", - "fill": "#FF6A00", - "fill-opacity": "1" - } - } - ] - }, - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M20.18796103515625,11.66909C19.46346103515625,11.5762,18.72726103515625,11.52975,17.991011035156248,11.52975C16.728921035156247,11.52975,15.45515103515625,11.66909,14.23981103515625,11.91292C13.02447103515625,12.156749999999999,11.85588103515625,12.539909999999999,10.73402103515625,12.98113C9.98612103515625,13.306239999999999,9.23822103515625,13.69327,8.49031803515625,14.14223C7.99950790415625,14.43251,7.85927603515625,15.10595,8.15142503515625,15.59361L11.11966103515625,19.9478C11.45855103515625,20.4354,12.13634103515625,20.5747,12.627151035156249,20.2845C12.821921035156251,20.152900000000002,13.14523103515625,19.990299999999998,13.59708103515625,19.796799999999998C14.27487103515625,19.506500000000003,14.964341035156249,19.3091,15.68887103515625,19.169800000000002C16.413401035156248,19.018900000000002,17.14962103515625,18.926000000000002,17.93258103515625,18.926000000000002L20.071061035156248,11.715530000000001L20.18796103515625,11.66909ZM22.91076103515625,12.20319L22.525161035156252,8L20.18796103515625,11.6807C20.72556103515625,11.72714,21.21636103515625,11.82003,21.74216103515625,11.92453C21.74216103515625,11.91679,22.13166103515625,12.00968,22.91076103515625,12.20319ZM18.09616103515625,18.9724L17.06782103515625,22.4557L18.773961035156248,24L21.11116103515625,23.465899999999998L21.788961035156248,19.5414C21.298161035156248,19.402,20.81896103515625,19.2511,20.32816103515625,19.1582C19.60366103515625,19.076900000000002,18.86746103515625,18.9724,18.09616103515625,18.9724ZM27.49166103515625,14.14223C26.74376103515625,13.69327,25.99586103515625,13.306239999999999,25.24796103515625,12.98113C24.52346103515625,12.69086,23.74046103515625,12.40058,22.95756103515625,12.20319L22.95756103515625,12.40058L21.69546103515625,19.5646C21.89416103515625,19.6575,22.139561035156248,19.7039,22.32646103515625,19.8084C22.77836103515625,20.0019,23.101661035156248,20.1645,23.29646103515625,20.2961C23.78726103515625,20.586399999999998,24.51176103515625,20.4354,24.80396103515625,19.959400000000002L27.77216103515625,15.605229999999999C28.16946103515625,15.05951,28.02926103515625,14.43251,27.49166103515625,14.14223Z", - "fill": "#FF6A00", - "fill-opacity": "1" - } - } - ] - }, - { - "type": "element", - "name": "g", - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M35.785,3.8624638671875L50.233000000000004,3.8624638671875L50.233000000000004,5.9204638671875L35.785,5.9204638671875L35.785,3.8624638671875ZM43.156,11.5904638671875Q42.106,13.4594638671875,40.7515,15.4754638671875Q39.397,17.4914638671875,38.599000000000004,18.3104638671875L46.978,17.5334638671875Q45.382,15.0974638671875,44.479,13.8794638671875L46.306,12.7034638671875Q47.397999999999996,14.1104638671875,48.9835,16.3784638671875Q50.569,18.6464638671875,51.492999999999995,20.1794638671875L49.54,21.6494638671875Q49.057,20.8094638671875,48.238,19.5074638671875Q46.243,19.6334638671875,42.82,19.9274638671875Q39.397,20.2214638671875,37.024,20.4734638671875L36.184,20.5784638671875L35.47,20.6834638671875L34.84,18.5624638671875Q35.281,18.4154638671875,35.512,18.2579638671875Q35.743,18.1004638671875,35.9005,17.963963867187502Q36.058,17.8274638671875,36.121,17.7644638671875Q37.087,16.840463867187502,38.305,15.1079638671875Q39.522999999999996,13.3754638671875,40.531,11.5904638671875L34,11.5904638671875L34,9.5114638671875L52.018,9.5114638671875L52.018,11.5904638671875L43.156,11.5904638671875ZM62.203,10.9814638671875L62.203,12.7244638671875L60.25,12.7244638671875L60.25,2.5814638671875L62.203,2.6654638671875L62.203,10.4144638671875Q63.19,8.6294638671875,64.051,6.4139638671875Q64.912,4.1984638671875,65.28999999999999,2.3504638671875L67.348,2.8334628671875Q67.15899999999999,3.7784638671875,66.80199999999999,4.9754638671875L72.619,4.9754638671875L72.619,6.9704638671875L66.13,6.9704638671875Q65.143,9.7004638671875,63.778,12.0524638671875L62.203,10.9814638671875ZM56.113,3.3794638671875L58.045,3.4634638671875L58.045,12.1784638671875L56.113,12.1784638671875L56.113,3.3794638671875ZM67.495,7.3064638671875Q68.251,7.8944638671875,69.469,9.1229638671875Q70.687,10.3514638671875,71.40100000000001,11.2334638671875L69.84700000000001,12.7454638671875Q69.238,11.9684638671875,68.083,10.7714638671875Q66.928,9.5744638671875,66.025,8.7134638671875L67.495,7.3064638671875ZM70.834,13.3754638671875L70.834,18.9194638671875L73.06,18.9194638671875L73.06,20.8094638671875L54.307,20.8094638671875L54.307,18.9194638671875L56.491,18.9194638671875L56.491,13.3754638671875L70.834,13.3754638671875ZM60.733000000000004,15.2444638671875L58.465,15.2444638671875L58.465,18.9194638671875L60.733000000000004,18.9194638671875L60.733000000000004,15.2444638671875ZM62.581,18.9194638671875L64.765,18.9194638671875L64.765,15.2444638671875L62.581,15.2444638671875L62.581,18.9194638671875ZM66.592,18.9194638671875L68.881,18.9194638671875L68.881,15.2444638671875L66.592,15.2444638671875L66.592,18.9194638671875ZM80.578,11.0444638671875L80.893,12.4514638671875L79.48599999999999,13.0814638671875L79.48599999999999,19.0874638671875Q79.48599999999999,20.0114638671875,79.2655,20.4629638671875Q79.045,20.9144638671875,78.52000000000001,21.1034638671875Q77.995,21.2924638671875,76.90299999999999,21.3974638671875L76.021,21.4814638671875L75.43299999999999,19.4864638671875L76.462,19.4024638671875Q76.987,19.3604638671875,77.197,19.2974638671875Q77.407,19.2344638671875,77.4805,19.0559638671875Q77.554,18.8774638671875,77.554,18.4364638671875L77.554,13.9004638671875Q76.189,14.4464638671875,75.202,14.7824638671875L74.74000000000001,12.7244638671875Q75.916,12.3464638671875,77.554,11.6744638671875L77.554,8.1464638671875L75.34899999999999,8.1464638671875L75.34899999999999,6.1094638671875L77.554,6.1094638671875L77.554,2.4974628671875L79.48599999999999,2.5814638671875L79.48599999999999,6.1094638671875L81.03999999999999,6.1094638671875L81.03999999999999,8.1464638671875L79.48599999999999,8.1464638671875L79.48599999999999,10.8344638671875L80.431,10.3934638671875L80.578,11.0444638671875ZM83.56,6.6764638671875L83.56,9.0074638671875L81.565,9.0074638671875L81.565,4.7444638671875L86.24799999999999,4.7444638671875Q85.84899999999999,3.3794638671875,85.618,2.7494638671875L87.655,2.4974628671875Q87.991,3.2744638671875,88.432,4.7444638671875L93.094,4.7444638671875L93.094,9.0074638671875L91.162,9.0074638671875L91.162,6.6764638671875L83.56,6.6764638671875ZM86.731,9.3434638671875Q85.807,10.2674638671875,84.7465,11.1284638671875Q83.686,11.9894638671875,82.15299999999999,13.1234638671875L81.082,11.5064638671875Q83.455,9.9524638671875,85.408,7.9154638671875L86.731,9.3434638671875ZM88.852,7.9154638671875Q89.755,8.5244638671875,91.3615,9.731963867187499Q92.968,10.9394638671875,93.703,11.5694638671875L92.632,13.3334638671875Q91.771,12.5354638671875,90.217,11.3384638671875Q88.663,10.1414638671875,87.718,9.5114638671875L88.852,7.9154638671875ZM92.107,15.2444638671875L88.285,15.2444638671875L88.285,18.7094638671875L93.577,18.7094638671875L93.577,20.5994638671875L80.935,20.5994638671875L80.935,18.7094638671875L86.164,18.7094638671875L86.164,15.2444638671875L82.3,15.2444638671875L82.3,13.3334638671875L92.107,13.3334638671875L92.107,15.2444638671875Z", - "fill": "#FF6A00", - "fill-opacity": "1" - } - } - ] - } - ] - } - ] - }, - "name": "AliyunBigIcon" + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "fill": "none", + "version": "1.1", + "width": "159", + "height": "24", + "viewBox": "0 0 159 24" + }, + "children": [ + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "clipPath", + "attributes": { + "id": "master_svg0_42_18775" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "x": "0", + "y": "0", + "width": "28.5", + "height": "24", + "rx": "0" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "clip-path": "url(#master_svg0_42_18775)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M6.10294,22C5.68819,22,5.18195,21.8532,4.58421,21.5595C4.46861,21.5027,4.39038,21.4658,4.34951,21.4488C3.49789,21.0943,2.74367,20.5941,2.08684,19.9484C0.695613,18.5806,0,16.9311,0,15C0,13.0689,0.695612,11.4194,2.08684,10.0516C3.24259,8.91537,4.59607,8.2511,6.14728,8.05878C6.34758,5.97414,7.22633,4.16634,8.78354,2.63539C10.5706,0.878463,12.7286,0,15.2573,0C17.2884,0,19.1146,0.595472,20.7358,1.78642C22.327,2.95528,23.4151,4.46783,24,6.32406L22.0568,6.91594C21.6024,5.47377,20.7561,4.29798,19.5181,3.38858C18.2579,2.46286,16.8377,2,15.2573,2C13.2903,2,11.6119,2.6832,10.222,4.04961C8.83217,5.41601,8.13725,7.06614,8.13725,9L8.13725,10L7.12009,10C5.71758,10,4.51932,10.4886,3.52532,11.4659C2.53132,12.4431,2.03431,13.6211,2.03431,15C2.03431,16.3789,2.53132,17.5569,3.52532,18.5341C3.99531,18.9962,4.53447,19.3538,5.14278,19.6071C5.2229,19.6405,5.33983,19.695,5.49356,19.7705C5.80505,19.9235,6.00818,20,6.10294,20L6.10294,22Z", + "fill-rule": "evenodd", + "fill": "#000000", + "fill-opacity": "1", + "style": "mix-blend-mode:passthrough" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M20.18796103515625,11.66909C19.46346103515625,11.5762,18.72726103515625,11.52975,17.991011035156248,11.52975C16.728921035156247,11.52975,15.45515103515625,11.66909,14.23981103515625,11.91292C13.02447103515625,12.156749999999999,11.85588103515625,12.539909999999999,10.73402103515625,12.98113C9.98612103515625,13.306239999999999,9.23822103515625,13.69327,8.49031803515625,14.14223C7.99950790415625,14.43251,7.85927603515625,15.10595,8.15142503515625,15.59361L11.11966103515625,19.9478C11.45855103515625,20.4354,12.13634103515625,20.5747,12.627151035156249,20.2845C12.821921035156251,20.152900000000002,13.14523103515625,19.990299999999998,13.59708103515625,19.796799999999998C14.27487103515625,19.506500000000003,14.964341035156249,19.3091,15.68887103515625,19.169800000000002C16.413401035156248,19.018900000000002,17.14962103515625,18.926000000000002,17.93258103515625,18.926000000000002L20.071061035156248,11.715530000000001L20.18796103515625,11.66909ZM22.91076103515625,12.20319L22.525161035156252,8L20.18796103515625,11.6807C20.72556103515625,11.72714,21.21636103515625,11.82003,21.74216103515625,11.92453C21.74216103515625,11.91679,22.13166103515625,12.00968,22.91076103515625,12.20319ZM18.09616103515625,18.9724L17.06782103515625,22.4557L18.773961035156248,24L21.11116103515625,23.465899999999998L21.788961035156248,19.5414C21.298161035156248,19.402,20.81896103515625,19.2511,20.32816103515625,19.1582C19.60366103515625,19.076900000000002,18.86746103515625,18.9724,18.09616103515625,18.9724ZM27.49166103515625,14.14223C26.74376103515625,13.69327,25.99586103515625,13.306239999999999,25.24796103515625,12.98113C24.52346103515625,12.69086,23.74046103515625,12.40058,22.95756103515625,12.20319L22.95756103515625,12.40058L21.69546103515625,19.5646C21.89416103515625,19.6575,22.139561035156248,19.7039,22.32646103515625,19.8084C22.77836103515625,20.0019,23.101661035156248,20.1645,23.29646103515625,20.2961C23.78726103515625,20.586399999999998,24.51176103515625,20.4354,24.80396103515625,19.959400000000002L27.77216103515625,15.605229999999999C28.16946103515625,15.05951,28.02926103515625,14.43251,27.49166103515625,14.14223Z", + "fill": "#000000", + "fill-opacity": "1", + "style": "mix-blend-mode:passthrough" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M53.295,19.1189814453125L51.951,21.2189814453125Q46.05,17.6279814453125,43.971000000000004,13.0079814453125Q42.921,15.4019814453125,40.884,17.3969814453125Q38.847,19.3919814453125,35.97,21.2399814453125L34.5,19.1609814453125Q41.997,14.9609814453125,42.585,9.2489814453125L35.214,9.2489814453125L35.214,7.1069814453125L42.647999999999996,7.1069814453125L42.647999999999996,2.2979812453125L44.958,2.3819804453125L44.958,7.1069814453125L52.455,7.1069814453125L52.455,9.2489814453125L44.916,9.2489814453125L44.894999999999996,9.5219814453125Q45.650999999999996,12.6509814453125,47.646,14.8979814453125Q49.641,17.1449814453125,53.295,19.1189814453125ZM66.021,7.0649814453125L64.215,7.0649814453125L64.215,5.9099814453125L61.653,5.9099814453125L61.653,4.1039814453125L64.215,4.1039814453125L64.215,2.2559814453125L66.021,2.3399810453125L66.021,4.1039814453125L68.77199999999999,4.1039814453125L68.77199999999999,2.2559814453125L70.557,2.3399810453125L70.557,4.1039814453125L73.413,4.1039814453125L73.413,5.9099814453125L70.557,5.9099814453125L70.557,7.0649814453125L68.77199999999999,7.0649814453125L68.77199999999999,5.9099814453125L66.021,5.9099814453125L66.021,7.0649814453125ZM68.814,16.8929814453125Q69.549,17.9009814453125,70.84049999999999,18.6044814453125Q72.132,19.3079814453125,74.19,19.7279814453125L73.62299999999999,21.6179814453125Q69.36,20.5679814453125,67.449,18.1109814453125Q66.693,19.2449814453125,65.202,20.1059814453125Q63.711,20.9669814453125,61.296,21.6389814453125L60.54,19.8119814453125Q62.766,19.3289814453125,64.0575,18.6044814453125Q65.349,17.879981445312502,65.895,16.8929814453125L61.317,16.8929814453125L61.317,15.2339814453125L66.378,15.2339814453125Q66.399,15.1499814453125,66.399,15.0029814453125Q66.42,14.7299814453125,66.42,13.9949814453125L62.262,13.9949814453125L62.262,12.4199814453125L60.96,13.3439814453125Q60.519,12.3779814453125,59.784,11.2439814453125L59.784,21.2189814453125L57.957,21.2189814453125L57.957,12.0839814453125Q56.949,14.6669814453125,55.962,16.3049814453125L54.45,14.7929814453125Q55.332,13.3649814453125,56.193,11.5904814453125Q57.054,9.815981445312499,57.620999999999995,8.1779814453125L55.521,8.1779814453125L55.521,6.2669814453125L57.957,6.2669814453125L57.957,2.3189811453125L59.784,2.4029824453125L59.784,6.2669814453125L61.757999999999996,6.2669814453125L61.757999999999996,8.1779814453125L59.784,8.1779814453125L59.784,10.3829814453125L60.708,9.6689814453125Q61.59,10.7609814453125,62.262,12.0419814453125L62.262,7.5479814453125L72.489,7.5479814453125L72.489,13.9949814453125L68.37299999999999,13.9949814453125Q68.331,14.7089814453125,68.331,15.0029814453125L68.331,15.2339814453125L73.497,15.2339814453125L73.497,16.8929814453125L68.814,16.8929814453125ZM70.809,10.1099814453125L70.809,9.1019814453125L64.005,9.1019814453125L64.005,10.1099814453125L70.809,10.1099814453125ZM70.809,11.4749814453125L64.005,11.4749814453125L64.005,12.4409814453125L70.809,12.4409814453125L70.809,11.4749814453125ZM88.89,13.7639814453125L88.30199999999999,11.8529814453125L89.856,11.7269814453125Q90.63300000000001,11.6639814453125,90.88499999999999,11.4644814453125Q91.137,11.2649814453125,91.137,10.5929814453125L91.137,2.6969814453125L93.09,2.7809824453125L93.09,11.1179814453125Q93.09,12.0839814453125,92.85900000000001,12.5879814453125Q92.628,13.0919814453125,92.0715,13.3229814453125Q91.515,13.5539814453125,90.444,13.6379814453125L88.89,13.7639814453125ZM76.35300000000001,13.5959814453125Q77.445,12.4619814453125,77.928,11.6639814453125Q78.411,10.8659814453125,78.55799999999999,9.8579814453125L76.311,9.8579814453125L76.311,8.0309814453125L78.684,8.0309814453125L78.684,7.4639814453125L78.684,5.2589814453125L76.836,5.2589814453125L76.836,3.3689814453125L86.706,3.3689814453125L86.706,5.2589814453125L84.9,5.2589814453125L84.9,8.0309814453125L87.126,8.0309814453125L87.126,9.8579814453125L84.9,9.8579814453125L84.9,13.4909814453125L82.926,13.4909814453125L82.926,9.8579814453125L80.532,9.8579814453125Q80.364,11.3699814453125,79.797,12.4619814453125Q79.22999999999999,13.5539814453125,77.949,14.8349814453125L76.35300000000001,13.5959814453125ZM87.672,3.7679814453125L89.583,3.8519814453125L89.583,11.0969814453125L87.672,11.0969814453125L87.672,3.7679814453125ZM80.637,5.2589814453125L80.637,7.4849814453125L80.637,8.0309814453125L82.926,8.0309814453125L82.926,5.2589814453125L80.637,5.2589814453125ZM86.223,16.7039814453125L86.223,18.9719814453125L94.32900000000001,18.9719814453125L94.32900000000001,20.8409814453125L76.017,20.8409814453125L76.017,18.9719814453125L84.144,18.9719814453125L84.144,16.7039814453125L78.15899999999999,16.7039814453125L78.15899999999999,14.8769814453125L84.144,14.8769814453125L84.144,13.6799814453125L86.223,13.7639814453125L86.223,14.8769814453125L92.229,14.8769814453125L92.229,16.7039814453125L86.223,16.7039814453125ZM115.119,3.4739814453125L115.119,5.5319814453125L112.494,5.5319814453125L112.494,18.0899814453125Q112.494,19.3289814453125,112.2315,19.9169814453125Q111.969,20.5049814453125,111.3075,20.7569814453125Q110.646,21.0089814453125,109.239,21.1349814453125L107.874,21.2609814453125L107.223,19.1819814453125L108.819,19.0559814453125Q109.554,18.9929814453125,109.8795,18.8669814453125Q110.205,18.7409814453125,110.31,18.4469814453125Q110.415,18.1529814453125,110.415,17.501981445312502L110.415,5.5319814453125L96.59700000000001,5.5319814453125L96.59700000000001,3.4739814453125L115.119,3.4739814453125ZM98.802,7.9679814453125L107.433,7.9679814453125L107.433,17.2499814453125L98.802,17.2499814453125L98.802,7.9679814453125ZM100.797,15.2129814453125L105.459,15.2129814453125L105.459,10.0259814453125L100.797,10.0259814453125L100.797,15.2129814453125ZM132.192,5.1539814453125L126.711,5.1539814453125L126.711,15.1289814453125L124.737,15.1289814453125L124.737,3.1799814453125L134.166,3.1799814453125L134.166,15.0869814453125L132.192,15.0869814453125L132.192,5.1539814453125ZM123.036,18.6569814453125Q122.385,17.2499814453125,121.482,15.4649814453125Q120.327,17.9009814453125,118.311,20.8199814453125L116.715,19.4549814453125Q119.088,16.2839814453125,120.369,13.2179814453125Q118.584,9.7739814453125,117.534,8.0099814453125L119.067,7.0229814453125Q119.76,8.1149814453125,121.251,10.7609814453125Q121.839,8.7449814453125,122.217,6.0989814453125L117.576,6.0989814453125L117.576,4.0829814453125L124.254,4.0829814453125L124.254,6.0989814453125Q123.75,9.8579814453125,122.511,13.0919814453125Q123.771,15.4439814453125,124.695,17.3549814453125L123.036,18.6569814453125ZM135.78300000000002,16.5779814453125Q135.72,17.8379814453125,135.594,18.6359814453125Q135.46800000000002,19.6019814453125,135.237,20.0849814453125Q135.006,20.5679814453125,134.523,20.7779814453125Q134.04000000000002,20.9879814453125,133.095,20.9879814453125L131.247,20.9879814453125Q130.05,20.9879814453125,129.5775,20.4839814453125Q129.10500000000002,19.9799814453125,129.10500000000002,18.6359814453125L129.10500000000002,16.3469814453125Q128.349,17.8379814453125,127.068,19.1399814453125Q125.787,20.4419814453125,123.834,21.7439814453125L122.532,20.0219814453125Q124.863,18.5939814453125,126.0705,17.2394814453125Q127.278,15.8849814453125,127.74,14.2994814453125Q128.202,12.7139814453125,128.286,10.2569814453125L128.349,6.1409814453125L130.449,6.224981445312499L130.386,10.5089814453125Q130.32299999999998,12.2309814453125,130.05,13.5959814453125L131.058,13.6379814453125L131.058,17.9219814453125Q131.058,18.5939814453125,131.226,18.7829814453125Q131.394,18.9719814453125,131.982,18.9719814453125L132.696,18.9719814453125Q133.263,18.9719814453125,133.4625,18.7934814453125Q133.662,18.6149814453125,133.74599999999998,17.942981445312498Q133.872,16.7249814453125,133.872,15.8639814453125L135.78300000000002,16.5779814453125ZM139.374,2.5079814453125Q140.088,2.9909814453125,141.054,3.8204814453125Q142.01999999999998,4.6499814453125,142.587,5.2379814453125L141.39,6.8759814453125Q140.928,6.3089814453125,139.941,5.3954814453125Q138.954,4.4819814453125,138.28199999999998,3.9569814453125L139.374,2.5079814453125ZM152.184,19.0769814453125Q152.751,19.0139814453125,153.014,18.9299814453125Q153.276,18.8459814453125,153.381,18.6359814453125Q153.486,18.4259814453125,153.486,17.9639814453125L153.486,2.6549814453125L155.124,2.7389824453125L155.124,18.5939814453125Q155.124,19.5389814453125,154.95600000000002,20.0009814453125Q154.788,20.4629814453125,154.315,20.6729814453125Q153.84300000000002,20.8829814453125,152.83499999999998,20.9669814453125L151.659,21.0509814453125L151.09199999999998,19.1609814453125L152.184,19.0769814453125ZM142.587,15.8429814453125L142.587,3.4529814453125L149.286,3.4529814453125L149.286,15.7799814453125L147.543,15.7799814453125L147.543,5.2799814453125L144.288,5.2799814453125L144.288,15.8429814453125L142.587,15.8429814453125ZM150.546,16.4099814453125L150.546,4.4819814453125L152.184,4.5659814453125005L152.184,16.4099814453125L150.546,16.4099814453125ZM141.012,19.7279814453125Q142.81799999999998,18.4049814453125,143.679,17.3654814453125Q144.54000000000002,16.3259814453125,144.834,15.0974814453125Q145.128,13.8689814453125,145.128,11.7689814453125L145.128,6.224981445312499L146.76600000000002,6.3089814453125L146.76600000000002,11.7689814453125Q146.76600000000002,14.2889814453125,146.33499999999998,15.8954814453125Q145.905,17.501981445312502,144.95,18.6779814453125Q143.994,19.8539814453125,142.209,21.1979814453125L141.012,19.7279814453125ZM138.639,7.2329814453125Q139.353,7.7369814453125,140.329,8.5874814453125Q141.30599999999998,9.4379814453125,141.957,10.1099814453125L140.76,11.7899814453125Q140.151,11.0969814453125,139.174,10.2044814453125Q138.19799999999998,9.311981445312501,137.421,8.7239814453125L138.639,7.2329814453125ZM137.82,20.2949814453125Q138.156,19.3709814453125,138.933,16.5989814453125Q139.70999999999998,13.8269814453125,139.878,12.9029814453125L140.781,13.1969814453125L141.642,13.4909814453125Q141.369,14.7299814453125,140.66500000000002,17.2814814453125Q139.962,19.8329814453125,139.60500000000002,20.9249814453125L137.82,20.2949814453125ZM147.144,15.9689814453125Q148.86599999999999,17.5439814453125,150.10500000000002,19.1189814453125L148.86599999999999,20.4839814453125Q148.06799999999998,19.4129814453125,147.449,18.6884814453125Q146.829,17.9639814453125,146.01,17.207981445312498L147.144,15.9689814453125Z", + "fill": "#000000", + "fill-opacity": "1" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "AliyunIconBig" } diff --git a/web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx b/web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx index 0924f70fbd..703ea1d37f 100644 --- a/web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx +++ b/web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx @@ -4,12 +4,16 @@ import * as React from 'react' import data from './AliyunIconBig.json' import IconBase from '@/app/components/base/icons/IconBase' -import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' -const Icon = React.forwardRef, Omit>(( - props, - ref, -) => ) +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => Icon.displayName = 'AliyunIconBig' diff --git a/web/app/components/base/icons/src/public/tracing/WeaveIcon.tsx b/web/app/components/base/icons/src/public/tracing/WeaveIcon.tsx index fd66bd79ae..9261604bfe 100644 --- a/web/app/components/base/icons/src/public/tracing/WeaveIcon.tsx +++ b/web/app/components/base/icons/src/public/tracing/WeaveIcon.tsx @@ -4,12 +4,16 @@ import * as React from 'react' import data from './WeaveIcon.json' import IconBase from '@/app/components/base/icons/IconBase' -import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' -const Icon = React.forwardRef, Omit>(( - props, - ref, -) => ) +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => Icon.displayName = 'WeaveIcon' diff --git a/web/app/components/base/icons/src/public/tracing/WeaveIconBig.tsx b/web/app/components/base/icons/src/public/tracing/WeaveIconBig.tsx index 1d2bb9fcc2..79267467db 100644 --- a/web/app/components/base/icons/src/public/tracing/WeaveIconBig.tsx +++ b/web/app/components/base/icons/src/public/tracing/WeaveIconBig.tsx @@ -4,12 +4,16 @@ import * as React from 'react' import data from './WeaveIconBig.json' import IconBase from '@/app/components/base/icons/IconBase' -import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' -const Icon = React.forwardRef, Omit>(( - props, - ref, -) => ) +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => Icon.displayName = 'WeaveIconBig' diff --git a/web/app/components/base/icons/src/public/tracing/index.ts b/web/app/components/base/icons/src/public/tracing/index.ts index 07e3385f46..2d9e7ff0a5 100644 --- a/web/app/components/base/icons/src/public/tracing/index.ts +++ b/web/app/components/base/icons/src/public/tracing/index.ts @@ -1,3 +1,5 @@ +export { default as AliyunIconBig } from './AliyunIconBig' +export { default as AliyunIcon } from './AliyunIcon' export { default as ArizeIconBig } from './ArizeIconBig' export { default as ArizeIcon } from './ArizeIcon' export { default as LangfuseIconBig } from './LangfuseIconBig' @@ -11,5 +13,3 @@ export { default as PhoenixIcon } from './PhoenixIcon' export { default as TracingIcon } from './TracingIcon' export { default as WeaveIconBig } from './WeaveIconBig' export { default as WeaveIcon } from './WeaveIcon' -export { default as AliyunIconBig } from './AliyunIconBig' -export { default as AliyunIcon } from './AliyunIcon' diff --git a/web/app/components/base/icons/src/vender/features/Citations.json b/web/app/components/base/icons/src/vender/features/Citations.json index 1b0b6250de..24a77db601 100644 --- a/web/app/components/base/icons/src/vender/features/Citations.json +++ b/web/app/components/base/icons/src/vender/features/Citations.json @@ -23,4 +23,4 @@ ] }, "name": "Citations" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/ContentModeration.json b/web/app/components/base/icons/src/vender/features/ContentModeration.json index 4f5c47acd2..fc609e0434 100644 --- a/web/app/components/base/icons/src/vender/features/ContentModeration.json +++ b/web/app/components/base/icons/src/vender/features/ContentModeration.json @@ -25,4 +25,4 @@ ] }, "name": "ContentModeration" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/Document.json b/web/app/components/base/icons/src/vender/features/Document.json index fdd08d5254..f0638eecf7 100644 --- a/web/app/components/base/icons/src/vender/features/Document.json +++ b/web/app/components/base/icons/src/vender/features/Document.json @@ -20,4 +20,4 @@ ] }, "name": "Document" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/FolderUpload.json b/web/app/components/base/icons/src/vender/features/FolderUpload.json index 2180127e3d..c113da043b 100644 --- a/web/app/components/base/icons/src/vender/features/FolderUpload.json +++ b/web/app/components/base/icons/src/vender/features/FolderUpload.json @@ -23,4 +23,4 @@ ] }, "name": "FolderUpload" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/LoveMessage.json b/web/app/components/base/icons/src/vender/features/LoveMessage.json index 7dbc062662..4874b94944 100644 --- a/web/app/components/base/icons/src/vender/features/LoveMessage.json +++ b/web/app/components/base/icons/src/vender/features/LoveMessage.json @@ -23,4 +23,4 @@ ] }, "name": "LoveMessage" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/MessageFast.json b/web/app/components/base/icons/src/vender/features/MessageFast.json index 4580398f31..b859b1f3f0 100644 --- a/web/app/components/base/icons/src/vender/features/MessageFast.json +++ b/web/app/components/base/icons/src/vender/features/MessageFast.json @@ -25,4 +25,4 @@ ] }, "name": "MessageFast" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/Microphone01.json b/web/app/components/base/icons/src/vender/features/Microphone01.json index a4ba1bc23f..57545716cf 100644 --- a/web/app/components/base/icons/src/vender/features/Microphone01.json +++ b/web/app/components/base/icons/src/vender/features/Microphone01.json @@ -34,4 +34,4 @@ ] }, "name": "Microphone01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/TextToAudio.json b/web/app/components/base/icons/src/vender/features/TextToAudio.json index 1d824f72cc..4369e0b6f1 100644 --- a/web/app/components/base/icons/src/vender/features/TextToAudio.json +++ b/web/app/components/base/icons/src/vender/features/TextToAudio.json @@ -74,4 +74,4 @@ ] }, "name": "TextToAudio" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/VirtualAssistant.json b/web/app/components/base/icons/src/vender/features/VirtualAssistant.json index b426eb4b0b..3cbeba0ea5 100644 --- a/web/app/components/base/icons/src/vender/features/VirtualAssistant.json +++ b/web/app/components/base/icons/src/vender/features/VirtualAssistant.json @@ -32,4 +32,4 @@ ] }, "name": "VirtualAssistant" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/features/Vision.json b/web/app/components/base/icons/src/vender/features/Vision.json index e9b5b4df85..6d60e32a29 100644 --- a/web/app/components/base/icons/src/vender/features/Vision.json +++ b/web/app/components/base/icons/src/vender/features/Vision.json @@ -25,4 +25,4 @@ ] }, "name": "Vision" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/alertsAndFeedback/AlertTriangle.json b/web/app/components/base/icons/src/vender/line/alertsAndFeedback/AlertTriangle.json index a200e6035e..057ecbdfc2 100644 --- a/web/app/components/base/icons/src/vender/line/alertsAndFeedback/AlertTriangle.json +++ b/web/app/components/base/icons/src/vender/line/alertsAndFeedback/AlertTriangle.json @@ -36,4 +36,4 @@ ] }, "name": "AlertTriangle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json b/web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json index b9ccbef3ec..41877c74bd 100644 --- a/web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json +++ b/web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json @@ -63,4 +63,4 @@ ] }, "name": "ThumbsDown" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json b/web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json index 674516b1c5..0ee442871f 100644 --- a/web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json +++ b/web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json @@ -63,4 +63,4 @@ ] }, "name": "ThumbsUp" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/arrows/ArrowNarrowLeft.json b/web/app/components/base/icons/src/vender/line/arrows/ArrowNarrowLeft.json index 73d6708c51..e3f7b5c674 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/ArrowNarrowLeft.json +++ b/web/app/components/base/icons/src/vender/line/arrows/ArrowNarrowLeft.json @@ -26,4 +26,4 @@ ] }, "name": "ArrowNarrowLeft" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/arrows/ArrowUpRight.json b/web/app/components/base/icons/src/vender/line/arrows/ArrowUpRight.json index 9ab1e6e0d0..621a37afdf 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/ArrowUpRight.json +++ b/web/app/components/base/icons/src/vender/line/arrows/ArrowUpRight.json @@ -36,4 +36,4 @@ ] }, "name": "ArrowUpRight" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/arrows/ChevronDownDouble.json b/web/app/components/base/icons/src/vender/line/arrows/ChevronDownDouble.json index cfae43931c..706e13533c 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/ChevronDownDouble.json +++ b/web/app/components/base/icons/src/vender/line/arrows/ChevronDownDouble.json @@ -36,4 +36,4 @@ ] }, "name": "ChevronDownDouble" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/arrows/ChevronRight.json b/web/app/components/base/icons/src/vender/line/arrows/ChevronRight.json index c144e678bb..e03a2c8dd0 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/ChevronRight.json +++ b/web/app/components/base/icons/src/vender/line/arrows/ChevronRight.json @@ -36,4 +36,4 @@ ] }, "name": "ChevronRight" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json b/web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json index 84da1f3dbb..67ff40298d 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json +++ b/web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json @@ -26,4 +26,4 @@ ] }, "name": "ChevronSelectorVertical" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/arrows/RefreshCcw01.json b/web/app/components/base/icons/src/vender/line/arrows/RefreshCcw01.json index 30033b41bd..8b3cb0d5a7 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/RefreshCcw01.json +++ b/web/app/components/base/icons/src/vender/line/arrows/RefreshCcw01.json @@ -26,4 +26,4 @@ ] }, "name": "RefreshCcw01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/arrows/RefreshCw05.json b/web/app/components/base/icons/src/vender/line/arrows/RefreshCw05.json index 5468171fe0..1ba0cedfd3 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/RefreshCw05.json +++ b/web/app/components/base/icons/src/vender/line/arrows/RefreshCw05.json @@ -26,4 +26,4 @@ ] }, "name": "RefreshCw05" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/arrows/ReverseLeft.json b/web/app/components/base/icons/src/vender/line/arrows/ReverseLeft.json index 48c6d1fbd6..b5173968c8 100644 --- a/web/app/components/base/icons/src/vender/line/arrows/ReverseLeft.json +++ b/web/app/components/base/icons/src/vender/line/arrows/ReverseLeft.json @@ -36,4 +36,4 @@ ] }, "name": "ReverseLeft" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/communication/AiText.json b/web/app/components/base/icons/src/vender/line/communication/AiText.json index 0f5ff57837..2473c64c22 100644 --- a/web/app/components/base/icons/src/vender/line/communication/AiText.json +++ b/web/app/components/base/icons/src/vender/line/communication/AiText.json @@ -36,4 +36,4 @@ ] }, "name": "AiText" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/communication/ChatBot.json b/web/app/components/base/icons/src/vender/line/communication/ChatBot.json index 69547f9353..0e7382d741 100644 --- a/web/app/components/base/icons/src/vender/line/communication/ChatBot.json +++ b/web/app/components/base/icons/src/vender/line/communication/ChatBot.json @@ -90,4 +90,4 @@ ] }, "name": "ChatBot" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/communication/ChatBotSlim.json b/web/app/components/base/icons/src/vender/line/communication/ChatBotSlim.json index 07f6cda56b..9be716acd6 100644 --- a/web/app/components/base/icons/src/vender/line/communication/ChatBotSlim.json +++ b/web/app/components/base/icons/src/vender/line/communication/ChatBotSlim.json @@ -65,4 +65,4 @@ ] }, "name": "ChatBotSlim" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/communication/CuteRobot.json b/web/app/components/base/icons/src/vender/line/communication/CuteRobot.json index 4ae74d2a77..35596bac9a 100644 --- a/web/app/components/base/icons/src/vender/line/communication/CuteRobot.json +++ b/web/app/components/base/icons/src/vender/line/communication/CuteRobot.json @@ -36,4 +36,4 @@ ] }, "name": "CuteRobot" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json b/web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json index a536c9f341..5d4202b2e1 100644 --- a/web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json +++ b/web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json @@ -36,4 +36,4 @@ ] }, "name": "MessageCheckRemove" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json b/web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json index 7d40cc7425..988e278325 100644 --- a/web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json +++ b/web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json @@ -26,4 +26,4 @@ ] }, "name": "MessageFastPlus" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json b/web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json index 7015ee281a..8bc500ea9b 100644 --- a/web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json +++ b/web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json @@ -26,4 +26,4 @@ ] }, "name": "ArtificialBrain" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/BarChartSquare02.json b/web/app/components/base/icons/src/vender/line/development/BarChartSquare02.json index 5b695a7e79..ef51cbec7f 100644 --- a/web/app/components/base/icons/src/vender/line/development/BarChartSquare02.json +++ b/web/app/components/base/icons/src/vender/line/development/BarChartSquare02.json @@ -36,4 +36,4 @@ ] }, "name": "BarChartSquare02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/BracketsX.json b/web/app/components/base/icons/src/vender/line/development/BracketsX.json index 08935cc7ff..2287a51073 100644 --- a/web/app/components/base/icons/src/vender/line/development/BracketsX.json +++ b/web/app/components/base/icons/src/vender/line/development/BracketsX.json @@ -26,4 +26,4 @@ ] }, "name": "BracketsX" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/CodeBrowser.json b/web/app/components/base/icons/src/vender/line/development/CodeBrowser.json index 1d0254d846..7234b42cea 100644 --- a/web/app/components/base/icons/src/vender/line/development/CodeBrowser.json +++ b/web/app/components/base/icons/src/vender/line/development/CodeBrowser.json @@ -36,4 +36,4 @@ ] }, "name": "CodeBrowser" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/Container.json b/web/app/components/base/icons/src/vender/line/development/Container.json index 3b15cd8f88..dbedb8aff3 100644 --- a/web/app/components/base/icons/src/vender/line/development/Container.json +++ b/web/app/components/base/icons/src/vender/line/development/Container.json @@ -26,4 +26,4 @@ ] }, "name": "Container" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/Database01.json b/web/app/components/base/icons/src/vender/line/development/Database01.json index e25b3e7cef..2be1974840 100644 --- a/web/app/components/base/icons/src/vender/line/development/Database01.json +++ b/web/app/components/base/icons/src/vender/line/development/Database01.json @@ -26,4 +26,4 @@ ] }, "name": "Database01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/Database03.json b/web/app/components/base/icons/src/vender/line/development/Database03.json index 5acf4bf1f9..24a004afed 100644 --- a/web/app/components/base/icons/src/vender/line/development/Database03.json +++ b/web/app/components/base/icons/src/vender/line/development/Database03.json @@ -26,4 +26,4 @@ ] }, "name": "Database03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/FileHeart02.json b/web/app/components/base/icons/src/vender/line/development/FileHeart02.json index ef9343dfc0..163e64b570 100644 --- a/web/app/components/base/icons/src/vender/line/development/FileHeart02.json +++ b/web/app/components/base/icons/src/vender/line/development/FileHeart02.json @@ -49,4 +49,4 @@ ] }, "name": "FileHeart02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/GitBranch01.json b/web/app/components/base/icons/src/vender/line/development/GitBranch01.json index 04205e57c6..f9d9d00e59 100644 --- a/web/app/components/base/icons/src/vender/line/development/GitBranch01.json +++ b/web/app/components/base/icons/src/vender/line/development/GitBranch01.json @@ -36,4 +36,4 @@ ] }, "name": "GitBranch01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/PromptEngineering.json b/web/app/components/base/icons/src/vender/line/development/PromptEngineering.json index c55bde8f57..97ca2e9353 100644 --- a/web/app/components/base/icons/src/vender/line/development/PromptEngineering.json +++ b/web/app/components/base/icons/src/vender/line/development/PromptEngineering.json @@ -62,4 +62,4 @@ ] }, "name": "PromptEngineering" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/PuzzlePiece01.json b/web/app/components/base/icons/src/vender/line/development/PuzzlePiece01.json index ce06d6125f..672e405ffa 100644 --- a/web/app/components/base/icons/src/vender/line/development/PuzzlePiece01.json +++ b/web/app/components/base/icons/src/vender/line/development/PuzzlePiece01.json @@ -63,4 +63,4 @@ ] }, "name": "PuzzlePiece01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/TerminalSquare.json b/web/app/components/base/icons/src/vender/line/development/TerminalSquare.json index 7a78b7b934..48fb6ce248 100644 --- a/web/app/components/base/icons/src/vender/line/development/TerminalSquare.json +++ b/web/app/components/base/icons/src/vender/line/development/TerminalSquare.json @@ -36,4 +36,4 @@ ] }, "name": "TerminalSquare" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/Variable.json b/web/app/components/base/icons/src/vender/line/development/Variable.json index b7545fe8ae..b366f11a06 100644 --- a/web/app/components/base/icons/src/vender/line/development/Variable.json +++ b/web/app/components/base/icons/src/vender/line/development/Variable.json @@ -59,4 +59,4 @@ ] }, "name": "Variable" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/development/Webhooks.json b/web/app/components/base/icons/src/vender/line/development/Webhooks.json index 452194deb3..bb9ccf1059 100644 --- a/web/app/components/base/icons/src/vender/line/development/Webhooks.json +++ b/web/app/components/base/icons/src/vender/line/development/Webhooks.json @@ -86,4 +86,4 @@ ] }, "name": "Webhooks" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/editor/AlignLeft.json b/web/app/components/base/icons/src/vender/line/editor/AlignLeft.json index ae8b150447..7ddf4d24c1 100644 --- a/web/app/components/base/icons/src/vender/line/editor/AlignLeft.json +++ b/web/app/components/base/icons/src/vender/line/editor/AlignLeft.json @@ -36,4 +36,4 @@ ] }, "name": "AlignLeft" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json b/web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json index bc87f9b00d..5f76ff1ac3 100644 --- a/web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json +++ b/web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json @@ -35,4 +35,4 @@ ] }, "name": "BezierCurve03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/editor/Collapse.json b/web/app/components/base/icons/src/vender/line/editor/Collapse.json index 5e3cf08ce0..224133e0b6 100644 --- a/web/app/components/base/icons/src/vender/line/editor/Collapse.json +++ b/web/app/components/base/icons/src/vender/line/editor/Collapse.json @@ -59,4 +59,4 @@ ] }, "name": "Collapse" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/editor/Colors.json b/web/app/components/base/icons/src/vender/line/editor/Colors.json index baee8ee347..0508092598 100644 --- a/web/app/components/base/icons/src/vender/line/editor/Colors.json +++ b/web/app/components/base/icons/src/vender/line/editor/Colors.json @@ -36,4 +36,4 @@ ] }, "name": "Colors" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/editor/ImageIndentLeft.json b/web/app/components/base/icons/src/vender/line/editor/ImageIndentLeft.json index 603696d969..33ba61f1c7 100644 --- a/web/app/components/base/icons/src/vender/line/editor/ImageIndentLeft.json +++ b/web/app/components/base/icons/src/vender/line/editor/ImageIndentLeft.json @@ -36,4 +36,4 @@ ] }, "name": "ImageIndentLeft" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/editor/LeftIndent02.json b/web/app/components/base/icons/src/vender/line/editor/LeftIndent02.json index 447ae887a9..cb77ee97ea 100644 --- a/web/app/components/base/icons/src/vender/line/editor/LeftIndent02.json +++ b/web/app/components/base/icons/src/vender/line/editor/LeftIndent02.json @@ -26,4 +26,4 @@ ] }, "name": "LeftIndent02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/editor/LetterSpacing01.json b/web/app/components/base/icons/src/vender/line/editor/LetterSpacing01.json index 98b3cd6617..e322926aa0 100644 --- a/web/app/components/base/icons/src/vender/line/editor/LetterSpacing01.json +++ b/web/app/components/base/icons/src/vender/line/editor/LetterSpacing01.json @@ -36,4 +36,4 @@ ] }, "name": "LetterSpacing01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/editor/TypeSquare.json b/web/app/components/base/icons/src/vender/line/editor/TypeSquare.json index 195b047746..b381c51420 100644 --- a/web/app/components/base/icons/src/vender/line/editor/TypeSquare.json +++ b/web/app/components/base/icons/src/vender/line/editor/TypeSquare.json @@ -35,4 +35,4 @@ ] }, "name": "TypeSquare" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/education/BookOpen01.json b/web/app/components/base/icons/src/vender/line/education/BookOpen01.json index bfa7941345..1c6f46b700 100644 --- a/web/app/components/base/icons/src/vender/line/education/BookOpen01.json +++ b/web/app/components/base/icons/src/vender/line/education/BookOpen01.json @@ -46,4 +46,4 @@ ] }, "name": "BookOpen01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/Clipboard.json b/web/app/components/base/icons/src/vender/line/files/Clipboard.json index f256747558..c8107fb74e 100644 --- a/web/app/components/base/icons/src/vender/line/files/Clipboard.json +++ b/web/app/components/base/icons/src/vender/line/files/Clipboard.json @@ -26,4 +26,4 @@ ] }, "name": "Clipboard" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/ClipboardCheck.json b/web/app/components/base/icons/src/vender/line/files/ClipboardCheck.json index 273b115001..dcedcaa47d 100644 --- a/web/app/components/base/icons/src/vender/line/files/ClipboardCheck.json +++ b/web/app/components/base/icons/src/vender/line/files/ClipboardCheck.json @@ -26,4 +26,4 @@ ] }, "name": "ClipboardCheck" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/File02.json b/web/app/components/base/icons/src/vender/line/files/File02.json index 110765adeb..6c0cf176c1 100644 --- a/web/app/components/base/icons/src/vender/line/files/File02.json +++ b/web/app/components/base/icons/src/vender/line/files/File02.json @@ -36,4 +36,4 @@ ] }, "name": "File02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/FileArrow01.json b/web/app/components/base/icons/src/vender/line/files/FileArrow01.json index 189f0814df..ce13dd0f5e 100644 --- a/web/app/components/base/icons/src/vender/line/files/FileArrow01.json +++ b/web/app/components/base/icons/src/vender/line/files/FileArrow01.json @@ -36,4 +36,4 @@ ] }, "name": "FileArrow01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/FileCheck02.json b/web/app/components/base/icons/src/vender/line/files/FileCheck02.json index 9a2e063c0a..0b08e61b30 100644 --- a/web/app/components/base/icons/src/vender/line/files/FileCheck02.json +++ b/web/app/components/base/icons/src/vender/line/files/FileCheck02.json @@ -36,4 +36,4 @@ ] }, "name": "FileCheck02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/FileDownload02.json b/web/app/components/base/icons/src/vender/line/files/FileDownload02.json index a0dccc280f..2c439adb31 100644 --- a/web/app/components/base/icons/src/vender/line/files/FileDownload02.json +++ b/web/app/components/base/icons/src/vender/line/files/FileDownload02.json @@ -26,4 +26,4 @@ ] }, "name": "FileDownload02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/FilePlus01.json b/web/app/components/base/icons/src/vender/line/files/FilePlus01.json index 67d8784494..470703abe1 100644 --- a/web/app/components/base/icons/src/vender/line/files/FilePlus01.json +++ b/web/app/components/base/icons/src/vender/line/files/FilePlus01.json @@ -36,4 +36,4 @@ ] }, "name": "FilePlus01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/FilePlus02.json b/web/app/components/base/icons/src/vender/line/files/FilePlus02.json index 447b1e91ba..cd55cad950 100644 --- a/web/app/components/base/icons/src/vender/line/files/FilePlus02.json +++ b/web/app/components/base/icons/src/vender/line/files/FilePlus02.json @@ -26,4 +26,4 @@ ] }, "name": "FilePlus02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/FileText.json b/web/app/components/base/icons/src/vender/line/files/FileText.json index 536bc45852..12335ec1c2 100644 --- a/web/app/components/base/icons/src/vender/line/files/FileText.json +++ b/web/app/components/base/icons/src/vender/line/files/FileText.json @@ -36,4 +36,4 @@ ] }, "name": "FileText" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/FileUpload.json b/web/app/components/base/icons/src/vender/line/files/FileUpload.json index 5dc2ec115e..6dfa30a350 100644 --- a/web/app/components/base/icons/src/vender/line/files/FileUpload.json +++ b/web/app/components/base/icons/src/vender/line/files/FileUpload.json @@ -49,4 +49,4 @@ ] }, "name": "FileUpload" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/files/Folder.json b/web/app/components/base/icons/src/vender/line/files/Folder.json index 6bbc4380ae..84e3cb9763 100644 --- a/web/app/components/base/icons/src/vender/line/files/Folder.json +++ b/web/app/components/base/icons/src/vender/line/files/Folder.json @@ -36,4 +36,4 @@ ] }, "name": "Folder" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json index c04fcda517..79bdc7024d 100644 --- a/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json @@ -26,4 +26,4 @@ ] }, "name": "Balance" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/CoinsStacked01.json b/web/app/components/base/icons/src/vender/line/financeAndECommerce/CoinsStacked01.json index 8a971909c8..328e38a1ce 100644 --- a/web/app/components/base/icons/src/vender/line/financeAndECommerce/CoinsStacked01.json +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/CoinsStacked01.json @@ -36,4 +36,4 @@ ] }, "name": "CoinsStacked01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json b/web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json index f10b5fa7cf..7a748bda5a 100644 --- a/web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json @@ -117,4 +117,4 @@ ] }, "name": "GoldCoin" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json b/web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json index 8e9c070875..ac3d3bdfe2 100644 --- a/web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json @@ -26,4 +26,4 @@ ] }, "name": "ReceiptList" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01.json b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01.json index b6f838d72f..82cd5af5f9 100644 --- a/web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01.json +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01.json @@ -63,4 +63,4 @@ ] }, "name": "Tag01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag03.json b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag03.json index ef0753b8d3..2fa00f62f7 100644 --- a/web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag03.json +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag03.json @@ -36,4 +36,4 @@ ] }, "name": "Tag03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/AtSign.json b/web/app/components/base/icons/src/vender/line/general/AtSign.json index 0722d8ff34..dd6d9cbbc7 100644 --- a/web/app/components/base/icons/src/vender/line/general/AtSign.json +++ b/web/app/components/base/icons/src/vender/line/general/AtSign.json @@ -63,4 +63,4 @@ ] }, "name": "AtSign" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Bookmark.json b/web/app/components/base/icons/src/vender/line/general/Bookmark.json index 1b6e517be7..378bc76be5 100644 --- a/web/app/components/base/icons/src/vender/line/general/Bookmark.json +++ b/web/app/components/base/icons/src/vender/line/general/Bookmark.json @@ -26,4 +26,4 @@ ] }, "name": "Bookmark" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Check.json b/web/app/components/base/icons/src/vender/line/general/Check.json index eae343816a..e3265f8138 100644 --- a/web/app/components/base/icons/src/vender/line/general/Check.json +++ b/web/app/components/base/icons/src/vender/line/general/Check.json @@ -36,4 +36,4 @@ ] }, "name": "Check" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/CheckDone01.json b/web/app/components/base/icons/src/vender/line/general/CheckDone01.json index 85355f93fd..ec3894aa00 100644 --- a/web/app/components/base/icons/src/vender/line/general/CheckDone01.json +++ b/web/app/components/base/icons/src/vender/line/general/CheckDone01.json @@ -36,4 +36,4 @@ ] }, "name": "CheckDone01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.json b/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.json index 737c69623d..6cf330994a 100644 --- a/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.json +++ b/web/app/components/base/icons/src/vender/line/general/ChecklistSquare.json @@ -33,4 +33,4 @@ ] }, "name": "ChecklistSquare" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/DotsGrid.json b/web/app/components/base/icons/src/vender/line/general/DotsGrid.json index 9aafed2f7b..85a24bb14c 100644 --- a/web/app/components/base/icons/src/vender/line/general/DotsGrid.json +++ b/web/app/components/base/icons/src/vender/line/general/DotsGrid.json @@ -131,4 +131,4 @@ ] }, "name": "DotsGrid" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Edit02.json b/web/app/components/base/icons/src/vender/line/general/Edit02.json index 38798fecf1..2d32d1da94 100644 --- a/web/app/components/base/icons/src/vender/line/general/Edit02.json +++ b/web/app/components/base/icons/src/vender/line/general/Edit02.json @@ -63,4 +63,4 @@ ] }, "name": "Edit02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Edit04.json b/web/app/components/base/icons/src/vender/line/general/Edit04.json index 73f275b732..34be957072 100644 --- a/web/app/components/base/icons/src/vender/line/general/Edit04.json +++ b/web/app/components/base/icons/src/vender/line/general/Edit04.json @@ -26,4 +26,4 @@ ] }, "name": "Edit04" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Edit05.json b/web/app/components/base/icons/src/vender/line/general/Edit05.json index 321336bc2f..f1bbf7138e 100644 --- a/web/app/components/base/icons/src/vender/line/general/Edit05.json +++ b/web/app/components/base/icons/src/vender/line/general/Edit05.json @@ -63,4 +63,4 @@ ] }, "name": "Edit05" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Hash02.json b/web/app/components/base/icons/src/vender/line/general/Hash02.json index 41b639f938..bd140198a1 100644 --- a/web/app/components/base/icons/src/vender/line/general/Hash02.json +++ b/web/app/components/base/icons/src/vender/line/general/Hash02.json @@ -35,4 +35,4 @@ ] }, "name": "Hash02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/InfoCircle.json b/web/app/components/base/icons/src/vender/line/general/InfoCircle.json index 4017e85ce1..6bc285c86b 100644 --- a/web/app/components/base/icons/src/vender/line/general/InfoCircle.json +++ b/web/app/components/base/icons/src/vender/line/general/InfoCircle.json @@ -63,4 +63,4 @@ ] }, "name": "InfoCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Link03.json b/web/app/components/base/icons/src/vender/line/general/Link03.json index ccd608f643..4728221b60 100644 --- a/web/app/components/base/icons/src/vender/line/general/Link03.json +++ b/web/app/components/base/icons/src/vender/line/general/Link03.json @@ -54,4 +54,4 @@ ] }, "name": "Link03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/LinkExternal02.json b/web/app/components/base/icons/src/vender/line/general/LinkExternal02.json index af445595c8..7016dd896f 100644 --- a/web/app/components/base/icons/src/vender/line/general/LinkExternal02.json +++ b/web/app/components/base/icons/src/vender/line/general/LinkExternal02.json @@ -35,4 +35,4 @@ ] }, "name": "LinkExternal02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/LogIn04.json b/web/app/components/base/icons/src/vender/line/general/LogIn04.json index a8316e9c27..27808a36a9 100644 --- a/web/app/components/base/icons/src/vender/line/general/LogIn04.json +++ b/web/app/components/base/icons/src/vender/line/general/LogIn04.json @@ -50,4 +50,4 @@ ] }, "name": "LogIn04" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/LogOut01.json b/web/app/components/base/icons/src/vender/line/general/LogOut01.json index bd2cb3e18c..d5c89394ef 100644 --- a/web/app/components/base/icons/src/vender/line/general/LogOut01.json +++ b/web/app/components/base/icons/src/vender/line/general/LogOut01.json @@ -36,4 +36,4 @@ ] }, "name": "LogOut01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/LogOut04.json b/web/app/components/base/icons/src/vender/line/general/LogOut04.json index a19bedfe4c..80a27ecf31 100644 --- a/web/app/components/base/icons/src/vender/line/general/LogOut04.json +++ b/web/app/components/base/icons/src/vender/line/general/LogOut04.json @@ -50,4 +50,4 @@ ] }, "name": "LogOut04" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Menu01.json b/web/app/components/base/icons/src/vender/line/general/Menu01.json index 5b32928738..5dfdebf7cc 100644 --- a/web/app/components/base/icons/src/vender/line/general/Menu01.json +++ b/web/app/components/base/icons/src/vender/line/general/Menu01.json @@ -36,4 +36,4 @@ ] }, "name": "Menu01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Pin01.json b/web/app/components/base/icons/src/vender/line/general/Pin01.json index b0e61a2373..3ad6bd8799 100644 --- a/web/app/components/base/icons/src/vender/line/general/Pin01.json +++ b/web/app/components/base/icons/src/vender/line/general/Pin01.json @@ -36,4 +36,4 @@ ] }, "name": "Pin01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Pin02.json b/web/app/components/base/icons/src/vender/line/general/Pin02.json index c5b51a5f33..474e7e102f 100644 --- a/web/app/components/base/icons/src/vender/line/general/Pin02.json +++ b/web/app/components/base/icons/src/vender/line/general/Pin02.json @@ -26,4 +26,4 @@ ] }, "name": "Pin02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Plus02.json b/web/app/components/base/icons/src/vender/line/general/Plus02.json index 8a9516f1ae..84b07b4251 100644 --- a/web/app/components/base/icons/src/vender/line/general/Plus02.json +++ b/web/app/components/base/icons/src/vender/line/general/Plus02.json @@ -36,4 +36,4 @@ ] }, "name": "Plus02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Refresh.json b/web/app/components/base/icons/src/vender/line/general/Refresh.json index 128dcb7d4d..693b9ab4f0 100644 --- a/web/app/components/base/icons/src/vender/line/general/Refresh.json +++ b/web/app/components/base/icons/src/vender/line/general/Refresh.json @@ -20,4 +20,4 @@ ] }, "name": "Refresh" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/SearchMenu.json b/web/app/components/base/icons/src/vender/line/general/SearchMenu.json new file mode 100644 index 0000000000..5854f25339 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/SearchMenu.json @@ -0,0 +1,77 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "32", + "height": "32", + "viewBox": "0 0 32 32", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.00488 16H6.67155", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.00488 9.33334H8.00488", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.00488 22.6667H8.00488", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M26 22L29.3333 25.3333", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + }, + "name": "SearchMenu" +} diff --git a/web/app/components/base/icons/src/vender/line/general/SearchMenu.tsx b/web/app/components/base/icons/src/vender/line/general/SearchMenu.tsx new file mode 100644 index 0000000000..4826abb20f --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/SearchMenu.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './SearchMenu.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => + +Icon.displayName = 'SearchMenu' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/general/Settings01.json b/web/app/components/base/icons/src/vender/line/general/Settings01.json index 8734e9f947..ca337d9b20 100644 --- a/web/app/components/base/icons/src/vender/line/general/Settings01.json +++ b/web/app/components/base/icons/src/vender/line/general/Settings01.json @@ -83,4 +83,4 @@ ] }, "name": "Settings01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Settings04.json b/web/app/components/base/icons/src/vender/line/general/Settings04.json index e46a0548ed..4dd34e68a4 100644 --- a/web/app/components/base/icons/src/vender/line/general/Settings04.json +++ b/web/app/components/base/icons/src/vender/line/general/Settings04.json @@ -36,4 +36,4 @@ ] }, "name": "Settings04" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Target04.json b/web/app/components/base/icons/src/vender/line/general/Target04.json index 5c07628bae..731fcc208c 100644 --- a/web/app/components/base/icons/src/vender/line/general/Target04.json +++ b/web/app/components/base/icons/src/vender/line/general/Target04.json @@ -62,4 +62,4 @@ ] }, "name": "Target04" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/Upload03.json b/web/app/components/base/icons/src/vender/line/general/Upload03.json index c3490f3cff..bda73041a9 100644 --- a/web/app/components/base/icons/src/vender/line/general/Upload03.json +++ b/web/app/components/base/icons/src/vender/line/general/Upload03.json @@ -63,4 +63,4 @@ ] }, "name": "Upload03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/UploadCloud01.json b/web/app/components/base/icons/src/vender/line/general/UploadCloud01.json index 03e448d7ad..5e5411c1cf 100644 --- a/web/app/components/base/icons/src/vender/line/general/UploadCloud01.json +++ b/web/app/components/base/icons/src/vender/line/general/UploadCloud01.json @@ -39,4 +39,4 @@ ] }, "name": "UploadCloud01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/X.json b/web/app/components/base/icons/src/vender/line/general/X.json index 5c2fde5df6..9c482caa25 100644 --- a/web/app/components/base/icons/src/vender/line/general/X.json +++ b/web/app/components/base/icons/src/vender/line/general/X.json @@ -36,4 +36,4 @@ ] }, "name": "X" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/general/index.ts b/web/app/components/base/icons/src/vender/line/general/index.ts index b5c7a7bbc1..1b6c7e7303 100644 --- a/web/app/components/base/icons/src/vender/line/general/index.ts +++ b/web/app/components/base/icons/src/vender/line/general/index.ts @@ -19,6 +19,7 @@ export { default as Pin01 } from './Pin01' export { default as Pin02 } from './Pin02' export { default as Plus02 } from './Plus02' export { default as Refresh } from './Refresh' +export { default as SearchMenu } from './SearchMenu' export { default as Settings01 } from './Settings01' export { default as Settings04 } from './Settings04' export { default as Target04 } from './Target04' diff --git a/web/app/components/base/icons/src/vender/line/images/ImagePlus.json b/web/app/components/base/icons/src/vender/line/images/ImagePlus.json index 127b04659a..ce3073f1c3 100644 --- a/web/app/components/base/icons/src/vender/line/images/ImagePlus.json +++ b/web/app/components/base/icons/src/vender/line/images/ImagePlus.json @@ -36,4 +36,4 @@ ] }, "name": "ImagePlus" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/layout/AlignLeft01.json b/web/app/components/base/icons/src/vender/line/layout/AlignLeft01.json index 5ed5add0d7..9450fd2403 100644 --- a/web/app/components/base/icons/src/vender/line/layout/AlignLeft01.json +++ b/web/app/components/base/icons/src/vender/line/layout/AlignLeft01.json @@ -36,4 +36,4 @@ ] }, "name": "AlignLeft01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/layout/AlignRight01.json b/web/app/components/base/icons/src/vender/line/layout/AlignRight01.json index 6690e6d474..05ecc93716 100644 --- a/web/app/components/base/icons/src/vender/line/layout/AlignRight01.json +++ b/web/app/components/base/icons/src/vender/line/layout/AlignRight01.json @@ -36,4 +36,4 @@ ] }, "name": "AlignRight01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/layout/Grid01.json b/web/app/components/base/icons/src/vender/line/layout/Grid01.json index 43a385c770..edc374a9cc 100644 --- a/web/app/components/base/icons/src/vender/line/layout/Grid01.json +++ b/web/app/components/base/icons/src/vender/line/layout/Grid01.json @@ -80,4 +80,4 @@ ] }, "name": "Grid01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/layout/LayoutGrid02.json b/web/app/components/base/icons/src/vender/line/layout/LayoutGrid02.json index d71e981723..a5e5b2479d 100644 --- a/web/app/components/base/icons/src/vender/line/layout/LayoutGrid02.json +++ b/web/app/components/base/icons/src/vender/line/layout/LayoutGrid02.json @@ -26,4 +26,4 @@ ] }, "name": "LayoutGrid02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/mapsAndTravel/Globe01.json b/web/app/components/base/icons/src/vender/line/mapsAndTravel/Globe01.json index 1e0896672b..9ccee71b02 100644 --- a/web/app/components/base/icons/src/vender/line/mapsAndTravel/Globe01.json +++ b/web/app/components/base/icons/src/vender/line/mapsAndTravel/Globe01.json @@ -63,4 +63,4 @@ ] }, "name": "Globe01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/mapsAndTravel/Route.json b/web/app/components/base/icons/src/vender/line/mapsAndTravel/Route.json index 19cb837362..cb0b7f01a9 100644 --- a/web/app/components/base/icons/src/vender/line/mapsAndTravel/Route.json +++ b/web/app/components/base/icons/src/vender/line/mapsAndTravel/Route.json @@ -63,4 +63,4 @@ ] }, "name": "Route" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/mediaAndDevices/Microphone01.json b/web/app/components/base/icons/src/vender/line/mediaAndDevices/Microphone01.json index 8f273d0a75..193aee5c3b 100644 --- a/web/app/components/base/icons/src/vender/line/mediaAndDevices/Microphone01.json +++ b/web/app/components/base/icons/src/vender/line/mediaAndDevices/Microphone01.json @@ -36,4 +36,4 @@ ] }, "name": "Microphone01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/mediaAndDevices/PlayCircle.json b/web/app/components/base/icons/src/vender/line/mediaAndDevices/PlayCircle.json index 278512534f..db313deb88 100644 --- a/web/app/components/base/icons/src/vender/line/mediaAndDevices/PlayCircle.json +++ b/web/app/components/base/icons/src/vender/line/mediaAndDevices/PlayCircle.json @@ -83,4 +83,4 @@ ] }, "name": "PlayCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/mediaAndDevices/SlidersH.json b/web/app/components/base/icons/src/vender/line/mediaAndDevices/SlidersH.json index fc138eecbc..4620cb9178 100644 --- a/web/app/components/base/icons/src/vender/line/mediaAndDevices/SlidersH.json +++ b/web/app/components/base/icons/src/vender/line/mediaAndDevices/SlidersH.json @@ -26,4 +26,4 @@ ] }, "name": "SlidersH" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/mediaAndDevices/Speaker.json b/web/app/components/base/icons/src/vender/line/mediaAndDevices/Speaker.json index 3e5cbe171b..2cb1df48f8 100644 --- a/web/app/components/base/icons/src/vender/line/mediaAndDevices/Speaker.json +++ b/web/app/components/base/icons/src/vender/line/mediaAndDevices/Speaker.json @@ -109,4 +109,4 @@ ] }, "name": "Speaker" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/mediaAndDevices/Stop.json b/web/app/components/base/icons/src/vender/line/mediaAndDevices/Stop.json index 7d25397087..b0860433ee 100644 --- a/web/app/components/base/icons/src/vender/line/mediaAndDevices/Stop.json +++ b/web/app/components/base/icons/src/vender/line/mediaAndDevices/Stop.json @@ -63,4 +63,4 @@ ] }, "name": "Stop" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/mediaAndDevices/StopCircle.json b/web/app/components/base/icons/src/vender/line/mediaAndDevices/StopCircle.json index 2d456014b8..3a211c78ce 100644 --- a/web/app/components/base/icons/src/vender/line/mediaAndDevices/StopCircle.json +++ b/web/app/components/base/icons/src/vender/line/mediaAndDevices/StopCircle.json @@ -56,4 +56,4 @@ ] }, "name": "StopCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/Apps02.json b/web/app/components/base/icons/src/vender/line/others/Apps02.json index 2ff128f24c..31378e175d 100644 --- a/web/app/components/base/icons/src/vender/line/others/Apps02.json +++ b/web/app/components/base/icons/src/vender/line/others/Apps02.json @@ -33,4 +33,4 @@ ] }, "name": "Apps02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/BubbleX.json b/web/app/components/base/icons/src/vender/line/others/BubbleX.json index 0cb5702c1f..7991ed4981 100644 --- a/web/app/components/base/icons/src/vender/line/others/BubbleX.json +++ b/web/app/components/base/icons/src/vender/line/others/BubbleX.json @@ -54,4 +54,4 @@ ] }, "name": "BubbleX" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/Colors.json b/web/app/components/base/icons/src/vender/line/others/Colors.json index b1832c2fe8..904e39fd18 100644 --- a/web/app/components/base/icons/src/vender/line/others/Colors.json +++ b/web/app/components/base/icons/src/vender/line/others/Colors.json @@ -63,4 +63,4 @@ ] }, "name": "Colors" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/DragHandle.json b/web/app/components/base/icons/src/vender/line/others/DragHandle.json index c1364aff18..ee1803c15c 100644 --- a/web/app/components/base/icons/src/vender/line/others/DragHandle.json +++ b/web/app/components/base/icons/src/vender/line/others/DragHandle.json @@ -35,4 +35,4 @@ ] }, "name": "DragHandle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/Env.json b/web/app/components/base/icons/src/vender/line/others/Env.json index 87a88edf3f..0cca4da4c4 100644 --- a/web/app/components/base/icons/src/vender/line/others/Env.json +++ b/web/app/components/base/icons/src/vender/line/others/Env.json @@ -87,4 +87,4 @@ ] }, "name": "Env" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/Exchange02.json b/web/app/components/base/icons/src/vender/line/others/Exchange02.json index 808a9ff644..3672d8b88b 100644 --- a/web/app/components/base/icons/src/vender/line/others/Exchange02.json +++ b/web/app/components/base/icons/src/vender/line/others/Exchange02.json @@ -23,4 +23,4 @@ ] }, "name": "Exchange02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/FileCode.json b/web/app/components/base/icons/src/vender/line/others/FileCode.json index 41050a559b..d61af3fdb3 100644 --- a/web/app/components/base/icons/src/vender/line/others/FileCode.json +++ b/web/app/components/base/icons/src/vender/line/others/FileCode.json @@ -23,4 +23,4 @@ ] }, "name": "FileCode" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/GlobalVariable.json b/web/app/components/base/icons/src/vender/line/others/GlobalVariable.json index d5fce59b4a..600c803f32 100644 --- a/web/app/components/base/icons/src/vender/line/others/GlobalVariable.json +++ b/web/app/components/base/icons/src/vender/line/others/GlobalVariable.json @@ -25,4 +25,4 @@ ] }, "name": "GlobalVariable" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/Icon3Dots.json b/web/app/components/base/icons/src/vender/line/others/Icon3Dots.json index 0942222f39..cd56eea903 100644 --- a/web/app/components/base/icons/src/vender/line/others/Icon3Dots.json +++ b/web/app/components/base/icons/src/vender/line/others/Icon3Dots.json @@ -36,4 +36,4 @@ ] }, "name": "Icon3Dots" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/LongArrowLeft.json b/web/app/components/base/icons/src/vender/line/others/LongArrowLeft.json index d2646b1090..43074803fe 100644 --- a/web/app/components/base/icons/src/vender/line/others/LongArrowLeft.json +++ b/web/app/components/base/icons/src/vender/line/others/LongArrowLeft.json @@ -24,4 +24,4 @@ ] }, "name": "LongArrowLeft" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/LongArrowRight.json b/web/app/components/base/icons/src/vender/line/others/LongArrowRight.json index 7582b81568..df05126c9a 100644 --- a/web/app/components/base/icons/src/vender/line/others/LongArrowRight.json +++ b/web/app/components/base/icons/src/vender/line/others/LongArrowRight.json @@ -24,4 +24,4 @@ ] }, "name": "LongArrowRight" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/SearchMenu.json b/web/app/components/base/icons/src/vender/line/others/SearchMenu.json index 5222574040..5854f25339 100644 --- a/web/app/components/base/icons/src/vender/line/others/SearchMenu.json +++ b/web/app/components/base/icons/src/vender/line/others/SearchMenu.json @@ -74,4 +74,4 @@ ] }, "name": "SearchMenu" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/others/Tools.json b/web/app/components/base/icons/src/vender/line/others/Tools.json index 0ab6857b09..12068ada07 100644 --- a/web/app/components/base/icons/src/vender/line/others/Tools.json +++ b/web/app/components/base/icons/src/vender/line/others/Tools.json @@ -116,4 +116,4 @@ ] }, "name": "Tools" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/shapes/CubeOutline.json b/web/app/components/base/icons/src/vender/line/shapes/CubeOutline.json index 4091004b72..bfeea58e92 100644 --- a/web/app/components/base/icons/src/vender/line/shapes/CubeOutline.json +++ b/web/app/components/base/icons/src/vender/line/shapes/CubeOutline.json @@ -95,4 +95,4 @@ ] }, "name": "CubeOutline" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/time/ClockFastForward.json b/web/app/components/base/icons/src/vender/line/time/ClockFastForward.json index 26b72084bf..72f2478958 100644 --- a/web/app/components/base/icons/src/vender/line/time/ClockFastForward.json +++ b/web/app/components/base/icons/src/vender/line/time/ClockFastForward.json @@ -26,4 +26,4 @@ ] }, "name": "ClockFastForward" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/time/ClockPlay.json b/web/app/components/base/icons/src/vender/line/time/ClockPlay.json index 7d3cc48b09..4f6739241c 100644 --- a/web/app/components/base/icons/src/vender/line/time/ClockPlay.json +++ b/web/app/components/base/icons/src/vender/line/time/ClockPlay.json @@ -63,4 +63,4 @@ ] }, "name": "ClockPlay" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/time/ClockPlaySlim.json b/web/app/components/base/icons/src/vender/line/time/ClockPlaySlim.json index 348694eeee..6790781864 100644 --- a/web/app/components/base/icons/src/vender/line/time/ClockPlaySlim.json +++ b/web/app/components/base/icons/src/vender/line/time/ClockPlaySlim.json @@ -36,4 +36,4 @@ ] }, "name": "ClockPlaySlim" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/time/ClockRefresh.json b/web/app/components/base/icons/src/vender/line/time/ClockRefresh.json index 925907ab8c..f0fda2c829 100644 --- a/web/app/components/base/icons/src/vender/line/time/ClockRefresh.json +++ b/web/app/components/base/icons/src/vender/line/time/ClockRefresh.json @@ -59,4 +59,4 @@ ] }, "name": "ClockRefresh" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/users/User01.json b/web/app/components/base/icons/src/vender/line/users/User01.json index 55353030f9..5b878f8deb 100644 --- a/web/app/components/base/icons/src/vender/line/users/User01.json +++ b/web/app/components/base/icons/src/vender/line/users/User01.json @@ -36,4 +36,4 @@ ] }, "name": "User01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/users/Users01.json b/web/app/components/base/icons/src/vender/line/users/Users01.json index 96dbeb30ec..497c258bc5 100644 --- a/web/app/components/base/icons/src/vender/line/users/Users01.json +++ b/web/app/components/base/icons/src/vender/line/users/Users01.json @@ -36,4 +36,4 @@ ] }, "name": "Users01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/line/weather/Stars02.json b/web/app/components/base/icons/src/vender/line/weather/Stars02.json index 54f6a42ecf..fdb25e6238 100644 --- a/web/app/components/base/icons/src/vender/line/weather/Stars02.json +++ b/web/app/components/base/icons/src/vender/line/weather/Stars02.json @@ -26,4 +26,4 @@ ] }, "name": "Stars02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/other/AnthropicText.json b/web/app/components/base/icons/src/vender/other/AnthropicText.json index a65ef47747..df844dec27 100644 --- a/web/app/components/base/icons/src/vender/other/AnthropicText.json +++ b/web/app/components/base/icons/src/vender/other/AnthropicText.json @@ -536,4 +536,4 @@ ] }, "name": "AnthropicText" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/other/Generator.json b/web/app/components/base/icons/src/vender/other/Generator.json index 3f24cfe18b..a72489d190 100644 --- a/web/app/components/base/icons/src/vender/other/Generator.json +++ b/web/app/components/base/icons/src/vender/other/Generator.json @@ -34,4 +34,4 @@ ] }, "name": "Generator" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/other/Group.json b/web/app/components/base/icons/src/vender/other/Group.json index 078febbc80..5f95dfc364 100644 --- a/web/app/components/base/icons/src/vender/other/Group.json +++ b/web/app/components/base/icons/src/vender/other/Group.json @@ -63,4 +63,4 @@ ] }, "name": "Group" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/other/Mcp.json b/web/app/components/base/icons/src/vender/other/Mcp.json index 7caa70b16b..c1162e64a1 100644 --- a/web/app/components/base/icons/src/vender/other/Mcp.json +++ b/web/app/components/base/icons/src/vender/other/Mcp.json @@ -32,4 +32,4 @@ ] }, "name": "Mcp" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json b/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json index d33d62d344..db2c952b1d 100644 --- a/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json +++ b/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json @@ -276,4 +276,4 @@ ] }, "name": "NoToolPlaceholder" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/other/Openai.json b/web/app/components/base/icons/src/vender/other/Openai.json index 236f66fcf2..ddf1243254 100644 --- a/web/app/components/base/icons/src/vender/other/Openai.json +++ b/web/app/components/base/icons/src/vender/other/Openai.json @@ -77,4 +77,4 @@ ] }, "name": "Openai" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/other/ReplayLine.json b/web/app/components/base/icons/src/vender/other/ReplayLine.json index 0fffbc98f5..2cc78753fd 100644 --- a/web/app/components/base/icons/src/vender/other/ReplayLine.json +++ b/web/app/components/base/icons/src/vender/other/ReplayLine.json @@ -33,4 +33,4 @@ ] }, "name": "ReplayLine" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/plugin/BoxSparkleFill.json b/web/app/components/base/icons/src/vender/plugin/BoxSparkleFill.json index 3733f98afd..55d7c64620 100644 --- a/web/app/components/base/icons/src/vender/plugin/BoxSparkleFill.json +++ b/web/app/components/base/icons/src/vender/plugin/BoxSparkleFill.json @@ -63,4 +63,4 @@ ] }, "name": "BoxSparkleFill" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/plugin/LeftCorner.json b/web/app/components/base/icons/src/vender/plugin/LeftCorner.json index d4cd0cd0ec..2374b1cfd8 100644 --- a/web/app/components/base/icons/src/vender/plugin/LeftCorner.json +++ b/web/app/components/base/icons/src/vender/plugin/LeftCorner.json @@ -24,4 +24,4 @@ ] }, "name": "LeftCorner" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/GoldCoin.json b/web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/GoldCoin.json index dac0e567f6..878c1f9f1f 100644 --- a/web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/GoldCoin.json +++ b/web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/GoldCoin.json @@ -23,4 +23,4 @@ ] }, "name": "GoldCoin" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/Scales02.json b/web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/Scales02.json index 9a781bd62d..3dd4e8908c 100644 --- a/web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/Scales02.json +++ b/web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/Scales02.json @@ -45,4 +45,4 @@ ] }, "name": "Scales02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle.json b/web/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle.json index c73fbc5855..1aa7089b6d 100644 --- a/web/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle.json +++ b/web/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle.json @@ -35,4 +35,4 @@ ] }, "name": "AlertTriangle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/arrows/ChevronDown.json b/web/app/components/base/icons/src/vender/solid/arrows/ChevronDown.json index ef9a33dc03..b4b6429d9c 100644 --- a/web/app/components/base/icons/src/vender/solid/arrows/ChevronDown.json +++ b/web/app/components/base/icons/src/vender/solid/arrows/ChevronDown.json @@ -36,4 +36,4 @@ ] }, "name": "ChevronDown" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/arrows/HighPriority.json b/web/app/components/base/icons/src/vender/solid/arrows/HighPriority.json index 6710fd8109..5b66153647 100644 --- a/web/app/components/base/icons/src/vender/solid/arrows/HighPriority.json +++ b/web/app/components/base/icons/src/vender/solid/arrows/HighPriority.json @@ -50,4 +50,4 @@ ] }, "name": "HighPriority" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/AiText.json b/web/app/components/base/icons/src/vender/solid/communication/AiText.json index c6e30fbf01..65860e58b9 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/AiText.json +++ b/web/app/components/base/icons/src/vender/solid/communication/AiText.json @@ -50,4 +50,4 @@ ] }, "name": "AiText" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.json b/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.json index fceddcc729..7b2e964a61 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.json +++ b/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.json @@ -25,4 +25,4 @@ ] }, "name": "BubbleTextMod" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/ChatBot.json b/web/app/components/base/icons/src/vender/solid/communication/ChatBot.json index 024b0edbeb..0378d60a53 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/ChatBot.json +++ b/web/app/components/base/icons/src/vender/solid/communication/ChatBot.json @@ -55,4 +55,4 @@ ] }, "name": "ChatBot" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/CuteRobot.json b/web/app/components/base/icons/src/vender/solid/communication/CuteRobot.json index 5b36575f56..fa9786473c 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/CuteRobot.json +++ b/web/app/components/base/icons/src/vender/solid/communication/CuteRobot.json @@ -35,4 +35,4 @@ ] }, "name": "CuteRobot" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/EditList.json b/web/app/components/base/icons/src/vender/solid/communication/EditList.json index 436f0be9f3..51278466c5 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/EditList.json +++ b/web/app/components/base/icons/src/vender/solid/communication/EditList.json @@ -50,4 +50,4 @@ ] }, "name": "EditList" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.json b/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.json index 2e348e4b8f..160172c88c 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.json +++ b/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.json @@ -50,4 +50,4 @@ ] }, "name": "ListSparkle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/Logic.json b/web/app/components/base/icons/src/vender/solid/communication/Logic.json index 57f86f4dd8..fa55d1c35b 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/Logic.json +++ b/web/app/components/base/icons/src/vender/solid/communication/Logic.json @@ -50,4 +50,4 @@ ] }, "name": "Logic" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.json b/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.json index dca92bf5d9..e4f41f22b4 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.json +++ b/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.json @@ -35,4 +35,4 @@ ] }, "name": "MessageDotsCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/MessageFast.json b/web/app/components/base/icons/src/vender/solid/communication/MessageFast.json index 4580398f31..b859b1f3f0 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/MessageFast.json +++ b/web/app/components/base/icons/src/vender/solid/communication/MessageFast.json @@ -25,4 +25,4 @@ ] }, "name": "MessageFast" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/MessageHeartCircle.json b/web/app/components/base/icons/src/vender/solid/communication/MessageHeartCircle.json index 84769ba909..ede7ecdb8b 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/MessageHeartCircle.json +++ b/web/app/components/base/icons/src/vender/solid/communication/MessageHeartCircle.json @@ -35,4 +35,4 @@ ] }, "name": "MessageHeartCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/MessageSmileSquare.json b/web/app/components/base/icons/src/vender/solid/communication/MessageSmileSquare.json index 7810d9043b..466f1d0207 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/MessageSmileSquare.json +++ b/web/app/components/base/icons/src/vender/solid/communication/MessageSmileSquare.json @@ -35,4 +35,4 @@ ] }, "name": "MessageSmileSquare" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/communication/Send03.json b/web/app/components/base/icons/src/vender/solid/communication/Send03.json index c6ff534838..8d0373ba7a 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/Send03.json +++ b/web/app/components/base/icons/src/vender/solid/communication/Send03.json @@ -33,4 +33,4 @@ ] }, "name": "Send03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/ApiConnection.json b/web/app/components/base/icons/src/vender/solid/development/ApiConnection.json index 6aafba9630..54a052241c 100644 --- a/web/app/components/base/icons/src/vender/solid/development/ApiConnection.json +++ b/web/app/components/base/icons/src/vender/solid/development/ApiConnection.json @@ -50,4 +50,4 @@ ] }, "name": "ApiConnection" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.json b/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.json index e8ebcc7448..21efdaa13e 100644 --- a/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.json +++ b/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.json @@ -35,4 +35,4 @@ ] }, "name": "ApiConnectionMod" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/BarChartSquare02.json b/web/app/components/base/icons/src/vender/solid/development/BarChartSquare02.json index 14b274eef7..8ae42ed951 100644 --- a/web/app/components/base/icons/src/vender/solid/development/BarChartSquare02.json +++ b/web/app/components/base/icons/src/vender/solid/development/BarChartSquare02.json @@ -35,4 +35,4 @@ ] }, "name": "BarChartSquare02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/Container.json b/web/app/components/base/icons/src/vender/solid/development/Container.json index c2c3701b4c..b0d23fef72 100644 --- a/web/app/components/base/icons/src/vender/solid/development/Container.json +++ b/web/app/components/base/icons/src/vender/solid/development/Container.json @@ -41,4 +41,4 @@ ] }, "name": "Container" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/Database02.json b/web/app/components/base/icons/src/vender/solid/development/Database02.json index a1c5230612..b18d20eaea 100644 --- a/web/app/components/base/icons/src/vender/solid/development/Database02.json +++ b/web/app/components/base/icons/src/vender/solid/development/Database02.json @@ -43,4 +43,4 @@ ] }, "name": "Database02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/Database03.json b/web/app/components/base/icons/src/vender/solid/development/Database03.json index fa0c7ce94f..b00726139c 100644 --- a/web/app/components/base/icons/src/vender/solid/development/Database03.json +++ b/web/app/components/base/icons/src/vender/solid/development/Database03.json @@ -25,4 +25,4 @@ ] }, "name": "Database03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/FileHeart02.json b/web/app/components/base/icons/src/vender/solid/development/FileHeart02.json index 08df0f27dd..681806b16f 100644 --- a/web/app/components/base/icons/src/vender/solid/development/FileHeart02.json +++ b/web/app/components/base/icons/src/vender/solid/development/FileHeart02.json @@ -47,4 +47,4 @@ ] }, "name": "FileHeart02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/PatternRecognition.json b/web/app/components/base/icons/src/vender/solid/development/PatternRecognition.json index 3d13c32b87..3b05d5ba8c 100644 --- a/web/app/components/base/icons/src/vender/solid/development/PatternRecognition.json +++ b/web/app/components/base/icons/src/vender/solid/development/PatternRecognition.json @@ -95,4 +95,4 @@ ] }, "name": "PatternRecognition" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/PromptEngineering.json b/web/app/components/base/icons/src/vender/solid/development/PromptEngineering.json index 01fbac5e93..a34a3f4fe8 100644 --- a/web/app/components/base/icons/src/vender/solid/development/PromptEngineering.json +++ b/web/app/components/base/icons/src/vender/solid/development/PromptEngineering.json @@ -50,4 +50,4 @@ ] }, "name": "PromptEngineering" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/PuzzlePiece01.json b/web/app/components/base/icons/src/vender/solid/development/PuzzlePiece01.json index f4008c81e2..20e6719fbe 100644 --- a/web/app/components/base/icons/src/vender/solid/development/PuzzlePiece01.json +++ b/web/app/components/base/icons/src/vender/solid/development/PuzzlePiece01.json @@ -35,4 +35,4 @@ ] }, "name": "PuzzlePiece01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/Semantic.json b/web/app/components/base/icons/src/vender/solid/development/Semantic.json index 333b3fa1c1..d9f6eeeb7e 100644 --- a/web/app/components/base/icons/src/vender/solid/development/Semantic.json +++ b/web/app/components/base/icons/src/vender/solid/development/Semantic.json @@ -50,4 +50,4 @@ ] }, "name": "Semantic" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/TerminalSquare.json b/web/app/components/base/icons/src/vender/solid/development/TerminalSquare.json index 7716cfd242..bf3c05a3b9 100644 --- a/web/app/components/base/icons/src/vender/solid/development/TerminalSquare.json +++ b/web/app/components/base/icons/src/vender/solid/development/TerminalSquare.json @@ -35,4 +35,4 @@ ] }, "name": "TerminalSquare" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/development/Variable02.json b/web/app/components/base/icons/src/vender/solid/development/Variable02.json index f506afd8bb..d957d38d4e 100644 --- a/web/app/components/base/icons/src/vender/solid/development/Variable02.json +++ b/web/app/components/base/icons/src/vender/solid/development/Variable02.json @@ -59,4 +59,4 @@ ] }, "name": "Variable02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/editor/Brush01.json b/web/app/components/base/icons/src/vender/solid/editor/Brush01.json index 049e5f2924..4087091bc0 100644 --- a/web/app/components/base/icons/src/vender/solid/editor/Brush01.json +++ b/web/app/components/base/icons/src/vender/solid/editor/Brush01.json @@ -32,4 +32,4 @@ ] }, "name": "Brush01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/editor/Citations.json b/web/app/components/base/icons/src/vender/solid/editor/Citations.json index 79d56b1a6c..1f7aa93794 100644 --- a/web/app/components/base/icons/src/vender/solid/editor/Citations.json +++ b/web/app/components/base/icons/src/vender/solid/editor/Citations.json @@ -33,4 +33,4 @@ ] }, "name": "Citations" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/editor/Colors.json b/web/app/components/base/icons/src/vender/solid/editor/Colors.json index 6e5dc69049..6fc4010c27 100644 --- a/web/app/components/base/icons/src/vender/solid/editor/Colors.json +++ b/web/app/components/base/icons/src/vender/solid/editor/Colors.json @@ -59,4 +59,4 @@ ] }, "name": "Colors" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/editor/Paragraph.json b/web/app/components/base/icons/src/vender/solid/editor/Paragraph.json index a16f076073..747f8e50b7 100644 --- a/web/app/components/base/icons/src/vender/solid/editor/Paragraph.json +++ b/web/app/components/base/icons/src/vender/solid/editor/Paragraph.json @@ -41,4 +41,4 @@ ] }, "name": "Paragraph" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/editor/TypeSquare.json b/web/app/components/base/icons/src/vender/solid/editor/TypeSquare.json index f901b0737f..9e6c72cf67 100644 --- a/web/app/components/base/icons/src/vender/solid/editor/TypeSquare.json +++ b/web/app/components/base/icons/src/vender/solid/editor/TypeSquare.json @@ -25,4 +25,4 @@ ] }, "name": "TypeSquare" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/education/Beaker02.json b/web/app/components/base/icons/src/vender/solid/education/Beaker02.json index 2f7830084e..b6dfd318ef 100644 --- a/web/app/components/base/icons/src/vender/solid/education/Beaker02.json +++ b/web/app/components/base/icons/src/vender/solid/education/Beaker02.json @@ -35,4 +35,4 @@ ] }, "name": "Beaker02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/education/BubbleText.json b/web/app/components/base/icons/src/vender/solid/education/BubbleText.json index 999f0db97e..e1b4b54fd2 100644 --- a/web/app/components/base/icons/src/vender/solid/education/BubbleText.json +++ b/web/app/components/base/icons/src/vender/solid/education/BubbleText.json @@ -35,4 +35,4 @@ ] }, "name": "BubbleText" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/education/Heart02.json b/web/app/components/base/icons/src/vender/solid/education/Heart02.json index 8cecaaee84..58ffe0c6f7 100644 --- a/web/app/components/base/icons/src/vender/solid/education/Heart02.json +++ b/web/app/components/base/icons/src/vender/solid/education/Heart02.json @@ -23,4 +23,4 @@ ] }, "name": "Heart02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/education/Unblur.json b/web/app/components/base/icons/src/vender/solid/education/Unblur.json index 13b8bb36f5..33c43170fe 100644 --- a/web/app/components/base/icons/src/vender/solid/education/Unblur.json +++ b/web/app/components/base/icons/src/vender/solid/education/Unblur.json @@ -149,4 +149,4 @@ ] }, "name": "Unblur" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/files/File05.json b/web/app/components/base/icons/src/vender/solid/files/File05.json index 17b9629741..ead7649bcf 100644 --- a/web/app/components/base/icons/src/vender/solid/files/File05.json +++ b/web/app/components/base/icons/src/vender/solid/files/File05.json @@ -52,4 +52,4 @@ ] }, "name": "File05" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/files/FileSearch02.json b/web/app/components/base/icons/src/vender/solid/files/FileSearch02.json index 7f8b0e8c78..e1decb16ac 100644 --- a/web/app/components/base/icons/src/vender/solid/files/FileSearch02.json +++ b/web/app/components/base/icons/src/vender/solid/files/FileSearch02.json @@ -54,4 +54,4 @@ ] }, "name": "FileSearch02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/files/FileZip.json b/web/app/components/base/icons/src/vender/solid/files/FileZip.json index 11fe823916..3d58745fa8 100644 --- a/web/app/components/base/icons/src/vender/solid/files/FileZip.json +++ b/web/app/components/base/icons/src/vender/solid/files/FileZip.json @@ -44,4 +44,4 @@ ] }, "name": "FileZip" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/files/Folder.json b/web/app/components/base/icons/src/vender/solid/files/Folder.json index 4fc5e5f51f..50c483bc92 100644 --- a/web/app/components/base/icons/src/vender/solid/files/Folder.json +++ b/web/app/components/base/icons/src/vender/solid/files/Folder.json @@ -35,4 +35,4 @@ ] }, "name": "Folder" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/AnswerTriangle.json b/web/app/components/base/icons/src/vender/solid/general/AnswerTriangle.json index a4b6283830..030e1efedf 100644 --- a/web/app/components/base/icons/src/vender/solid/general/AnswerTriangle.json +++ b/web/app/components/base/icons/src/vender/solid/general/AnswerTriangle.json @@ -24,4 +24,4 @@ ] }, "name": "AnswerTriangle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/ArrowDownRoundFill.json b/web/app/components/base/icons/src/vender/solid/general/ArrowDownRoundFill.json index 4e7da3c801..8367c942cb 100644 --- a/web/app/components/base/icons/src/vender/solid/general/ArrowDownRoundFill.json +++ b/web/app/components/base/icons/src/vender/solid/general/ArrowDownRoundFill.json @@ -33,4 +33,4 @@ ] }, "name": "ArrowDownRoundFill" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/CheckCircle.json b/web/app/components/base/icons/src/vender/solid/general/CheckCircle.json index 1b567e859e..403a5fe1c4 100644 --- a/web/app/components/base/icons/src/vender/solid/general/CheckCircle.json +++ b/web/app/components/base/icons/src/vender/solid/general/CheckCircle.json @@ -35,4 +35,4 @@ ] }, "name": "CheckCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/CheckDone01.json b/web/app/components/base/icons/src/vender/solid/general/CheckDone01.json index b4d5530b06..57a48650f1 100644 --- a/web/app/components/base/icons/src/vender/solid/general/CheckDone01.json +++ b/web/app/components/base/icons/src/vender/solid/general/CheckDone01.json @@ -34,4 +34,4 @@ ] }, "name": "CheckDone01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/Download02.json b/web/app/components/base/icons/src/vender/solid/general/Download02.json index 5854e64301..8afe46c93e 100644 --- a/web/app/components/base/icons/src/vender/solid/general/Download02.json +++ b/web/app/components/base/icons/src/vender/solid/general/Download02.json @@ -26,4 +26,4 @@ ] }, "name": "Download02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/Edit03.json b/web/app/components/base/icons/src/vender/solid/general/Edit03.json index f736ef56dd..ddf3a0d234 100644 --- a/web/app/components/base/icons/src/vender/solid/general/Edit03.json +++ b/web/app/components/base/icons/src/vender/solid/general/Edit03.json @@ -54,4 +54,4 @@ ] }, "name": "Edit03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/Edit04.json b/web/app/components/base/icons/src/vender/solid/general/Edit04.json index aa923c2862..c254f600a4 100644 --- a/web/app/components/base/icons/src/vender/solid/general/Edit04.json +++ b/web/app/components/base/icons/src/vender/solid/general/Edit04.json @@ -36,4 +36,4 @@ ] }, "name": "Edit04" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/Eye.json b/web/app/components/base/icons/src/vender/solid/general/Eye.json index a7e63484da..a76bb81725 100644 --- a/web/app/components/base/icons/src/vender/solid/general/Eye.json +++ b/web/app/components/base/icons/src/vender/solid/general/Eye.json @@ -34,4 +34,4 @@ ] }, "name": "Eye" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/Github.json b/web/app/components/base/icons/src/vender/solid/general/Github.json index 46e694215b..a46c81bfef 100644 --- a/web/app/components/base/icons/src/vender/solid/general/Github.json +++ b/web/app/components/base/icons/src/vender/solid/general/Github.json @@ -33,4 +33,4 @@ ] }, "name": "Github" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/MessageClockCircle.json b/web/app/components/base/icons/src/vender/solid/general/MessageClockCircle.json index 4307f82ef8..d52347f6d9 100644 --- a/web/app/components/base/icons/src/vender/solid/general/MessageClockCircle.json +++ b/web/app/components/base/icons/src/vender/solid/general/MessageClockCircle.json @@ -33,4 +33,4 @@ ] }, "name": "MessageClockCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/PlusCircle.json b/web/app/components/base/icons/src/vender/solid/general/PlusCircle.json index 005a7ba5bf..e3a86132ec 100644 --- a/web/app/components/base/icons/src/vender/solid/general/PlusCircle.json +++ b/web/app/components/base/icons/src/vender/solid/general/PlusCircle.json @@ -35,4 +35,4 @@ ] }, "name": "PlusCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json b/web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json index 8830ee5837..32df4b0cd5 100644 --- a/web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json +++ b/web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json @@ -42,4 +42,4 @@ ] }, "name": "QuestionTriangle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/SearchMd.json b/web/app/components/base/icons/src/vender/solid/general/SearchMd.json index 808195f1fa..89cf471ca5 100644 --- a/web/app/components/base/icons/src/vender/solid/general/SearchMd.json +++ b/web/app/components/base/icons/src/vender/solid/general/SearchMd.json @@ -35,4 +35,4 @@ ] }, "name": "SearchMd" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/Target04.json b/web/app/components/base/icons/src/vender/solid/general/Target04.json index 6b22fab885..70895ccc00 100644 --- a/web/app/components/base/icons/src/vender/solid/general/Target04.json +++ b/web/app/components/base/icons/src/vender/solid/general/Target04.json @@ -43,4 +43,4 @@ ] }, "name": "Target04" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/Tool03.json b/web/app/components/base/icons/src/vender/solid/general/Tool03.json index 0a7f1ab96c..843fb36fba 100644 --- a/web/app/components/base/icons/src/vender/solid/general/Tool03.json +++ b/web/app/components/base/icons/src/vender/solid/general/Tool03.json @@ -59,4 +59,4 @@ ] }, "name": "Tool03" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/XCircle.json b/web/app/components/base/icons/src/vender/solid/general/XCircle.json index dd269fafcc..5e09c5a9e1 100644 --- a/web/app/components/base/icons/src/vender/solid/general/XCircle.json +++ b/web/app/components/base/icons/src/vender/solid/general/XCircle.json @@ -26,4 +26,4 @@ ] }, "name": "XCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/ZapFast.json b/web/app/components/base/icons/src/vender/solid/general/ZapFast.json index 865a48ee65..418a5c0019 100644 --- a/web/app/components/base/icons/src/vender/solid/general/ZapFast.json +++ b/web/app/components/base/icons/src/vender/solid/general/ZapFast.json @@ -76,4 +76,4 @@ ] }, "name": "ZapFast" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/general/ZapNarrow.json b/web/app/components/base/icons/src/vender/solid/general/ZapNarrow.json index 740c823f6e..27a8059fc8 100644 --- a/web/app/components/base/icons/src/vender/solid/general/ZapNarrow.json +++ b/web/app/components/base/icons/src/vender/solid/general/ZapNarrow.json @@ -35,4 +35,4 @@ ] }, "name": "ZapNarrow" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/layout/Grid01.json b/web/app/components/base/icons/src/vender/solid/layout/Grid01.json index 722cdecef6..35d4190237 100644 --- a/web/app/components/base/icons/src/vender/solid/layout/Grid01.json +++ b/web/app/components/base/icons/src/vender/solid/layout/Grid01.json @@ -76,4 +76,4 @@ ] }, "name": "Grid01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.json b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.json index 6cc565ffdf..b86197ae7e 100644 --- a/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.json +++ b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Globe06.json @@ -54,4 +54,4 @@ ] }, "name": "Globe06" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Route.json b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Route.json index 24d3f35954..ac94bf2109 100644 --- a/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Route.json +++ b/web/app/components/base/icons/src/vender/solid/mapsAndTravel/Route.json @@ -55,4 +55,4 @@ ] }, "name": "Route" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/AudioSupportIcon.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/AudioSupportIcon.json index cd3006b76d..aaab128672 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/AudioSupportIcon.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/AudioSupportIcon.json @@ -23,4 +23,4 @@ ] }, "name": "AudioSupportIcon" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/DocumentSupportIcon.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/DocumentSupportIcon.json index 49cb6a521c..1047de09e0 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/DocumentSupportIcon.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/DocumentSupportIcon.json @@ -23,4 +23,4 @@ ] }, "name": "DocumentSupportIcon" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicBox.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicBox.json index 4668e9eba8..ee079108c6 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicBox.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicBox.json @@ -61,4 +61,4 @@ ] }, "name": "MagicBox" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicEyes.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicEyes.json index 00e16960a6..e7c0b53bf2 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicEyes.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicEyes.json @@ -35,4 +35,4 @@ ] }, "name": "MagicEyes" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicWand.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicWand.json index bf13ab9e00..d9852c839f 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicWand.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/MagicWand.json @@ -70,4 +70,4 @@ ] }, "name": "MagicWand" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Microphone01.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Microphone01.json index 36aad43649..95ff504b07 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Microphone01.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Microphone01.json @@ -52,4 +52,4 @@ ] }, "name": "Microphone01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Play.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Play.json index b32d786e4e..dc3cb6b4c0 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Play.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Play.json @@ -35,4 +35,4 @@ ] }, "name": "Play" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Robot.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Robot.json index 650ca36528..616821956e 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Robot.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Robot.json @@ -35,4 +35,4 @@ ] }, "name": "Robot" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Sliders02.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Sliders02.json index d72b99aa57..015509fca5 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Sliders02.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Sliders02.json @@ -74,4 +74,4 @@ ] }, "name": "Sliders02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Speaker.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Speaker.json index 3e5cbe171b..2cb1df48f8 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Speaker.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/Speaker.json @@ -109,4 +109,4 @@ ] }, "name": "Speaker" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/StopCircle.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/StopCircle.json index 67e02fca63..62ae331783 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/StopCircle.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/StopCircle.json @@ -35,4 +35,4 @@ ] }, "name": "StopCircle" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/VideoSupportIcon.json b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/VideoSupportIcon.json index 4bc6881a5d..c801c12c4a 100644 --- a/web/app/components/base/icons/src/vender/solid/mediaAndDevices/VideoSupportIcon.json +++ b/web/app/components/base/icons/src/vender/solid/mediaAndDevices/VideoSupportIcon.json @@ -23,4 +23,4 @@ ] }, "name": "VideoSupportIcon" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/security/Lock01.json b/web/app/components/base/icons/src/vender/solid/security/Lock01.json index aa01bc574f..080a309f4d 100644 --- a/web/app/components/base/icons/src/vender/solid/security/Lock01.json +++ b/web/app/components/base/icons/src/vender/solid/security/Lock01.json @@ -35,4 +35,4 @@ ] }, "name": "Lock01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/shapes/Corner.json b/web/app/components/base/icons/src/vender/solid/shapes/Corner.json index 2f35483a66..23e1194c73 100644 --- a/web/app/components/base/icons/src/vender/solid/shapes/Corner.json +++ b/web/app/components/base/icons/src/vender/solid/shapes/Corner.json @@ -24,4 +24,4 @@ ] }, "name": "Corner" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/shapes/Star04.json b/web/app/components/base/icons/src/vender/solid/shapes/Star04.json index 5e5393a9a4..eba208cca0 100644 --- a/web/app/components/base/icons/src/vender/solid/shapes/Star04.json +++ b/web/app/components/base/icons/src/vender/solid/shapes/Star04.json @@ -33,4 +33,4 @@ ] }, "name": "Star04" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/shapes/Star06.json b/web/app/components/base/icons/src/vender/solid/shapes/Star06.json index 0466602376..5baa4d9a02 100644 --- a/web/app/components/base/icons/src/vender/solid/shapes/Star06.json +++ b/web/app/components/base/icons/src/vender/solid/shapes/Star06.json @@ -59,4 +59,4 @@ ] }, "name": "Star06" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/users/User01.json b/web/app/components/base/icons/src/vender/solid/users/User01.json index c9b8ea90d2..110cb2c020 100644 --- a/web/app/components/base/icons/src/vender/solid/users/User01.json +++ b/web/app/components/base/icons/src/vender/solid/users/User01.json @@ -54,4 +54,4 @@ ] }, "name": "User01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/users/UserEdit02.json b/web/app/components/base/icons/src/vender/solid/users/UserEdit02.json index f4451ea16f..7040d17ccb 100644 --- a/web/app/components/base/icons/src/vender/solid/users/UserEdit02.json +++ b/web/app/components/base/icons/src/vender/solid/users/UserEdit02.json @@ -89,4 +89,4 @@ ] }, "name": "UserEdit02" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/users/Users01.json b/web/app/components/base/icons/src/vender/solid/users/Users01.json index c18d59a00f..9c78dd5a09 100644 --- a/web/app/components/base/icons/src/vender/solid/users/Users01.json +++ b/web/app/components/base/icons/src/vender/solid/users/Users01.json @@ -76,4 +76,4 @@ ] }, "name": "Users01" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/solid/users/UsersPlus.json b/web/app/components/base/icons/src/vender/solid/users/UsersPlus.json index a70117f655..af8c22709f 100644 --- a/web/app/components/base/icons/src/vender/solid/users/UsersPlus.json +++ b/web/app/components/base/icons/src/vender/solid/users/UsersPlus.json @@ -74,4 +74,4 @@ ] }, "name": "UsersPlus" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/system/AutoUpdateLine.json b/web/app/components/base/icons/src/vender/system/AutoUpdateLine.json new file mode 100644 index 0000000000..5acc316bdd --- /dev/null +++ b/web/app/components/base/icons/src/vender/system/AutoUpdateLine.json @@ -0,0 +1,37 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "24", + "height": "24", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M16.3308 16H14.2915L13.6249 13.9476H10.3761L9.70846 16H7.66918L10.7759 7H13.2281L16.3308 16ZM10.8595 12.4622H13.1435L12.0378 9.05639H11.9673L10.8595 12.4622Z", + "fill": "currentColor" + }, + "children": [] + } + ] + }, + "name": "AutoUpdateLine" +} diff --git a/web/app/components/base/icons/src/vender/system/AutoUpdateLine.tsx b/web/app/components/base/icons/src/vender/system/AutoUpdateLine.tsx new file mode 100644 index 0000000000..d162edaa5a --- /dev/null +++ b/web/app/components/base/icons/src/vender/system/AutoUpdateLine.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './AutoUpdateLine.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => + +Icon.displayName = 'AutoUpdateLine' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/system/index.ts b/web/app/components/base/icons/src/vender/system/index.ts new file mode 100644 index 0000000000..01553789b8 --- /dev/null +++ b/web/app/components/base/icons/src/vender/system/index.ts @@ -0,0 +1 @@ +export { default as AutoUpdateLine } from './AutoUpdateLine' diff --git a/web/app/components/base/icons/src/vender/workflow/Agent.json b/web/app/components/base/icons/src/vender/workflow/Agent.json index e7ed19369b..200475fc44 100644 --- a/web/app/components/base/icons/src/vender/workflow/Agent.json +++ b/web/app/components/base/icons/src/vender/workflow/Agent.json @@ -50,4 +50,4 @@ ] }, "name": "Agent" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Answer.json b/web/app/components/base/icons/src/vender/workflow/Answer.json index 4f15b339bb..b0ad3f87ea 100644 --- a/web/app/components/base/icons/src/vender/workflow/Answer.json +++ b/web/app/components/base/icons/src/vender/workflow/Answer.json @@ -35,4 +35,4 @@ ] }, "name": "Answer" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Assigner.json b/web/app/components/base/icons/src/vender/workflow/Assigner.json index 7106e5ad43..89e76a9cbe 100644 --- a/web/app/components/base/icons/src/vender/workflow/Assigner.json +++ b/web/app/components/base/icons/src/vender/workflow/Assigner.json @@ -65,4 +65,4 @@ ] }, "name": "Assigner" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Code.json b/web/app/components/base/icons/src/vender/workflow/Code.json index d94f12ab3a..d72f02e289 100644 --- a/web/app/components/base/icons/src/vender/workflow/Code.json +++ b/web/app/components/base/icons/src/vender/workflow/Code.json @@ -35,4 +35,4 @@ ] }, "name": "Code" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/DocsExtractor.json b/web/app/components/base/icons/src/vender/workflow/DocsExtractor.json index 5b454590be..4e43c6c208 100644 --- a/web/app/components/base/icons/src/vender/workflow/DocsExtractor.json +++ b/web/app/components/base/icons/src/vender/workflow/DocsExtractor.json @@ -61,4 +61,4 @@ ] }, "name": "DocsExtractor" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/End.json b/web/app/components/base/icons/src/vender/workflow/End.json index 3e281cb575..56336efad1 100644 --- a/web/app/components/base/icons/src/vender/workflow/End.json +++ b/web/app/components/base/icons/src/vender/workflow/End.json @@ -35,4 +35,4 @@ ] }, "name": "End" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Home.json b/web/app/components/base/icons/src/vender/workflow/Home.json index fd3096f658..982a9db222 100644 --- a/web/app/components/base/icons/src/vender/workflow/Home.json +++ b/web/app/components/base/icons/src/vender/workflow/Home.json @@ -35,4 +35,4 @@ ] }, "name": "Home" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Http.json b/web/app/components/base/icons/src/vender/workflow/Http.json index 53b5c3a9fd..4affe7a5b0 100644 --- a/web/app/components/base/icons/src/vender/workflow/Http.json +++ b/web/app/components/base/icons/src/vender/workflow/Http.json @@ -68,4 +68,4 @@ ] }, "name": "Http" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/IfElse.json b/web/app/components/base/icons/src/vender/workflow/IfElse.json index 0ff778bc24..359f695c26 100644 --- a/web/app/components/base/icons/src/vender/workflow/IfElse.json +++ b/web/app/components/base/icons/src/vender/workflow/IfElse.json @@ -35,4 +35,4 @@ ] }, "name": "IfElse" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Iteration.json b/web/app/components/base/icons/src/vender/workflow/Iteration.json index ee5748d1f1..84148e7bac 100644 --- a/web/app/components/base/icons/src/vender/workflow/Iteration.json +++ b/web/app/components/base/icons/src/vender/workflow/Iteration.json @@ -33,4 +33,4 @@ ] }, "name": "Iteration" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/IterationStart.json b/web/app/components/base/icons/src/vender/workflow/IterationStart.json index 2941cdb65d..cb574b7ea5 100644 --- a/web/app/components/base/icons/src/vender/workflow/IterationStart.json +++ b/web/app/components/base/icons/src/vender/workflow/IterationStart.json @@ -33,4 +33,4 @@ ] }, "name": "IterationStart" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Jinja.json b/web/app/components/base/icons/src/vender/workflow/Jinja.json index ba46cb9ca6..91eee1534b 100644 --- a/web/app/components/base/icons/src/vender/workflow/Jinja.json +++ b/web/app/components/base/icons/src/vender/workflow/Jinja.json @@ -95,4 +95,4 @@ ] }, "name": "Jinja" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/KnowledgeRetrieval.json b/web/app/components/base/icons/src/vender/workflow/KnowledgeRetrieval.json index 4bdc83f868..69eb10eb83 100644 --- a/web/app/components/base/icons/src/vender/workflow/KnowledgeRetrieval.json +++ b/web/app/components/base/icons/src/vender/workflow/KnowledgeRetrieval.json @@ -35,4 +35,4 @@ ] }, "name": "KnowledgeRetrieval" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/ListFilter.json b/web/app/components/base/icons/src/vender/workflow/ListFilter.json index 568020f4a6..6ed383c315 100644 --- a/web/app/components/base/icons/src/vender/workflow/ListFilter.json +++ b/web/app/components/base/icons/src/vender/workflow/ListFilter.json @@ -35,4 +35,4 @@ ] }, "name": "ListFilter" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Llm.json b/web/app/components/base/icons/src/vender/workflow/Llm.json index d900a67041..cdf114a490 100644 --- a/web/app/components/base/icons/src/vender/workflow/Llm.json +++ b/web/app/components/base/icons/src/vender/workflow/Llm.json @@ -35,4 +35,4 @@ ] }, "name": "Llm" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/Loop.json b/web/app/components/base/icons/src/vender/workflow/Loop.json index 65a70d82a1..b6abd13dfa 100644 --- a/web/app/components/base/icons/src/vender/workflow/Loop.json +++ b/web/app/components/base/icons/src/vender/workflow/Loop.json @@ -35,4 +35,4 @@ ] }, "name": "Loop" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/LoopEnd.json b/web/app/components/base/icons/src/vender/workflow/LoopEnd.json index 1427dfdcc5..eea9e717ca 100644 --- a/web/app/components/base/icons/src/vender/workflow/LoopEnd.json +++ b/web/app/components/base/icons/src/vender/workflow/LoopEnd.json @@ -35,4 +35,4 @@ ] }, "name": "LoopEnd" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/ParameterExtractor.json b/web/app/components/base/icons/src/vender/workflow/ParameterExtractor.json index 7d4fa6424a..eb66f4846b 100644 --- a/web/app/components/base/icons/src/vender/workflow/ParameterExtractor.json +++ b/web/app/components/base/icons/src/vender/workflow/ParameterExtractor.json @@ -263,4 +263,4 @@ ] }, "name": "ParameterExtractor" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/QuestionClassifier.json b/web/app/components/base/icons/src/vender/workflow/QuestionClassifier.json index a50ee6c410..6bd3dbf096 100644 --- a/web/app/components/base/icons/src/vender/workflow/QuestionClassifier.json +++ b/web/app/components/base/icons/src/vender/workflow/QuestionClassifier.json @@ -35,4 +35,4 @@ ] }, "name": "QuestionClassifier" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/TemplatingTransform.json b/web/app/components/base/icons/src/vender/workflow/TemplatingTransform.json index 69ee236611..6399208a2d 100644 --- a/web/app/components/base/icons/src/vender/workflow/TemplatingTransform.json +++ b/web/app/components/base/icons/src/vender/workflow/TemplatingTransform.json @@ -151,4 +151,4 @@ ] }, "name": "TemplatingTransform" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/VariableX.json b/web/app/components/base/icons/src/vender/workflow/VariableX.json index 1560684e55..a87b000587 100644 --- a/web/app/components/base/icons/src/vender/workflow/VariableX.json +++ b/web/app/components/base/icons/src/vender/workflow/VariableX.json @@ -35,4 +35,4 @@ ] }, "name": "VariableX" -} \ No newline at end of file +} diff --git a/web/app/components/base/icons/src/vender/workflow/WindowCursor.json b/web/app/components/base/icons/src/vender/workflow/WindowCursor.json index b64ba912bb..66591c1116 100644 --- a/web/app/components/base/icons/src/vender/workflow/WindowCursor.json +++ b/web/app/components/base/icons/src/vender/workflow/WindowCursor.json @@ -59,4 +59,4 @@ ] }, "name": "WindowCursor" -} \ No newline at end of file +} diff --git a/web/app/components/base/modal/modal.tsx b/web/app/components/base/modal/modal.tsx new file mode 100644 index 0000000000..5738704722 --- /dev/null +++ b/web/app/components/base/modal/modal.tsx @@ -0,0 +1,123 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { RiCloseLine } from '@remixicon/react' +import { + PortalToFollowElem, + PortalToFollowElemContent, +} from '@/app/components/base/portal-to-follow-elem' +import Button from '@/app/components/base/button' +import type { ButtonProps } from '@/app/components/base/button' +import cn from '@/utils/classnames' + +type ModalProps = { + onClose?: () => void + size?: 'sm' | 'md' + title: string + subTitle?: string + children?: React.ReactNode + confirmButtonText?: string + onConfirm?: () => void + cancelButtonText?: string + onCancel?: () => void + showExtraButton?: boolean + extraButtonText?: string + extraButtonVariant?: ButtonProps['variant'] + onExtraButtonClick?: () => void + footerSlot?: React.ReactNode + bottomSlot?: React.ReactNode + disabled?: boolean +} +const Modal = ({ + onClose, + size = 'sm', + title, + subTitle, + children, + confirmButtonText, + onConfirm, + cancelButtonText, + onCancel, + showExtraButton, + extraButtonVariant = 'warning', + extraButtonText, + onExtraButtonClick, + footerSlot, + bottomSlot, + disabled, +}: ModalProps) => { + const { t } = useTranslation() + + return ( + + +
e.stopPropagation()} + > +
+ {title} + { + subTitle && ( +
+ {subTitle} +
+ ) + } +
+ +
+
+ { + children && ( +
{children}
+ ) + } +
+ {footerSlot} + { + showExtraButton && ( + <> + +
+ + ) + } + + +
+ {bottomSlot} +
+
+
+ ) +} + +export default memo(Modal) diff --git a/web/app/components/base/select/pure.tsx b/web/app/components/base/select/pure.tsx index 81cc2fbadf..be88c936fd 100644 --- a/web/app/components/base/select/pure.tsx +++ b/web/app/components/base/select/pure.tsx @@ -39,6 +39,9 @@ type PureSelectProps = { itemClassName?: string title?: string }, + placeholder?: string + disabled?: boolean + triggerPopupSameWidth?: boolean } const PureSelect = ({ options, @@ -47,6 +50,9 @@ const PureSelect = ({ containerProps, triggerProps, popupProps, + placeholder, + disabled, + triggerPopupSameWidth, }: PureSelectProps) => { const { t } = useTranslation() const { @@ -74,7 +80,7 @@ const PureSelect = ({ }, [onOpenChange]) const selectedOption = options.find(option => option.value === value) - const triggerText = selectedOption?.label || t('common.placeholder.select') + const triggerText = selectedOption?.label || placeholder || t('common.placeholder.select') return ( handleOpenChange(!mergedOpen)} @@ -135,6 +142,7 @@ const PureSelect = ({ )} title={option.label} onClick={() => { + if (disabled) return onChange?.(option.value) handleOpenChange(false) }} diff --git a/web/app/components/explore/index.tsx b/web/app/components/explore/index.tsx index bae2610cba..e716de96f1 100644 --- a/web/app/components/explore/index.tsx +++ b/web/app/components/explore/index.tsx @@ -22,6 +22,7 @@ const Explore: FC = ({ const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext() const [hasEditPermission, setHasEditPermission] = useState(false) const [installedApps, setInstalledApps] = useState([]) + const [isFetchingInstalledApps, setIsFetchingInstalledApps] = useState(false) const { t } = useTranslation() useDocumentTitle(t('common.menus.explore')) @@ -51,6 +52,8 @@ const Explore: FC = ({ hasEditPermission, installedApps, setInstalledApps, + isFetchingInstalledApps, + setIsFetchingInstalledApps, } } > diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx index 71013fc2e1..8032e173c6 100644 --- a/web/app/components/explore/installed-app/index.tsx +++ b/web/app/components/explore/installed-app/index.tsx @@ -1,11 +1,17 @@ 'use client' import type { FC } from 'react' +import { useEffect } from 'react' import React from 'react' import { useContext } from 'use-context-selector' import ExploreContext from '@/context/explore-context' import TextGenerationApp from '@/app/components/share/text-generation' import Loading from '@/app/components/base/loading' import ChatWithHistory from '@/app/components/base/chat/chat-with-history' +import { useWebAppStore } from '@/context/web-app-context' +import AppUnavailable from '../../base/app-unavailable' +import { useGetUserCanAccessApp } from '@/service/access-control' +import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore' +import type { AppData } from '@/models/share' export type IInstalledAppProps = { id: string @@ -14,26 +20,95 @@ export type IInstalledAppProps = { const InstalledApp: FC = ({ id, }) => { - const { installedApps } = useContext(ExploreContext) + const { installedApps, isFetchingInstalledApps } = useContext(ExploreContext) + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) const installedApp = installedApps.find(item => item.id === id) + const updateWebAppAccessMode = useWebAppStore(s => s.updateWebAppAccessMode) + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) + const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) + const { isFetching: isFetchingWebAppAccessMode, data: webAppAccessMode, error: webAppAccessModeError } = useGetInstalledAppAccessModeByAppId(installedApp?.id ?? null) + const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetInstalledAppParams(installedApp?.id ?? null) + const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetInstalledAppMeta(installedApp?.id ?? null) + const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: installedApp?.app.id, isInstalledApp: true }) + useEffect(() => { + if (!installedApp) { + updateAppInfo(null) + } + else { + const { id, app } = installedApp + updateAppInfo({ + app_id: id, + site: { + title: app.name, + icon_type: app.icon_type, + icon: app.icon, + icon_background: app.icon_background, + icon_url: app.icon_url, + prompt_public: false, + copyright: '', + show_workflow_steps: true, + use_icon_as_answer_icon: app.use_icon_as_answer_icon, + }, + plan: 'basic', + custom_config: null, + } as AppData) + } + + if (appParams) + updateAppParams(appParams) + if (appMeta) + updateWebAppMeta(appMeta) + if (webAppAccessMode) + updateWebAppAccessMode(webAppAccessMode.accessMode) + updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + }, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode]) + + if (appParamsError) { + return
+ +
+ } + if (appMetaError) { + return
+ +
+ } + if (useCanAccessAppError) { + return
+ +
+ } + if (webAppAccessModeError) { + return
+ +
+ } + if (userCanAccessApp && !userCanAccessApp.result) { + return
+ +
+ } + if (isFetchingAppParams || isFetchingAppMeta || isFetchingWebAppAccessMode || isFetchingInstalledApps) { + return
+ +
+ } if (!installedApp) { - return ( -
- -
- ) + return
+ +
} - return (
- {installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && ( + {installedApp?.app.mode !== 'completion' && installedApp?.app.mode !== 'workflow' && ( )} - {installedApp.app.mode === 'completion' && ( + {installedApp?.app.mode === 'completion' && ( )} - {installedApp.app.mode === 'workflow' && ( + {installedApp?.app.mode === 'workflow' && ( )}
diff --git a/web/app/components/explore/sidebar/index.tsx b/web/app/components/explore/sidebar/index.tsx index fe5935bcd3..74c397f4fd 100644 --- a/web/app/components/explore/sidebar/index.tsx +++ b/web/app/components/explore/sidebar/index.tsx @@ -8,11 +8,11 @@ import Link from 'next/link' import Toast from '../../base/toast' import Item from './app-nav-item' import cn from '@/utils/classnames' -import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore' import ExploreContext from '@/context/explore-context' import Confirm from '@/app/components/base/confirm' import Divider from '@/app/components/base/divider' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore' const SelectedDiscoveryIcon = () => ( @@ -50,16 +50,14 @@ const SideBar: FC = ({ const lastSegment = segments.slice(-1)[0] const isDiscoverySelected = lastSegment === 'apps' const isChatSelected = lastSegment === 'chat' - const { installedApps, setInstalledApps } = useContext(ExploreContext) + const { installedApps, setInstalledApps, setIsFetchingInstalledApps } = useContext(ExploreContext) + const { isFetching: isFetchingInstalledApps, data: ret, refetch: fetchInstalledAppList } = useGetInstalledApps() + const { mutateAsync: uninstallApp } = useUninstallApp() + const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus() const media = useBreakpoints() const isMobile = media === MediaType.mobile - const fetchInstalledAppList = async () => { - const { installed_apps }: any = await doFetchInstalledAppList() - setInstalledApps(installed_apps) - } - const [showConfirm, setShowConfirm] = useState(false) const [currId, setCurrId] = useState('') const handleDelete = async () => { @@ -70,25 +68,31 @@ const SideBar: FC = ({ type: 'success', message: t('common.api.remove'), }) - fetchInstalledAppList() } const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { - await updatePinStatus(id, isPinned) + await updatePinStatus({ appId: id, isPinned }) Toast.notify({ type: 'success', message: t('common.api.success'), }) - fetchInstalledAppList() } useEffect(() => { - fetchInstalledAppList() - }, []) + const installed_apps = (ret as any)?.installed_apps + if (installed_apps && installed_apps.length > 0) + setInstalledApps(installed_apps) + else + setInstalledApps([]) + }, [ret, setInstalledApps]) + + useEffect(() => { + setIsFetchingInstalledApps(isFetchingInstalledApps) + }, [isFetchingInstalledApps, setIsFetchingInstalledApps]) useEffect(() => { fetchInstalledAppList() - }, [controlUpdateInstalledApps]) + }, [controlUpdateInstalledApps, fetchInstalledAppList]) const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length return ( diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx index 2d8bdcd3d9..fabad62397 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx @@ -10,7 +10,7 @@ import type { ExposeRefs } from './install-multi' import InstallMulti from './install-multi' import { useInstallOrUpdate } from '@/service/use-plugins' import useRefreshPluginList from '../../hooks/use-refresh-plugin-list' -import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission' +import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-reference-setting' import { useMittContextSelector } from '@/context/mitt-context' import Checkbox from '@/app/components/base/checkbox' const i18nPrefix = 'plugin.installModal' diff --git a/web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx b/web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx new file mode 100644 index 0000000000..733ebbd945 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx @@ -0,0 +1,47 @@ +import { + memo, + useState, +} from 'react' +import Button from '@/app/components/base/button' +import type { ButtonProps } from '@/app/components/base/button' +import ApiKeyModal from './api-key-modal' +import type { PluginPayload } from '../types' + +export type AddApiKeyButtonProps = { + pluginPayload: PluginPayload + buttonVariant?: ButtonProps['variant'] + buttonText?: string + disabled?: boolean +} +const AddApiKeyButton = ({ + pluginPayload, + buttonVariant = 'secondary-accent', + buttonText = 'use api key', + disabled, +}: AddApiKeyButtonProps) => { + const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false) + + return ( + <> + + { + isApiKeyModalOpen && ( + setIsApiKeyModalOpen(false)} + /> + ) + } + + + ) +} + +export default memo(AddApiKeyButton) diff --git a/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx new file mode 100644 index 0000000000..8e6fc5aa03 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx @@ -0,0 +1,228 @@ +import { + memo, + useCallback, + useMemo, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { + RiClipboardLine, + RiEqualizer2Line, + RiInformation2Fill, +} from '@remixicon/react' +import Button from '@/app/components/base/button' +import type { ButtonProps } from '@/app/components/base/button' +import OAuthClientSettings from './oauth-client-settings' +import cn from '@/utils/classnames' +import type { PluginPayload } from '../types' +import { openOAuthPopup } from '@/hooks/use-oauth' +import Badge from '@/app/components/base/badge' +import { + useGetPluginOAuthClientSchemaHook, + useGetPluginOAuthUrlHook, + useInvalidPluginCredentialInfoHook, +} from '../hooks/use-credential' +import type { FormSchema } from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' +import ActionButton from '@/app/components/base/action-button' +import { useRenderI18nObject } from '@/hooks/use-i18n' + +export type AddOAuthButtonProps = { + pluginPayload: PluginPayload + buttonVariant?: ButtonProps['variant'] + buttonText?: string + className?: string + buttonLeftClassName?: string + buttonRightClassName?: string + dividerClassName?: string + disabled?: boolean +} +const AddOAuthButton = ({ + pluginPayload, + buttonVariant = 'primary', + buttonText = 'use oauth', + className, + buttonLeftClassName, + buttonRightClassName, + dividerClassName, + disabled, +}: AddOAuthButtonProps) => { + const { t } = useTranslation() + const renderI18nObject = useRenderI18nObject() + const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false) + const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload) + const { data } = useGetPluginOAuthClientSchemaHook(pluginPayload) + const { + schema = [], + is_oauth_custom_client_enabled, + is_system_oauth_params_exists, + client_params, + redirect_uri, + } = data || {} + + const isConfigured = is_system_oauth_params_exists || !!client_params + const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) + const handleOAuth = useCallback(async () => { + const { authorization_url } = await getPluginOAuthUrl() + + if (authorization_url) { + openOAuthPopup( + authorization_url, + invalidatePluginCredentialInfo, + ) + } + }, [getPluginOAuthUrl, invalidatePluginCredentialInfo]) + + const renderCustomLabel = useCallback((item: FormSchema) => { + return ( +
+
+
+ +
+
+
+ {t('plugin.auth.clientInfo')} +
+ { + redirect_uri && ( +
+
{redirect_uri}
+ { + navigator.clipboard.writeText(redirect_uri || '') + }} + > + + +
+ ) + } +
+
+
+ {renderI18nObject(item.label as Record)} +
+
+ ) + }, [t, redirect_uri, renderI18nObject]) + const memorizedSchemas = useMemo(() => { + const result: FormSchema[] = schema.map((item, index) => { + return { + ...item, + label: index === 0 ? renderCustomLabel(item) : item.label, + labelClassName: index === 0 ? 'h-auto' : undefined, + } + }) + if (is_system_oauth_params_exists) { + result.unshift({ + name: '__oauth_client__', + label: t('plugin.auth.oauthClient'), + type: FormTypeEnum.radio, + options: [ + { + label: t('plugin.auth.default'), + value: 'default', + }, + { + label: t('plugin.auth.custom'), + value: 'custom', + }, + ], + required: false, + default: is_oauth_custom_client_enabled ? 'custom' : 'default', + } as FormSchema) + result.forEach((item, index) => { + if (index > 0) { + item.show_on = [ + { + variable: '__oauth_client__', + value: 'custom', + }, + ] + if (client_params) + item.default = client_params[item.name] || item.default + } + }) + } + + return result + }, [schema, renderCustomLabel, t, is_system_oauth_params_exists, is_oauth_custom_client_enabled, client_params]) + + return ( + <> + { + isConfigured && ( + + ) + } + { + !isConfigured && ( + + ) + } + { + isOAuthSettingsOpen && ( + setIsOAuthSettingsOpen(false)} + disabled={disabled} + schemas={memorizedSchemas} + onAuth={handleOAuth} + editValues={client_params} + /> + ) + } + + ) +} + +export default memo(AddOAuthButton) diff --git a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx new file mode 100644 index 0000000000..5e7f8a7adb --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -0,0 +1,175 @@ +import { + memo, + useCallback, + useMemo, + useRef, +} from 'react' +import { useTranslation } from 'react-i18next' +import { RiExternalLinkLine } from '@remixicon/react' +import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' +import Modal from '@/app/components/base/modal/modal' +import { CredentialTypeEnum } from '../types' +import { transformFormSchemasSecretInput } from '../utils' +import AuthForm from '@/app/components/base/form/form-scenarios/auth' +import type { FormRefObject } from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' +import { useToastContext } from '@/app/components/base/toast' +import Loading from '@/app/components/base/loading' +import type { PluginPayload } from '../types' +import { + useAddPluginCredentialHook, + useGetPluginCredentialSchemaHook, + useInvalidPluginCredentialInfoHook, + useUpdatePluginCredentialHook, +} from '../hooks/use-credential' +import { useRenderI18nObject } from '@/hooks/use-i18n' + +export type ApiKeyModalProps = { + pluginPayload: PluginPayload + onClose?: () => void + editValues?: Record + onRemove?: () => void + disabled?: boolean +} +const ApiKeyModal = ({ + pluginPayload, + onClose, + editValues, + onRemove, + disabled, +}: ApiKeyModalProps) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY) + const formSchemas = useMemo(() => { + return [ + { + type: FormTypeEnum.textInput, + name: '__name__', + label: t('plugin.auth.authorizationName'), + required: false, + }, + ...data, + ] + }, [data, t]) + const defaultValues = formSchemas.reduce((acc, schema) => { + if (schema.default) + acc[schema.name] = schema.default + return acc + }, {} as Record) + const secretInput = formSchemas.find(schema => schema.type === FormTypeEnum.secretInput) + const renderI18nObject = useRenderI18nObject() + const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload) + const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) + const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) + const formRef = useRef(null) + const handleConfirm = useCallback(async () => { + const form = formRef.current?.getForm() + const store = form?.store + const { + __name__, + __credential_id__, + ...values + } = store?.state.values + const isPristineSecretInputNames: string[] = [] + for (let i = 0; i < formSchemas.length; i++) { + const schema = formSchemas[i] + if (schema.required && !values[schema.name]) { + notify({ + type: 'error', + message: t('common.errorMsg.fieldRequired', { field: schema.name }), + }) + return + } + if (schema.type === FormTypeEnum.secretInput) { + const fieldMeta = form?.getFieldMeta(schema.name) + if (fieldMeta?.isPristine) + isPristineSecretInputNames.push(schema.name) + } + } + + const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values) + + if (editValues) { + await updatePluginCredential({ + credentials: transformedValues, + credential_id: __credential_id__, + name: __name__ || '', + }) + } + else { + await addPluginCredential({ + credentials: transformedValues, + type: CredentialTypeEnum.API_KEY, + name: __name__ || '', + }) + } + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + + onClose?.() + invalidatePluginCredentialInfo() + }, [addPluginCredential, onClose, invalidatePluginCredentialInfo, updatePluginCredential, notify, t, editValues, formSchemas]) + + return ( + + {renderI18nObject(secretInput?.help as any)} + + + ) + } + bottomSlot={ +
+ + {t('common.modelProvider.encrypted.front')} + + PKCS1_OAEP + + {t('common.modelProvider.encrypted.back')} +
+ } + onConfirm={handleConfirm} + showExtraButton={!!editValues} + onExtraButtonClick={onRemove} + disabled={disabled} + > + { + isLoading && ( +
+ +
+ ) + } + { + !isLoading && !!data.length && ( + + ) + } +
+ ) +} + +export default memo(ApiKeyModal) diff --git a/web/app/components/plugins/plugin-auth/authorize/index.tsx b/web/app/components/plugins/plugin-auth/authorize/index.tsx new file mode 100644 index 0000000000..80f1656fb8 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/index.tsx @@ -0,0 +1,96 @@ +import { + memo, + useMemo, +} from 'react' +import { useTranslation } from 'react-i18next' +import AddOAuthButton from './add-oauth-button' +import type { AddOAuthButtonProps } from './add-oauth-button' +import AddApiKeyButton from './add-api-key-button' +import type { AddApiKeyButtonProps } from './add-api-key-button' +import type { PluginPayload } from '../types' + +type AuthorizeProps = { + pluginPayload: PluginPayload + theme?: 'primary' | 'secondary' + showDivider?: boolean + canOAuth?: boolean + canApiKey?: boolean + disabled?: boolean +} +const Authorize = ({ + pluginPayload, + theme = 'primary', + showDivider = true, + canOAuth, + canApiKey, + disabled, +}: AuthorizeProps) => { + const { t } = useTranslation() + const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => { + if (theme === 'secondary') { + return { + buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'), + buttonVariant: 'secondary', + className: 'hover:bg-components-button-secondary-bg', + buttonLeftClassName: 'hover:bg-components-button-secondary-bg-hover', + buttonRightClassName: 'hover:bg-components-button-secondary-bg-hover', + dividerClassName: 'bg-divider-regular opacity-100', + pluginPayload, + } + } + + return { + buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'), + pluginPayload, + } + }, [canApiKey, theme, pluginPayload, t]) + + const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => { + if (theme === 'secondary') { + return { + pluginPayload, + buttonVariant: 'secondary', + buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'), + } + } + return { + pluginPayload, + buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'), + buttonVariant: !canOAuth ? 'primary' : 'secondary-accent', + } + }, [canOAuth, theme, pluginPayload, t]) + + return ( + <> +
+ { + canOAuth && ( + + ) + } + { + showDivider && canOAuth && canApiKey && ( +
+
+ or +
+
+ ) + } + { + canApiKey && ( + + ) + } +
+ + ) +} + +export default memo(Authorize) diff --git a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx new file mode 100644 index 0000000000..7752d584fb --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx @@ -0,0 +1,115 @@ +import { + memo, + useCallback, + useRef, +} from 'react' +import { useTranslation } from 'react-i18next' +import Modal from '@/app/components/base/modal/modal' +import { + useInvalidPluginCredentialInfoHook, + useSetPluginOAuthCustomClientHook, +} from '../hooks/use-credential' +import type { PluginPayload } from '../types' +import AuthForm from '@/app/components/base/form/form-scenarios/auth' +import type { + FormRefObject, + FormSchema, +} from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' +import { transformFormSchemasSecretInput } from '../utils' +import { useToastContext } from '@/app/components/base/toast' + +type OAuthClientSettingsProps = { + pluginPayload: PluginPayload + onClose?: () => void + editValues?: Record + disabled?: boolean + schemas: FormSchema[] + onAuth?: () => Promise +} +const OAuthClientSettings = ({ + pluginPayload, + onClose, + editValues, + disabled, + schemas, + onAuth, +}: OAuthClientSettingsProps) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const defaultValues = schemas.reduce((acc, schema) => { + if (schema.default) + acc[schema.name] = schema.default + return acc + }, {} as Record) + const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload) + const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) + const formRef = useRef(null) + const handleConfirm = useCallback(async () => { + const form = formRef.current?.getForm() + const store = form?.store + const { + __oauth_client__, + ...values + } = store?.state.values + const isPristineSecretInputNames: string[] = [] + for (let i = 0; i < schemas.length; i++) { + const schema = schemas[i] + if (schema.required && !values[schema.name]) { + notify({ + type: 'error', + message: t('common.errorMsg.fieldRequired', { field: schema.name }), + }) + return + } + if (schema.type === FormTypeEnum.secretInput) { + const fieldMeta = form?.getFieldMeta(schema.name) + if (fieldMeta?.isPristine) + isPristineSecretInputNames.push(schema.name) + } + } + + const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values) + + await setPluginOAuthCustomClient({ + client_params: transformedValues, + enable_oauth_custom_client: __oauth_client__ === 'custom', + }) + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + + onClose?.() + invalidatePluginCredentialInfo() + }, [onClose, invalidatePluginCredentialInfo, setPluginOAuthCustomClient, notify, t, schemas]) + + const handleConfirmAndAuthorize = useCallback(async () => { + await handleConfirm() + if (onAuth) + await onAuth() + }, [handleConfirm, onAuth]) + return ( + + + + ) +} + +export default memo(OAuthClientSettings) diff --git a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx new file mode 100644 index 0000000000..1b6e090a44 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx @@ -0,0 +1,111 @@ +import { + memo, + useCallback, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { RiArrowDownSLine } from '@remixicon/react' +import Button from '@/app/components/base/button' +import Indicator from '@/app/components/header/indicator' +import cn from '@/utils/classnames' +import type { + Credential, + PluginPayload, +} from './types' +import { + Authorized, + usePluginAuth, +} from '.' + +type AuthorizedInNodeProps = { + pluginPayload: PluginPayload + onAuthorizationItemClick: (id: string) => void + credentialId?: string +} +const AuthorizedInNode = ({ + pluginPayload, + onAuthorizationItemClick, + credentialId, +}: AuthorizedInNodeProps) => { + const { t } = useTranslation() + const [isOpen, setIsOpen] = useState(false) + const { + canApiKey, + canOAuth, + credentials, + disabled, + } = usePluginAuth(pluginPayload, isOpen || !!credentialId) + const renderTrigger = useCallback((open?: boolean) => { + let label = '' + let removed = false + if (!credentialId) { + label = t('plugin.auth.workspaceDefault') + } + else { + const credential = credentials.find(c => c.id === credentialId) + label = credential ? credential.name : t('plugin.auth.authRemoved') + removed = !credential + } + return ( + + ) + }, [credentialId, credentials, t]) + const extraAuthorizationItems: Credential[] = [ + { + id: '__workspace_default__', + name: t('plugin.auth.workspaceDefault'), + provider: '', + is_default: !credentialId, + isWorkspaceDefault: true, + }, + ] + const handleAuthorizationItemClick = useCallback((id: string) => { + onAuthorizationItemClick(id) + setIsOpen(false) + }, [ + onAuthorizationItemClick, + setIsOpen, + ]) + + return ( + + ) +} + +export default memo(AuthorizedInNode) diff --git a/web/app/components/plugins/plugin-auth/authorized/index.tsx b/web/app/components/plugins/plugin-auth/authorized/index.tsx new file mode 100644 index 0000000000..9caef74d95 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized/index.tsx @@ -0,0 +1,309 @@ +import { + memo, + useCallback, + useRef, + useState, +} from 'react' +import { + RiArrowDownSLine, +} from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import type { + PortalToFollowElemOptions, +} from '@/app/components/base/portal-to-follow-elem' +import Button from '@/app/components/base/button' +import Indicator from '@/app/components/header/indicator' +import cn from '@/utils/classnames' +import Confirm from '@/app/components/base/confirm' +import Authorize from '../authorize' +import type { Credential } from '../types' +import { CredentialTypeEnum } from '../types' +import ApiKeyModal from '../authorize/api-key-modal' +import Item from './item' +import { useToastContext } from '@/app/components/base/toast' +import type { PluginPayload } from '../types' +import { + useDeletePluginCredentialHook, + useInvalidPluginCredentialInfoHook, + useSetPluginDefaultCredentialHook, + useUpdatePluginCredentialHook, +} from '../hooks/use-credential' + +type AuthorizedProps = { + pluginPayload: PluginPayload + credentials: Credential[] + canOAuth?: boolean + canApiKey?: boolean + disabled?: boolean + renderTrigger?: (open?: boolean) => React.ReactNode + isOpen?: boolean + onOpenChange?: (open: boolean) => void + offset?: PortalToFollowElemOptions['offset'] + placement?: PortalToFollowElemOptions['placement'] + triggerPopupSameWidth?: boolean + popupClassName?: string + disableSetDefault?: boolean + onItemClick?: (id: string) => void + extraAuthorizationItems?: Credential[] + showItemSelectedIcon?: boolean + selectedCredentialId?: string +} +const Authorized = ({ + pluginPayload, + credentials, + canOAuth, + canApiKey, + disabled, + renderTrigger, + isOpen, + onOpenChange, + offset = 8, + placement = 'bottom-start', + triggerPopupSameWidth = true, + popupClassName, + disableSetDefault, + onItemClick, + extraAuthorizationItems, + showItemSelectedIcon, + selectedCredentialId, +}: AuthorizedProps) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const [isLocalOpen, setIsLocalOpen] = useState(false) + const mergedIsOpen = isOpen ?? isLocalOpen + const setMergedIsOpen = useCallback((open: boolean) => { + if (onOpenChange) + onOpenChange(open) + + setIsLocalOpen(open) + }, [onOpenChange]) + const oAuthCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.OAUTH2) + const apiKeyCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.API_KEY) + const pendingOperationCredentialId = useRef(null) + const [deleteCredentialId, setDeleteCredentialId] = useState(null) + const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload) + const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) + const openConfirm = useCallback((credentialId?: string) => { + if (credentialId) + pendingOperationCredentialId.current = credentialId + + setDeleteCredentialId(pendingOperationCredentialId.current) + }, []) + const closeConfirm = useCallback(() => { + setDeleteCredentialId(null) + pendingOperationCredentialId.current = null + }, []) + const handleConfirm = useCallback(async () => { + if (!pendingOperationCredentialId.current) { + setDeleteCredentialId(null) + return + } + + await deletePluginCredential({ credential_id: pendingOperationCredentialId.current }) + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + invalidatePluginCredentialInfo() + setDeleteCredentialId(null) + pendingOperationCredentialId.current = null + }, [deletePluginCredential, invalidatePluginCredentialInfo, notify, t]) + const [editValues, setEditValues] = useState | null>(null) + const handleEdit = useCallback((id: string, values: Record) => { + pendingOperationCredentialId.current = id + setEditValues(values) + }, []) + const handleRemove = useCallback(() => { + setDeleteCredentialId(pendingOperationCredentialId.current) + }, []) + const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload) + const handleSetDefault = useCallback(async (id: string) => { + await setPluginDefaultCredential(id) + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + invalidatePluginCredentialInfo() + }, [setPluginDefaultCredential, invalidatePluginCredentialInfo, notify, t]) + const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) + const handleRename = useCallback(async (payload: { + credential_id: string + name: string + }) => { + await updatePluginCredential(payload) + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + }, [updatePluginCredential, notify, t]) + + return ( + <> + + setMergedIsOpen(!mergedIsOpen)} + asChild + > + { + renderTrigger + ? renderTrigger(mergedIsOpen) + : ( + + ) + } + + +
+
+ { + !!extraAuthorizationItems?.length && ( +
+ { + extraAuthorizationItems.map(credential => ( + + )) + } +
+ ) + } + { + !!oAuthCredentials.length && ( +
+
+ OAuth +
+ { + oAuthCredentials.map(credential => ( + + )) + } +
+ ) + } + { + !!apiKeyCredentials.length && ( +
+
+ API Keys +
+ { + apiKeyCredentials.map(credential => ( + + )) + } +
+ ) + } +
+
+
+ +
+
+
+
+ { + deleteCredentialId && ( + + ) + } + { + !!editValues && ( + { + setEditValues(null) + pendingOperationCredentialId.current = null + }} + onRemove={handleRemove} + disabled={disabled} + /> + ) + } + + ) +} + +export default memo(Authorized) diff --git a/web/app/components/plugins/plugin-auth/authorized/item.tsx b/web/app/components/plugins/plugin-auth/authorized/item.tsx new file mode 100644 index 0000000000..4c0f88d985 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized/item.tsx @@ -0,0 +1,214 @@ +import { + memo, + useMemo, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCheckLine, + RiDeleteBinLine, + RiEditLine, + RiEqualizer2Line, +} from '@remixicon/react' +import Indicator from '@/app/components/header/indicator' +import Badge from '@/app/components/base/badge' +import ActionButton from '@/app/components/base/action-button' +import Tooltip from '@/app/components/base/tooltip' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import cn from '@/utils/classnames' +import type { Credential } from '../types' +import { CredentialTypeEnum } from '../types' + +type ItemProps = { + credential: Credential + disabled?: boolean + onDelete?: (id: string) => void + onEdit?: (id: string, values: Record) => void + onSetDefault?: (id: string) => void + onRename?: (payload: { + credential_id: string + name: string + }) => void + disableRename?: boolean + disableEdit?: boolean + disableDelete?: boolean + disableSetDefault?: boolean + onItemClick?: (id: string) => void + showSelectedIcon?: boolean + selectedCredentialId?: string +} +const Item = ({ + credential, + disabled, + onDelete, + onEdit, + onSetDefault, + onRename, + disableRename, + disableEdit, + disableDelete, + disableSetDefault, + onItemClick, + showSelectedIcon, + selectedCredentialId, +}: ItemProps) => { + const { t } = useTranslation() + const [renaming, setRenaming] = useState(false) + const [renameValue, setRenameValue] = useState(credential.name) + const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2 + const showAction = useMemo(() => { + return !(disableRename && disableEdit && disableDelete && disableSetDefault) + }, [disableRename, disableEdit, disableDelete, disableSetDefault]) + + return ( +
onItemClick?.(credential.id)} + > + { + renaming && ( +
+ setRenameValue(e.target.value)} + placeholder={t('common.placeholder.input')} + /> + + +
+ ) + } + { + !renaming && ( +
+ { + showSelectedIcon && ( +
+ { + selectedCredentialId === credential.id && ( + + ) + } +
+ ) + } + +
+ {credential.name} +
+ { + credential.is_default && ( + + {t('plugin.auth.default')} + + ) + } +
+ ) + } + { + showAction && !renaming && ( +
+ { + !credential.is_default && !disableSetDefault && ( + + ) + } + { + !disableRename && ( + + { + e.stopPropagation() + setRenaming(true) + setRenameValue(credential.name) + }} + > + + + + ) + } + { + !isOAuth && !disableEdit && ( + + { + e.stopPropagation() + onEdit?.( + credential.id, + { + ...credential.credentials, + __name__: credential.name, + __credential_id__: credential.id, + }, + ) + }} + > + + + + ) + } + { + !disableDelete && ( + + { + e.stopPropagation() + onDelete?.(credential.id) + }} + > + + + + ) + } +
+ ) + } +
+ ) +} + +export default memo(Item) diff --git a/web/app/components/plugins/plugin-auth/hooks/use-credential.ts b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts new file mode 100644 index 0000000000..de1ec8a80d --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts @@ -0,0 +1,74 @@ +import { + useAddPluginCredential, + useDeletePluginCredential, + useGetPluginCredentialInfo, + useGetPluginCredentialSchema, + useGetPluginOAuthClientSchema, + useGetPluginOAuthUrl, + useInvalidPluginCredentialInfo, + useSetPluginDefaultCredential, + useSetPluginOAuthCustomClient, + useUpdatePluginCredential, +} from '@/service/use-plugins-auth' +import { useGetApi } from './use-get-api' +import type { PluginPayload } from '../types' +import type { CredentialTypeEnum } from '../types' + +export const useGetPluginCredentialInfoHook = (pluginPayload: PluginPayload, enable?: boolean) => { + const apiMap = useGetApi(pluginPayload) + return useGetPluginCredentialInfo(enable ? apiMap.getCredentialInfo : '') +} + +export const useDeletePluginCredentialHook = (pluginPayload: PluginPayload) => { + const apiMap = useGetApi(pluginPayload) + + return useDeletePluginCredential(apiMap.deleteCredential) +} + +export const useInvalidPluginCredentialInfoHook = (pluginPayload: PluginPayload) => { + const apiMap = useGetApi(pluginPayload) + + return useInvalidPluginCredentialInfo(apiMap.getCredentialInfo) +} + +export const useSetPluginDefaultCredentialHook = (pluginPayload: PluginPayload) => { + const apiMap = useGetApi(pluginPayload) + + return useSetPluginDefaultCredential(apiMap.setDefaultCredential) +} + +export const useGetPluginCredentialSchemaHook = (pluginPayload: PluginPayload, credentialType: CredentialTypeEnum) => { + const apiMap = useGetApi(pluginPayload) + + return useGetPluginCredentialSchema(apiMap.getCredentialSchema(credentialType)) +} + +export const useAddPluginCredentialHook = (pluginPayload: PluginPayload) => { + const apiMap = useGetApi(pluginPayload) + + return useAddPluginCredential(apiMap.addCredential) +} + +export const useUpdatePluginCredentialHook = (pluginPayload: PluginPayload) => { + const apiMap = useGetApi(pluginPayload) + + return useUpdatePluginCredential(apiMap.updateCredential) +} + +export const useGetPluginOAuthUrlHook = (pluginPayload: PluginPayload) => { + const apiMap = useGetApi(pluginPayload) + + return useGetPluginOAuthUrl(apiMap.getOauthUrl) +} + +export const useGetPluginOAuthClientSchemaHook = (pluginPayload: PluginPayload) => { + const apiMap = useGetApi(pluginPayload) + + return useGetPluginOAuthClientSchema(apiMap.getOauthClientSchema) +} + +export const useSetPluginOAuthCustomClientHook = (pluginPayload: PluginPayload) => { + const apiMap = useGetApi(pluginPayload) + + return useSetPluginOAuthCustomClient(apiMap.setCustomOauthClient) +} diff --git a/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts b/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts new file mode 100644 index 0000000000..9b88cb0dad --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts @@ -0,0 +1,39 @@ +import { + AuthCategory, +} from '../types' +import type { + CredentialTypeEnum, + PluginPayload, +} from '../types' + +export const useGetApi = ({ category = AuthCategory.tool, provider }: PluginPayload) => { + if (category === AuthCategory.tool) { + return { + getCredentialInfo: `/workspaces/current/tool-provider/builtin/${provider}/credential/info`, + setDefaultCredential: `/workspaces/current/tool-provider/builtin/${provider}/default-credential`, + getCredentials: `/workspaces/current/tool-provider/builtin/${provider}/credentials`, + addCredential: `/workspaces/current/tool-provider/builtin/${provider}/add`, + updateCredential: `/workspaces/current/tool-provider/builtin/${provider}/update`, + deleteCredential: `/workspaces/current/tool-provider/builtin/${provider}/delete`, + getCredentialSchema: (credential_type: CredentialTypeEnum) => `/workspaces/current/tool-provider/builtin/${provider}/credential/schema/${credential_type}`, + getOauthUrl: `/oauth/plugin/${provider}/tool/authorization-url`, + getOauthClientSchema: `/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`, + setCustomOauthClient: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, + getCustomOAuthClientValues: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, + } + } + + return { + getCredentialInfo: '', + setDefaultCredential: '', + getCredentials: '', + addCredential: '', + updateCredential: '', + deleteCredential: '', + getCredentialSchema: () => '', + getOauthUrl: '', + getOauthClientSchema: '', + setCustomOauthClient: '', + getCustomOAuthClientValues: '', + } +} diff --git a/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts new file mode 100644 index 0000000000..67d19e3de1 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts @@ -0,0 +1,20 @@ +import { useAppContext } from '@/context/app-context' +import { useGetPluginCredentialInfoHook } from './use-credential' +import { CredentialTypeEnum } from '../types' +import type { PluginPayload } from '../types' + +export const usePluginAuth = (pluginPayload: PluginPayload, enable?: boolean) => { + const { data } = useGetPluginCredentialInfoHook(pluginPayload, enable) + const { isCurrentWorkspaceManager } = useAppContext() + const isAuthorized = !!data?.credentials.length + const canOAuth = data?.supported_credential_types.includes(CredentialTypeEnum.OAUTH2) + const canApiKey = data?.supported_credential_types.includes(CredentialTypeEnum.API_KEY) + + return { + isAuthorized, + canOAuth, + canApiKey, + credentials: data?.credentials || [], + disabled: !isCurrentWorkspaceManager, + } +} diff --git a/web/app/components/plugins/plugin-auth/index.tsx b/web/app/components/plugins/plugin-auth/index.tsx new file mode 100644 index 0000000000..e4f6ae8b2f --- /dev/null +++ b/web/app/components/plugins/plugin-auth/index.tsx @@ -0,0 +1,6 @@ +export { default as PluginAuth } from './plugin-auth' +export { default as Authorized } from './authorized' +export { default as AuthorizedInNode } from './authorized-in-node' +export { default as PluginAuthInAgent } from './plugin-auth-in-agent' +export { usePluginAuth } from './hooks/use-plugin-auth' +export * from './types' diff --git a/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx b/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx new file mode 100644 index 0000000000..4c7b885226 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx @@ -0,0 +1,120 @@ +import { + memo, + useCallback, + useState, +} from 'react' +import { RiArrowDownSLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import Authorize from './authorize' +import Authorized from './authorized' +import type { + Credential, + PluginPayload, +} from './types' +import { usePluginAuth } from './hooks/use-plugin-auth' +import Button from '@/app/components/base/button' +import Indicator from '@/app/components/header/indicator' +import cn from '@/utils/classnames' + +type PluginAuthInAgentProps = { + pluginPayload: PluginPayload + credentialId?: string + onAuthorizationItemClick?: (id: string) => void +} +const PluginAuthInAgent = ({ + pluginPayload, + credentialId, + onAuthorizationItemClick, +}: PluginAuthInAgentProps) => { + const { t } = useTranslation() + const [isOpen, setIsOpen] = useState(false) + const { + isAuthorized, + canOAuth, + canApiKey, + credentials, + disabled, + } = usePluginAuth(pluginPayload, true) + + const extraAuthorizationItems: Credential[] = [ + { + id: '__workspace_default__', + name: t('plugin.auth.workspaceDefault'), + provider: '', + is_default: !credentialId, + isWorkspaceDefault: true, + }, + ] + + const handleAuthorizationItemClick = useCallback((id: string) => { + onAuthorizationItemClick?.(id) + setIsOpen(false) + }, [ + onAuthorizationItemClick, + setIsOpen, + ]) + + const renderTrigger = useCallback((isOpen?: boolean) => { + let label = '' + let removed = false + if (!credentialId) { + label = t('plugin.auth.workspaceDefault') + } + else { + const credential = credentials.find(c => c.id === credentialId) + label = credential ? credential.name : t('plugin.auth.authRemoved') + removed = !credential + } + return ( + + ) + }, [credentialId, credentials, t]) + + return ( + <> + { + !isAuthorized && ( + + ) + } + { + isAuthorized && ( + + ) + } + + ) +} + +export default memo(PluginAuthInAgent) diff --git a/web/app/components/plugins/plugin-auth/plugin-auth.tsx b/web/app/components/plugins/plugin-auth/plugin-auth.tsx new file mode 100644 index 0000000000..5056105641 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx @@ -0,0 +1,53 @@ +import { memo } from 'react' +import Authorize from './authorize' +import Authorized from './authorized' +import type { PluginPayload } from './types' +import { usePluginAuth } from './hooks/use-plugin-auth' + +type PluginAuthProps = { + pluginPayload: PluginPayload + children?: React.ReactNode +} +const PluginAuth = ({ + pluginPayload, + children, +}: PluginAuthProps) => { + const { + isAuthorized, + canOAuth, + canApiKey, + credentials, + disabled, + } = usePluginAuth(pluginPayload, !!pluginPayload.provider) + + return ( + <> + { + !isAuthorized && ( + + ) + } + { + isAuthorized && !children && ( + + ) + } + { + isAuthorized && children + } + + ) +} + +export default memo(PluginAuth) diff --git a/web/app/components/plugins/plugin-auth/types.ts b/web/app/components/plugins/plugin-auth/types.ts new file mode 100644 index 0000000000..ad41733bde --- /dev/null +++ b/web/app/components/plugins/plugin-auth/types.ts @@ -0,0 +1,25 @@ +export enum AuthCategory { + tool = 'tool', + datasource = 'datasource', + model = 'model', +} + +export type PluginPayload = { + category: AuthCategory + provider: string +} + +export enum CredentialTypeEnum { + OAUTH2 = 'oauth2', + API_KEY = 'api-key', +} + +export type Credential = { + id: string + name: string + provider: string + credential_type?: CredentialTypeEnum + is_default: boolean + credentials?: Record + isWorkspaceDefault?: boolean +} diff --git a/web/app/components/plugins/plugin-auth/utils.ts b/web/app/components/plugins/plugin-auth/utils.ts new file mode 100644 index 0000000000..d264cfb198 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/utils.ts @@ -0,0 +1,10 @@ +export const transformFormSchemasSecretInput = (isPristineSecretInputNames: string[], values: Record) => { + const transformedValues: Record = { ...values } + + isPristineSecretInputNames.forEach((name) => { + if (transformedValues[name]) + transformedValues[name] = '[__HIDDEN__]' + }) + + return transformedValues +} diff --git a/web/app/components/plugins/plugin-detail-panel/action-list.tsx b/web/app/components/plugins/plugin-detail-panel/action-list.tsx index 2505b6d5aa..040c728630 100644 --- a/web/app/components/plugins/plugin-detail-panel/action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/action-list.tsx @@ -1,17 +1,9 @@ -import React, { useMemo, useState } from 'react' +import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useAppContext } from '@/context/app-context' -import Button from '@/app/components/base/button' -import Toast from '@/app/components/base/toast' -import Indicator from '@/app/components/header/indicator' import ToolItem from '@/app/components/tools/provider/tool-item' -import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import { useAllToolProviders, useBuiltinTools, - useInvalidateAllToolProviders, - useRemoveProviderCredentials, - useUpdateProviderCredentials, } from '@/service/use-tools' import type { PluginDetail } from '@/app/components/plugins/types' @@ -23,35 +15,14 @@ const ActionList = ({ detail, }: Props) => { const { t } = useTranslation() - const { isCurrentWorkspaceManager } = useAppContext() const providerBriefInfo = detail.declaration.tool.identity const providerKey = `${detail.plugin_id}/${providerBriefInfo.name}` const { data: collectionList = [] } = useAllToolProviders() - const invalidateAllToolProviders = useInvalidateAllToolProviders() const provider = useMemo(() => { return collectionList.find(collection => collection.name === providerKey) }, [collectionList, providerKey]) const { data } = useBuiltinTools(providerKey) - const [showSettingAuth, setShowSettingAuth] = useState(false) - - const handleCredentialSettingUpdate = () => { - invalidateAllToolProviders() - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - setShowSettingAuth(false) - } - - const { mutate: updatePermission, isPending } = useUpdateProviderCredentials({ - onSuccess: handleCredentialSettingUpdate, - }) - - const { mutate: removePermission } = useRemoveProviderCredentials({ - onSuccess: handleCredentialSettingUpdate, - }) - if (!data || !provider) return null @@ -60,26 +31,7 @@ const ActionList = ({
{t('plugin.detailPanel.actionNum', { num: data.length, action: data.length > 1 ? 'actions' : 'action' })} - {provider.is_team_authorization && provider.allow_delete && ( - - )}
- {!provider.is_team_authorization && provider.allow_delete && ( - - )}
{data.map(tool => ( @@ -93,18 +45,6 @@ const ActionList = ({ /> ))}
- {showSettingAuth && ( - setShowSettingAuth(false)} - onSaved={async value => updatePermission({ - providerName: provider.name, - credentials: value, - })} - onRemove={async () => removePermission(provider.name)} - isSaving={isPending} - /> - )}
) } diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 0a5a8b87d6..53d01c37db 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -35,7 +35,15 @@ import { useProviderContext } from '@/context/provider-context' import { useInvalidateAllToolProviders } from '@/service/use-tools' import { API_PREFIX } from '@/config' import cn from '@/utils/classnames' +import { AutoUpdateLine } from '../../base/icons/src/vender/system' +import { convertUTCDaySecondsToLocalSeconds, timeOfDayToDayjs } from '../reference-setting-modal/auto-update-setting/utils' import { getMarketplaceUrl } from '@/utils/var' +import useReferenceSetting from '../plugin-page/use-reference-setting' +import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types' +import { useAppContext } from '@/context/app-context' +import { PluginAuth } from '@/app/components/plugins/plugin-auth' +import { AuthCategory } from '@/app/components/plugins/plugin-auth' +import { useAllToolProviders } from '@/service/use-tools' const i18nPrefix = 'plugin.action' @@ -51,6 +59,8 @@ const DetailHeader = ({ onUpdate, }: Props) => { const { t } = useTranslation() + const { userProfile: { timezone } } = useAppContext() + const { theme } = useTheme() const locale = useGetLanguage() const { checkForUpdates, fetchReleases } = useGitHubReleases() @@ -68,7 +78,14 @@ const DetailHeader = ({ meta, plugin_id, } = detail - const { author, category, name, label, description, icon, verified } = detail.declaration + const { author, category, name, label, description, icon, verified, tool } = detail.declaration + const isTool = category === PluginType.tool + const providerBriefInfo = tool?.identity + const providerKey = `${plugin_id}/${providerBriefInfo?.name}` + const { data: collectionList = [] } = useAllToolProviders(isTool) + const provider = useMemo(() => { + return collectionList.find(collection => collection.name === providerKey) + }, [collectionList, providerKey]) const isFromGitHub = source === PluginSource.github const isFromMarketplace = source === PluginSource.marketplace @@ -97,8 +114,24 @@ const DetailHeader = ({ setFalse: hideUpdateModal, }] = useBoolean(false) - const handleUpdate = async () => { + const { referenceSetting } = useReferenceSetting() + const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {} + const isAutoUpgradeEnabled = useMemo(() => { + if(!autoUpgradeInfo) + return false + if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.update_all) + return true + if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.partial && autoUpgradeInfo.include_plugins.includes(plugin_id)) + return true + if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.exclude && !autoUpgradeInfo.exclude_plugins.includes(plugin_id)) + return true + return false + }, [autoUpgradeInfo, plugin_id]) + + const [isDowngrade, setIsDowngrade] = useState(false) + const handleUpdate = async (isDowngrade?: boolean) => { if (isFromMarketplace) { + setIsDowngrade(!!isDowngrade) showUpdateModal() return } @@ -165,9 +198,6 @@ const DetailHeader = ({ } }, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders]) - // #plugin TODO# used in apps - // const usedInApps = 3 - return (
@@ -186,7 +216,7 @@ const DetailHeader = ({ currentVersion={version} onSelect={(state) => { setTargetVersion(state) - handleUpdate() + handleUpdate(state.isDowngrade) }} trigger={ } /> + {/* Auto update info */} + {isAutoUpgradeEnabled && ( + + {/* add a a div to fix tooltip hover not show problem */} +
+ + + +
+
+ )} + {(hasNewVersion || isFromGitHub) && (
- + + { + category === PluginType.tool && ( + + ) + } {isShowPluginInfo && ( ) } diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index 42467ce111..9c7c7a0c41 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -3,9 +3,6 @@ import type { FC } from 'react' import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Link from 'next/link' -import { - RiArrowLeftLine, -} from '@remixicon/react' import { PortalToFollowElem, PortalToFollowElemContent, @@ -15,24 +12,17 @@ import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selec import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form' -import Button from '@/app/components/base/button' -import Indicator from '@/app/components/header/indicator' -import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form' -import Toast from '@/app/components/base/toast' import Textarea from '@/app/components/base/textarea' import Divider from '@/app/components/base/divider' import TabSlider from '@/app/components/base/tab-slider-plain' import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form' import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' - -import { useAppContext } from '@/context/app-context' import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools, - useUpdateProviderCredentials, } from '@/service/use-tools' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks' @@ -46,6 +36,10 @@ import { MARKETPLACE_API_PREFIX } from '@/config' import type { Node } from 'reactflow' import type { NodeOutPutVar } from '@/app/components/workflow/types' import cn from '@/utils/classnames' +import { + AuthCategory, + PluginAuthInAgent, +} from '@/app/components/plugins/plugin-auth' type Props = { disabled?: boolean @@ -55,7 +49,7 @@ type Props = { value?: ToolValue selectedTools?: ToolValue[] onSelect: (tool: ToolValue) => void - onSelectMultiple: (tool: ToolValue[]) => void + onSelectMultiple?: (tool: ToolValue[]) => void isEdit?: boolean onDelete?: () => void supportEnableSwitch?: boolean @@ -143,7 +137,7 @@ const ToolSelector: FC = ({ } const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => { const toolValues = tool.map(item => getToolValue(item)) - onSelectMultiple(toolValues) + onSelectMultiple?.(toolValues) } const handleDescriptionChange = (e: React.ChangeEvent) => { @@ -196,23 +190,6 @@ const ToolSelector: FC = ({ } as any) } - // authorization - const { isCurrentWorkspaceManager } = useAppContext() - const [isShowSettingAuth, setShowSettingAuth] = useState(false) - const handleCredentialSettingUpdate = () => { - invalidateAllBuiltinTools() - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - setShowSettingAuth(false) - onShowChange(false) - } - - const { mutate: updatePermission } = useUpdateProviderCredentials({ - onSuccess: handleCredentialSettingUpdate, - }) - // install from marketplace const currentTool = useMemo(() => { return currentProvider?.tools.find(tool => tool.name === value?.tool_name) @@ -226,6 +203,12 @@ const ToolSelector: FC = ({ invalidateAllBuiltinTools() invalidateInstalledPluginList() } + const handleAuthorizationItemClick = (id: string) => { + onSelect({ + ...value, + credential_id: id, + } as any) + } return ( <> @@ -264,7 +247,6 @@ const ToolSelector: FC = ({ onSwitchChange={handleEnabledChange} onDelete={onDelete} noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization} - onAuth={() => setShowSettingAuth(true)} uninstalled={!currentProvider && inMarketPlace} versionMismatch={currentProvider && inMarketPlace && !currentTool} installInfo={manifest?.latest_package_identifier} @@ -284,171 +266,131 @@ const ToolSelector: FC = ({ )} -
- {!isShowSettingAuth && ( - <> -
{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}
- {/* base form */} -
-
-
{t('plugin.detailPanel.toolSelector.toolLabel')}
- - } - isShow={panelShowState || isShowChooseTool} - onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool} - disabled={false} - supportAddCustomTool - onSelect={handleSelectTool} - onSelectMultiple={handleSelectMultipleTool} - scope={scope} - selectedTools={selectedTools} - canChooseMCPTool={canChooseMCPTool} - /> -
-
-
{t('plugin.detailPanel.toolSelector.descriptionLabel')}
-