diff --git a/.devcontainer/post_create_command.sh b/.devcontainer/post_create_command.sh index 93ecac48f2..022f71bfb4 100755 --- a/.devcontainer/post_create_command.sh +++ b/.devcontainer/post_create_command.sh @@ -1,6 +1,6 @@ #!/bin/bash -npm add -g pnpm@10.11.1 +npm add -g pnpm@10.13.1 cd web && pnpm install pipx install uv @@ -12,3 +12,4 @@ echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f do echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc source /home/vscode/.bashrc + diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index b06ab9653e..a283f8d5ca 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -28,7 +28,7 @@ jobs: - name: Check changed files id: changed-files - uses: tj-actions/changed-files@v45 + uses: tj-actions/changed-files@v46 with: files: | api/** @@ -75,7 +75,7 @@ jobs: - name: Check changed files id: changed-files - uses: tj-actions/changed-files@v45 + uses: tj-actions/changed-files@v46 with: files: web/** @@ -113,7 +113,7 @@ jobs: - name: Check changed files id: changed-files - uses: tj-actions/changed-files@v45 + uses: tj-actions/changed-files@v46 with: files: | docker/generate_docker_compose @@ -144,7 +144,7 @@ jobs: - name: Check changed files id: changed-files - uses: tj-actions/changed-files@v45 + uses: tj-actions/changed-files@v46 with: files: | **.sh @@ -152,13 +152,15 @@ jobs: **.yml **Dockerfile dev/** + .editorconfig - name: Super-linter - uses: super-linter/super-linter/slim@v7 + uses: super-linter/super-linter/slim@v8 if: steps.changed-files.outputs.any_changed == 'true' env: BASH_SEVERITY: warning - DEFAULT_BRANCH: main + DEFAULT_BRANCH: origin/main + EDITORCONFIG_FILE_NAME: editorconfig-checker.json FILTER_REGEX_INCLUDE: pnpm-lock.yaml GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} IGNORE_GENERATED_FILES: true @@ -168,16 +170,6 @@ jobs: # FIXME: temporarily disabled until api-docker.yaml's run script is fixed for shellcheck # VALIDATE_GITHUB_ACTIONS: true VALIDATE_DOCKERFILE_HADOLINT: true + VALIDATE_EDITORCONFIG: true VALIDATE_XML: true VALIDATE_YAML: true - - - name: EditorConfig checks - uses: super-linter/super-linter/slim@v7 - env: - DEFAULT_BRANCH: main - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - IGNORE_GENERATED_FILES: true - IGNORE_GITIGNORED_FILES: true - # EditorConfig validation - VALIDATE_EDITORCONFIG: true - EDITORCONFIG_FILE_NAME: editorconfig-checker.json diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 37cfdc5c1e..c3f8fdbaf6 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -27,7 +27,7 @@ jobs: - name: Check changed files id: changed-files - uses: tj-actions/changed-files@v45 + uses: tj-actions/changed-files@v46 with: files: web/** diff --git a/api/.env.example b/api/.env.example index 6d20d28c80..80b1c12cd8 100644 --- a/api/.env.example +++ b/api/.env.example @@ -144,6 +144,8 @@ CONSOLE_CORS_ALLOW_ORIGINS=http://localhost:3000,* # Vector database configuration # Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`. VECTOR_STORE=weaviate +# Prefix used to create collection name in vector database +VECTOR_INDEX_NAME_PREFIX=Vector_index # Weaviate configuration WEAVIATE_ENDPOINT=http://localhost:8080 @@ -469,6 +471,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/Dockerfile b/api/Dockerfile index 7e4997507f..8c7a1717b9 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -47,6 +47,8 @@ RUN \ curl nodejs libgmp-dev libmpfr-dev libmpc-dev \ # For Security expat libldap-2.5-0 perl libsqlite3-0 zlib1g \ + # install fonts to support the use of tools like pypdfium2 + fonts-noto-cjk \ # install a package to improve the accuracy of guessing mime type and file extension media-types \ # install libmagic to support the use of python-magic guess MIMETYPE diff --git a/api/README.md b/api/README.md index 9308d5dc44..6ab923070e 100644 --- a/api/README.md +++ b/api/README.md @@ -74,7 +74,12 @@ 10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service. ```bash - uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion + uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin + ``` + + Addition, if you want to debug the celery scheduled tasks, you can use the following command in another terminal: + ```bash + uv run celery -A app.celery beat ``` ## Testing diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index f1d529355d..9f1646ea7d 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -832,6 +832,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", @@ -961,5 +996,6 @@ class FeatureConfig( # hosted services config HostedServiceConfig, CeleryBeatConfig, + CeleryScheduleTasksConfig, ): pass diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py index 3c349060ca..587ea55ca7 100644 --- a/api/configs/middleware/__init__.py +++ b/api/configs/middleware/__init__.py @@ -85,6 +85,11 @@ class VectorStoreConfig(BaseSettings): default=False, ) + VECTOR_INDEX_NAME_PREFIX: Optional[str] = Field( + description="Prefix used to create collection name in vector database", + default="Vector_index", + ) + class KeywordStoreConfig(BaseSettings): KEYWORD_STORE: str = Field( diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index 70d6216497..4eef9fed43 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -1,4 +1,4 @@ -from datetime import UTC, datetime +from datetime import datetime import pytz # pip install pytz from flask_login import current_user @@ -19,6 +19,7 @@ from fields.conversation_fields import ( conversation_pagination_fields, conversation_with_summary_pagination_fields, ) +from libs.datetime_utils import naive_utc_now from libs.helper import DatetimeString from libs.login import login_required from models import Conversation, EndUser, Message, MessageAnnotation @@ -315,7 +316,7 @@ def _get_conversation(app_model, conversation_id): raise NotFound("Conversation Not Exists.") if not conversation.read_at: - conversation.read_at = datetime.now(UTC).replace(tzinfo=None) + conversation.read_at = naive_utc_now() conversation.read_account_id = current_user.id db.session.commit() diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py index 3c3a359eeb..358a5e8cdb 100644 --- a/api/controllers/console/app/site.py +++ b/api/controllers/console/app/site.py @@ -1,5 +1,3 @@ -from datetime import UTC, datetime - from flask_login import current_user from flask_restful import Resource, marshal_with, reqparse from werkzeug.exceptions import Forbidden, NotFound @@ -10,6 +8,7 @@ from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required from extensions.ext_database import db from fields.app_fields import app_site_fields +from libs.datetime_utils import naive_utc_now from libs.login import login_required from models import Site @@ -77,7 +76,7 @@ class AppSite(Resource): setattr(site, attr_name, value) site.updated_by = current_user.id - site.updated_at = datetime.now(UTC).replace(tzinfo=None) + site.updated_at = naive_utc_now() db.session.commit() return site @@ -101,7 +100,7 @@ class AppSiteAccessTokenReset(Resource): site.code = Site.generate_code(16) site.updated_by = current_user.id - site.updated_at = datetime.now(UTC).replace(tzinfo=None) + site.updated_at = naive_utc_now() db.session.commit() return site diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 1795563ff7..2562fb5eb8 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -1,5 +1,3 @@ -import datetime - from flask import request from flask_restful import Resource, reqparse @@ -7,6 +5,7 @@ from constants.languages import supported_language from controllers.console import api from controllers.console.error import AlreadyActivateError from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from libs.helper import StrLen, email, extract_remote_ip, timezone from models.account import AccountStatus from services.account_service import AccountService, RegisterService @@ -65,7 +64,7 @@ class ActivateApi(Resource): account.timezone = args["timezone"] account.interface_theme = "light" account.status = AccountStatus.ACTIVE.value - account.initialized_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + account.initialized_at = naive_utc_now() db.session.commit() token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 395367c9e2..d0a4f3ff6d 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -1,5 +1,4 @@ import logging -from datetime import UTC, datetime from typing import Optional import requests @@ -13,6 +12,7 @@ from configs import dify_config from constants.languages import languages from events.tenant_event import tenant_was_created from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from libs.helper import extract_remote_ip from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from models import Account @@ -110,7 +110,7 @@ class OAuthCallback(Resource): if account.status == AccountStatus.PENDING.value: account.status = AccountStatus.ACTIVE.value - account.initialized_at = datetime.now(UTC).replace(tzinfo=None) + account.initialized_at = naive_utc_now() db.session.commit() try: diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py index 7b0d9373cf..b49f8affc8 100644 --- a/api/controllers/console/datasets/data_source.py +++ b/api/controllers/console/datasets/data_source.py @@ -1,4 +1,3 @@ -import datetime import json from flask import request @@ -15,6 +14,7 @@ from core.rag.extractor.entity.extract_setting import ExtractSetting from core.rag.extractor.notion_extractor import NotionExtractor from extensions.ext_database import db from fields.data_source_fields import integrate_list_fields, integrate_notion_info_list_fields +from libs.datetime_utils import naive_utc_now from libs.login import login_required from models import DataSourceOauthBinding, Document from services.dataset_service import DatasetService, DocumentService @@ -88,7 +88,7 @@ class DataSourceApi(Resource): if action == "enable": if data_source_binding.disabled: data_source_binding.disabled = False - data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + data_source_binding.updated_at = naive_utc_now() db.session.add(data_source_binding) db.session.commit() else: @@ -97,7 +97,7 @@ class DataSourceApi(Resource): if action == "disable": if not data_source_binding.disabled: data_source_binding.disabled = True - data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + data_source_binding.updated_at = naive_utc_now() db.session.add(data_source_binding) db.session.commit() else: diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index b2fcf3ce7b..28a2e93049 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -1,6 +1,5 @@ import logging from argparse import ArgumentTypeError -from datetime import UTC, datetime from typing import cast from flask import request @@ -49,6 +48,7 @@ from fields.document_fields import ( document_status_fields, document_with_segments_fields, ) +from libs.datetime_utils import naive_utc_now from libs.login import login_required from models import Dataset, DatasetProcessRule, Document, DocumentSegment, UploadFile from services.dataset_service import DatasetService, DocumentService @@ -750,7 +750,7 @@ class DocumentProcessingApi(DocumentResource): raise InvalidActionError("Document not in indexing state.") document.paused_by = current_user.id - document.paused_at = datetime.now(UTC).replace(tzinfo=None) + document.paused_at = naive_utc_now() document.is_paused = True db.session.commit() @@ -830,7 +830,7 @@ class DocumentMetadataApi(DocumentResource): document.doc_metadata[key] = value document.doc_type = doc_type - document.updated_at = datetime.now(UTC).replace(tzinfo=None) + document.updated_at = naive_utc_now() db.session.commit() return {"result": "success", "message": "Document metadata updated."}, 200 diff --git a/api/controllers/console/explore/completion.py b/api/controllers/console/explore/completion.py index 4367da1162..4842fefc57 100644 --- a/api/controllers/console/explore/completion.py +++ b/api/controllers/console/explore/completion.py @@ -1,5 +1,4 @@ import logging -from datetime import UTC, datetime from flask_login import current_user from flask_restful import reqparse @@ -27,6 +26,7 @@ from core.errors.error import ( from core.model_runtime.errors.invoke import InvokeError from extensions.ext_database import db from libs import helper +from libs.datetime_utils import naive_utc_now from libs.helper import uuid_value from models.model import AppMode from services.app_generate_service import AppGenerateService @@ -51,7 +51,7 @@ class CompletionApi(InstalledAppResource): streaming = args["response_mode"] == "streaming" args["auto_generate_name"] = False - installed_app.last_used_at = datetime.now(UTC).replace(tzinfo=None) + installed_app.last_used_at = naive_utc_now() db.session.commit() try: @@ -111,7 +111,7 @@ class ChatApi(InstalledAppResource): args["auto_generate_name"] = False - installed_app.last_used_at = datetime.now(UTC).replace(tzinfo=None) + installed_app.last_used_at = naive_utc_now() db.session.commit() try: diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py index 9d0c08564e..29111fb865 100644 --- a/api/controllers/console/explore/installed_app.py +++ b/api/controllers/console/explore/installed_app.py @@ -1,5 +1,4 @@ import logging -from datetime import UTC, datetime from typing import Any from flask import request @@ -13,6 +12,7 @@ from controllers.console.explore.wraps import InstalledAppResource from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check from extensions.ext_database import db from fields.installed_app_fields import installed_app_list_fields +from libs.datetime_utils import naive_utc_now from libs.login import login_required from models import App, InstalledApp, RecommendedApp from services.account_service import TenantService @@ -122,7 +122,7 @@ class InstalledAppsListApi(Resource): tenant_id=current_tenant_id, app_owner_tenant_id=app.tenant_id, is_pinned=False, - last_used_at=datetime.now(UTC).replace(tzinfo=None), + last_used_at=naive_utc_now(), ) db.session.add(new_installed_app) db.session.commit() diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 1f22e3fd01..7f7e64a59c 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -1,5 +1,3 @@ -import datetime - import pytz from flask import request from flask_login import current_user @@ -35,6 +33,7 @@ from controllers.console.wraps import ( ) from extensions.ext_database import db from fields.member_fields import account_fields +from libs.datetime_utils import naive_utc_now from libs.helper import TimestampField, email, extract_remote_ip, timezone from libs.login import login_required from models import AccountIntegrate, InvitationCode @@ -80,7 +79,7 @@ class AccountInitApi(Resource): raise InvalidInvitationCodeError() invitation_code.status = "used" - invitation_code.used_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + invitation_code.used_at = naive_utc_now() invitation_code.used_by_tenant_id = account.current_tenant_id invitation_code.used_by_account_id = account.id @@ -88,7 +87,7 @@ class AccountInitApi(Resource): account.timezone = args["timezone"] account.interface_theme = "light" account.status = "active" - account.initialized_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + account.initialized_at = naive_utc_now() db.session.commit() return {"result": "success"} 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 e41375e52b..c4d1ef70d8 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -29,7 +29,7 @@ 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 +from services.tools.mcp_tools_manage_service import MCPToolManageService from services.tools.tool_labels_service import ToolLabelsService from services.tools.tools_manage_service import ToolCommonService from services.tools.tools_transform_service import ToolTransformService @@ -739,7 +739,7 @@ class ToolOAuthCallback(Resource): 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( + credentials_response = oauth_handler.get_credentials( tenant_id=tenant_id, user_id=user_id, plugin_id=plugin_id, @@ -747,7 +747,10 @@ class ToolOAuthCallback(Resource): redirect_uri=redirect_uri, system_credentials=oauth_client_params, request=request, - ).credentials + ) + + credentials = credentials_response.credentials + expires_at = credentials_response.expires_at if not credentials: raise Exception("the plugin credentials failed") @@ -758,6 +761,7 @@ class ToolOAuthCallback(Resource): tenant_id=tenant_id, provider=provider, credentials=dict(credentials), + expires_at=expires_at, api_type=CredentialType.OAUTH2, ) return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback") diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py index 1d9890199d..7762672494 100644 --- a/api/controllers/service_api/app/completion.py +++ b/api/controllers/service_api/app/completion.py @@ -1,5 +1,6 @@ import logging +from flask import request from flask_restful import Resource, reqparse from werkzeug.exceptions import InternalServerError, NotFound @@ -23,6 +24,7 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) +from core.helper.trace_id_helper import get_external_trace_id from core.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import uuid_value @@ -111,6 +113,10 @@ class ChatApi(Resource): args = parser.parse_args() + external_trace_id = get_external_trace_id(request) + if external_trace_id: + args["external_trace_id"] = external_trace_id + streaming = args["response_mode"] == "streaming" try: diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index ac2ebf2b09..370ff911b4 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -1,6 +1,7 @@ import logging from dateutil.parser import isoparse +from flask import request from flask_restful import Resource, fields, marshal_with, reqparse from flask_restful.inputs import int_range from sqlalchemy.orm import Session, sessionmaker @@ -23,6 +24,7 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) +from core.helper.trace_id_helper import get_external_trace_id from core.model_runtime.errors.invoke import InvokeError from core.workflow.entities.workflow_execution import WorkflowExecutionStatus from extensions.ext_database import db @@ -90,7 +92,9 @@ class WorkflowRunApi(Resource): parser.add_argument("files", type=list, required=False, location="json") parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json") args = parser.parse_args() - + external_trace_id = get_external_trace_id(request) + if external_trace_id: + args["external_trace_id"] = external_trace_id streaming = args.get("response_mode") == "streaming" try: diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py index 5b919a68d4..eeed321430 100644 --- a/api/controllers/service_api/wraps.py +++ b/api/controllers/service_api/wraps.py @@ -1,6 +1,6 @@ import time from collections.abc import Callable -from datetime import UTC, datetime, timedelta +from datetime import timedelta from enum import Enum from functools import wraps from typing import Optional @@ -15,6 +15,7 @@ from werkzeug.exceptions import Forbidden, NotFound, Unauthorized from extensions.ext_database import db from extensions.ext_redis import redis_client +from libs.datetime_utils import naive_utc_now from libs.login import _get_user from models.account import Account, Tenant, TenantAccountJoin, TenantStatus from models.dataset import Dataset, RateLimitLog @@ -256,7 +257,7 @@ def validate_and_get_api_token(scope: str | None = None): if auth_scheme != "bearer": raise Unauthorized("Authorization scheme must be 'Bearer'") - current_time = datetime.now(UTC).replace(tzinfo=None) + current_time = naive_utc_now() cutoff_time = current_time - timedelta(minutes=1) with Session(db.engine, expire_on_commit=False) as session: update_stmt = ( diff --git a/api/core/app/apps/README.md b/api/core/app/apps/README.md deleted file mode 100644 index 7a57bb3658..0000000000 --- a/api/core/app/apps/README.md +++ /dev/null @@ -1,48 +0,0 @@ -## Guidelines for Database Connection Management in App Runner and Task Pipeline - -Due to the presence of tasks in App Runner that require long execution times, such as LLM generation and external requests, Flask-Sqlalchemy's strategy for database connection pooling is to allocate one connection (transaction) per request. This approach keeps a connection occupied even during non-DB tasks, leading to the inability to acquire new connections during high concurrency requests due to multiple long-running tasks. - -Therefore, the database operations in App Runner and Task Pipeline must ensure connections are closed immediately after use, and it's better to pass IDs rather than Model objects to avoid detach errors. - -Examples: - -1. Creating a new record: - - ```python - app = App(id=1) - db.session.add(app) - db.session.commit() - db.session.refresh(app) # Retrieve table default values, like created_at, cached in the app object, won't affect after close - - # Handle non-long-running tasks or store the content of the App instance in memory (via variable assignment). - - db.session.close() - - return app.id - ``` - -2. Fetching a record from the table: - - ```python - app = db.session.query(App).filter(App.id == app_id).first() - - created_at = app.created_at - - db.session.close() - - # Handle tasks (include long-running). - - ``` - -3. Updating a table field: - - ```python - app = db.session.query(App).filter(App.id == app_id).first() - - app.updated_at = time.utcnow() - db.session.commit() - db.session.close() - - return app_id - ``` - diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index bd5ad9c51b..610a5bb278 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -7,7 +7,8 @@ from typing import Any, Literal, Optional, Union, overload from flask import Flask, current_app from pydantic import ValidationError -from sqlalchemy.orm import sessionmaker +from sqlalchemy import select +from sqlalchemy.orm import Session, sessionmaker import contexts from configs import dify_config @@ -23,6 +24,7 @@ from core.app.apps.message_based_app_generator import MessageBasedAppGenerator from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueManager from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotAppStreamResponse +from core.helper.trace_id_helper import extract_external_trace_id_from_args from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager from core.prompt.utils.get_thread_messages_length import get_thread_messages_length @@ -112,7 +114,10 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): query = query.replace("\x00", "") inputs = args["inputs"] - extras = {"auto_generate_conversation_name": args.get("auto_generate_name", False)} + extras = { + "auto_generate_conversation_name": args.get("auto_generate_name", False), + **extract_external_trace_id_from_args(args), + } # get conversation conversation = None @@ -482,21 +487,52 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): """ with preserve_flask_contexts(flask_app, context_vars=context): - try: - # get conversation and message - conversation = self._get_conversation(conversation_id) - message = self._get_message(message_id) - - # chatbot app - runner = AdvancedChatAppRunner( - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - conversation=conversation, - message=message, - dialogue_count=self._dialogue_count, - variable_loader=variable_loader, + # get conversation and message + conversation = self._get_conversation(conversation_id) + message = self._get_message(message_id) + + with Session(db.engine, expire_on_commit=False) as session: + workflow = session.scalar( + select(Workflow).where( + Workflow.tenant_id == application_generate_entity.app_config.tenant_id, + Workflow.app_id == application_generate_entity.app_config.app_id, + Workflow.id == application_generate_entity.app_config.workflow_id, + ) ) + if workflow is None: + raise ValueError("Workflow not found") + + # Determine system_user_id based on invocation source + is_external_api_call = application_generate_entity.invoke_from in { + InvokeFrom.WEB_APP, + InvokeFrom.SERVICE_API, + } + + if is_external_api_call: + # For external API calls, use end user's session ID + end_user = session.scalar(select(EndUser).where(EndUser.id == application_generate_entity.user_id)) + system_user_id = end_user.session_id if end_user else "" + else: + # For internal calls, use the original user ID + system_user_id = application_generate_entity.user_id + + app = session.scalar(select(App).where(App.id == application_generate_entity.app_config.app_id)) + if app is None: + raise ValueError("App not found") + + runner = AdvancedChatAppRunner( + application_generate_entity=application_generate_entity, + queue_manager=queue_manager, + conversation=conversation, + message=message, + dialogue_count=self._dialogue_count, + variable_loader=variable_loader, + workflow=workflow, + system_user_id=system_user_id, + app=app, + ) + try: runner.run() except GenerateTaskStoppedError: pass diff --git a/api/core/app/apps/advanced_chat/app_runner.py b/api/core/app/apps/advanced_chat/app_runner.py index af15324f46..80af9a3c60 100644 --- a/api/core/app/apps/advanced_chat/app_runner.py +++ b/api/core/app/apps/advanced_chat/app_runner.py @@ -1,6 +1,6 @@ import logging from collections.abc import Mapping -from typing import Any, cast +from typing import Any, Optional, cast from sqlalchemy import select from sqlalchemy.orm import Session @@ -9,13 +9,19 @@ from configs import dify_config from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner -from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom +from core.app.entities.app_invoke_entities import ( + AdvancedChatAppGenerateEntity, + AppGenerateEntity, + InvokeFrom, +) from core.app.entities.queue_entities import ( QueueAnnotationReplyEvent, QueueStopEvent, QueueTextChunkEvent, ) +from core.app.features.annotation_reply.annotation_reply import AnnotationReplyFeature from core.moderation.base import ModerationError +from core.moderation.input_moderation import InputModeration from core.variables.variables import VariableUnion from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback from core.workflow.entities.variable_pool import VariablePool @@ -23,8 +29,9 @@ from core.workflow.system_variable import SystemVariable from core.workflow.variable_loader import VariableLoader from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db +from models import Workflow from models.enums import UserFrom -from models.model import App, Conversation, EndUser, Message +from models.model import App, Conversation, Message, MessageAnnotation from models.workflow import ConversationVariable, WorkflowType logger = logging.getLogger(__name__) @@ -37,21 +44,29 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): def __init__( self, + *, application_generate_entity: AdvancedChatAppGenerateEntity, queue_manager: AppQueueManager, conversation: Conversation, message: Message, dialogue_count: int, variable_loader: VariableLoader, + workflow: Workflow, + system_user_id: str, + app: App, ) -> None: - super().__init__(queue_manager, variable_loader) + super().__init__( + queue_manager=queue_manager, + variable_loader=variable_loader, + app_id=application_generate_entity.app_config.app_id, + ) self.application_generate_entity = application_generate_entity self.conversation = conversation self.message = message self._dialogue_count = dialogue_count - - def _get_app_id(self) -> str: - return self.application_generate_entity.app_config.app_id + self._workflow = workflow + self.system_user_id = system_user_id + self._app = app def run(self) -> None: app_config = self.application_generate_entity.app_config @@ -61,18 +76,6 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): if not app_record: raise ValueError("App not found") - workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id) - if not workflow: - raise ValueError("Workflow not initialized") - - user_id: str | None = None - if self.application_generate_entity.invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}: - end_user = db.session.query(EndUser).filter(EndUser.id == self.application_generate_entity.user_id).first() - if end_user: - user_id = end_user.session_id - else: - user_id = self.application_generate_entity.user_id - workflow_callbacks: list[WorkflowCallback] = [] if dify_config.DEBUG: workflow_callbacks.append(WorkflowLoggingCallback()) @@ -80,14 +83,14 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): if self.application_generate_entity.single_iteration_run: # if only single iteration run is requested graph, variable_pool = self._get_graph_and_variable_pool_of_single_iteration( - workflow=workflow, + workflow=self._workflow, node_id=self.application_generate_entity.single_iteration_run.node_id, user_inputs=dict(self.application_generate_entity.single_iteration_run.inputs), ) elif self.application_generate_entity.single_loop_run: # if only single loop run is requested graph, variable_pool = self._get_graph_and_variable_pool_of_single_loop( - workflow=workflow, + workflow=self._workflow, node_id=self.application_generate_entity.single_loop_run.node_id, user_inputs=dict(self.application_generate_entity.single_loop_run.inputs), ) @@ -98,7 +101,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): # moderation if self.handle_input_moderation( - app_record=app_record, + app_record=self._app, app_generate_entity=self.application_generate_entity, inputs=inputs, query=query, @@ -108,7 +111,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): # annotation reply if self.handle_annotation_reply( - app_record=app_record, + app_record=self._app, message=self.message, query=query, app_generate_entity=self.application_generate_entity, @@ -128,7 +131,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): ConversationVariable.from_variable( app_id=self.conversation.app_id, conversation_id=self.conversation.id, variable=variable ) - for variable in workflow.conversation_variables + for variable in self._workflow.conversation_variables ] session.add_all(db_conversation_variables) # Convert database entities to variables. @@ -141,7 +144,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): query=query, files=files, conversation_id=self.conversation.id, - user_id=user_id, + user_id=self.system_user_id, dialogue_count=self._dialogue_count, app_id=app_config.app_id, workflow_id=app_config.workflow_id, @@ -152,25 +155,25 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): variable_pool = VariablePool( system_variables=system_inputs, user_inputs=inputs, - environment_variables=workflow.environment_variables, + environment_variables=self._workflow.environment_variables, # Based on the definition of `VariableUnion`, # `list[Variable]` can be safely used as `list[VariableUnion]` since they are compatible. conversation_variables=cast(list[VariableUnion], conversation_variables), ) # init graph - graph = self._init_graph(graph_config=workflow.graph_dict) + graph = self._init_graph(graph_config=self._workflow.graph_dict) db.session.close() # RUN WORKFLOW workflow_entry = WorkflowEntry( - tenant_id=workflow.tenant_id, - app_id=workflow.app_id, - workflow_id=workflow.id, - workflow_type=WorkflowType.value_of(workflow.type), + tenant_id=self._workflow.tenant_id, + app_id=self._workflow.app_id, + workflow_id=self._workflow.id, + workflow_type=WorkflowType.value_of(self._workflow.type), graph=graph, - graph_config=workflow.graph_dict, + graph_config=self._workflow.graph_dict, user_id=self.application_generate_entity.user_id, user_from=( UserFrom.ACCOUNT @@ -241,3 +244,51 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): self._publish_event(QueueTextChunkEvent(text=text)) self._publish_event(QueueStopEvent(stopped_by=stopped_by)) + + def query_app_annotations_to_reply( + self, app_record: App, message: Message, query: str, user_id: str, invoke_from: InvokeFrom + ) -> Optional[MessageAnnotation]: + """ + Query app annotations to reply + :param app_record: app record + :param message: message + :param query: query + :param user_id: user id + :param invoke_from: invoke from + :return: + """ + annotation_reply_feature = AnnotationReplyFeature() + return annotation_reply_feature.query( + app_record=app_record, message=message, query=query, user_id=user_id, invoke_from=invoke_from + ) + + def moderation_for_inputs( + self, + *, + app_id: str, + tenant_id: str, + app_generate_entity: AppGenerateEntity, + inputs: Mapping[str, Any], + query: str | None = None, + message_id: str, + ) -> tuple[bool, Mapping[str, Any], str]: + """ + Process sensitive_word_avoidance. + :param app_id: app id + :param tenant_id: tenant id + :param app_generate_entity: app generate entity + :param inputs: inputs + :param query: query + :param message_id: message id + :return: + """ + moderation_feature = InputModeration() + return moderation_feature.check( + app_id=app_id, + tenant_id=tenant_id, + app_config=app_generate_entity.app_config, + inputs=dict(inputs), + query=query or "", + message_id=message_id, + trace_manager=app_generate_entity.trace_manager, + ) diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py index 337b779b50..dc27076a4d 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -559,6 +559,7 @@ class AdvancedChatAppGenerateTaskPipeline: outputs=event.outputs, conversation_id=self._conversation_id, trace_manager=trace_manager, + external_trace_id=self._application_generate_entity.extras.get("external_trace_id"), ) workflow_finish_resp = self._workflow_response_converter.workflow_finish_to_stream_response( session=session, @@ -590,6 +591,7 @@ class AdvancedChatAppGenerateTaskPipeline: exceptions_count=event.exceptions_count, conversation_id=None, trace_manager=trace_manager, + external_trace_id=self._application_generate_entity.extras.get("external_trace_id"), ) workflow_finish_resp = self._workflow_response_converter.workflow_finish_to_stream_response( session=session, @@ -622,6 +624,7 @@ class AdvancedChatAppGenerateTaskPipeline: conversation_id=self._conversation_id, trace_manager=trace_manager, exceptions_count=event.exceptions_count, + external_trace_id=self._application_generate_entity.extras.get("external_trace_id"), ) workflow_finish_resp = self._workflow_response_converter.workflow_finish_to_stream_response( session=session, @@ -653,6 +656,7 @@ class AdvancedChatAppGenerateTaskPipeline: error_message=event.get_stop_reason(), conversation_id=self._conversation_id, trace_manager=trace_manager, + external_trace_id=self._application_generate_entity.extras.get("external_trace_id"), ) workflow_finish_resp = self._workflow_response_converter.workflow_finish_to_stream_response( session=session, diff --git a/api/core/app/apps/message_based_app_generator.py b/api/core/app/apps/message_based_app_generator.py index 85fafe6980..d50cf1c941 100644 --- a/api/core/app/apps/message_based_app_generator.py +++ b/api/core/app/apps/message_based_app_generator.py @@ -1,7 +1,6 @@ import json import logging from collections.abc import Generator -from datetime import UTC, datetime from typing import Optional, Union, cast from core.app.app_config.entities import EasyUIBasedAppConfig, EasyUIBasedAppModelConfigFrom @@ -25,6 +24,7 @@ from core.app.entities.task_entities import ( from core.app.task_pipeline.easy_ui_based_generate_task_pipeline import EasyUIBasedGenerateTaskPipeline from core.prompt.utils.prompt_template_parser import PromptTemplateParser from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from models import Account from models.enums import CreatorUserRole from models.model import App, AppMode, AppModelConfig, Conversation, EndUser, Message, MessageFile @@ -184,7 +184,7 @@ class MessageBasedAppGenerator(BaseAppGenerator): db.session.commit() db.session.refresh(conversation) else: - conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) + conversation.updated_at = naive_utc_now() db.session.commit() message = Message( diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index 6f560b3253..4c36f63c71 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -7,7 +7,8 @@ from typing import Any, Literal, Optional, Union, overload from flask import Flask, current_app from pydantic import ValidationError -from sqlalchemy.orm import sessionmaker +from sqlalchemy import select +from sqlalchemy.orm import Session, sessionmaker import contexts from configs import dify_config @@ -22,6 +23,7 @@ from core.app.apps.workflow.generate_response_converter import WorkflowAppGenera from core.app.apps.workflow.generate_task_pipeline import WorkflowAppGenerateTaskPipeline from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse +from core.helper.trace_id_helper import extract_external_trace_id_from_args from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager from core.repositories import DifyCoreRepositoryFactory @@ -123,6 +125,10 @@ class WorkflowAppGenerator(BaseAppGenerator): ) inputs: Mapping[str, Any] = args["inputs"] + + extras = { + **extract_external_trace_id_from_args(args), + } workflow_run_id = str(uuid.uuid4()) # init application generate entity application_generate_entity = WorkflowAppGenerateEntity( @@ -142,6 +148,7 @@ class WorkflowAppGenerator(BaseAppGenerator): call_depth=call_depth, trace_manager=trace_manager, workflow_execution_id=workflow_run_id, + extras=extras, ) contexts.plugin_tool_providers.set({}) @@ -439,17 +446,44 @@ class WorkflowAppGenerator(BaseAppGenerator): """ with preserve_flask_contexts(flask_app, context_vars=context): - try: - # workflow app - runner = WorkflowAppRunner( - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - workflow_thread_pool_id=workflow_thread_pool_id, - variable_loader=variable_loader, + with Session(db.engine, expire_on_commit=False) as session: + workflow = session.scalar( + select(Workflow).where( + Workflow.tenant_id == application_generate_entity.app_config.tenant_id, + Workflow.app_id == application_generate_entity.app_config.app_id, + Workflow.id == application_generate_entity.app_config.workflow_id, + ) ) + if workflow is None: + raise ValueError("Workflow not found") + + # Determine system_user_id based on invocation source + is_external_api_call = application_generate_entity.invoke_from in { + InvokeFrom.WEB_APP, + InvokeFrom.SERVICE_API, + } + + if is_external_api_call: + # For external API calls, use end user's session ID + end_user = session.scalar(select(EndUser).where(EndUser.id == application_generate_entity.user_id)) + system_user_id = end_user.session_id if end_user else "" + else: + # For internal calls, use the original user ID + system_user_id = application_generate_entity.user_id + + runner = WorkflowAppRunner( + application_generate_entity=application_generate_entity, + queue_manager=queue_manager, + workflow_thread_pool_id=workflow_thread_pool_id, + variable_loader=variable_loader, + workflow=workflow, + system_user_id=system_user_id, + ) + try: runner.run() - except GenerateTaskStoppedError: + except GenerateTaskStoppedError as e: + logger.warning(f"Task stopped: {str(e)}") pass except InvokeAuthorizationError: queue_manager.publish_error( @@ -465,8 +499,6 @@ class WorkflowAppGenerator(BaseAppGenerator): except Exception as e: logger.exception("Unknown Error when generating") queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) - finally: - db.session.close() def _handle_response( self, diff --git a/api/core/app/apps/workflow/app_runner.py b/api/core/app/apps/workflow/app_runner.py index 3a66ffa578..4f4c1460ae 100644 --- a/api/core/app/apps/workflow/app_runner.py +++ b/api/core/app/apps/workflow/app_runner.py @@ -14,10 +14,8 @@ from core.workflow.entities.variable_pool import VariablePool from core.workflow.system_variable import SystemVariable from core.workflow.variable_loader import VariableLoader from core.workflow.workflow_entry import WorkflowEntry -from extensions.ext_database import db from models.enums import UserFrom -from models.model import App, EndUser -from models.workflow import WorkflowType +from models.workflow import Workflow, WorkflowType logger = logging.getLogger(__name__) @@ -29,22 +27,23 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): def __init__( self, + *, application_generate_entity: WorkflowAppGenerateEntity, queue_manager: AppQueueManager, variable_loader: VariableLoader, workflow_thread_pool_id: Optional[str] = None, + workflow: Workflow, + system_user_id: str, ) -> None: - """ - :param application_generate_entity: application generate entity - :param queue_manager: application queue manager - :param workflow_thread_pool_id: workflow thread pool id - """ - super().__init__(queue_manager, variable_loader) + super().__init__( + queue_manager=queue_manager, + variable_loader=variable_loader, + app_id=application_generate_entity.app_config.app_id, + ) self.application_generate_entity = application_generate_entity self.workflow_thread_pool_id = workflow_thread_pool_id - - def _get_app_id(self) -> str: - return self.application_generate_entity.app_config.app_id + self._workflow = workflow + self._sys_user_id = system_user_id def run(self) -> None: """ @@ -53,24 +52,6 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): app_config = self.application_generate_entity.app_config app_config = cast(WorkflowAppConfig, app_config) - user_id = None - if self.application_generate_entity.invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}: - end_user = db.session.query(EndUser).filter(EndUser.id == self.application_generate_entity.user_id).first() - if end_user: - user_id = end_user.session_id - else: - user_id = self.application_generate_entity.user_id - - app_record = db.session.query(App).filter(App.id == app_config.app_id).first() - if not app_record: - raise ValueError("App not found") - - workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id) - if not workflow: - raise ValueError("Workflow not initialized") - - db.session.close() - workflow_callbacks: list[WorkflowCallback] = [] if dify_config.DEBUG: workflow_callbacks.append(WorkflowLoggingCallback()) @@ -79,14 +60,14 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): if self.application_generate_entity.single_iteration_run: # if only single iteration run is requested graph, variable_pool = self._get_graph_and_variable_pool_of_single_iteration( - workflow=workflow, + workflow=self._workflow, node_id=self.application_generate_entity.single_iteration_run.node_id, user_inputs=self.application_generate_entity.single_iteration_run.inputs, ) elif self.application_generate_entity.single_loop_run: # if only single loop run is requested graph, variable_pool = self._get_graph_and_variable_pool_of_single_loop( - workflow=workflow, + workflow=self._workflow, node_id=self.application_generate_entity.single_loop_run.node_id, user_inputs=self.application_generate_entity.single_loop_run.inputs, ) @@ -98,7 +79,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): system_inputs = SystemVariable( files=files, - user_id=user_id, + user_id=self._sys_user_id, app_id=app_config.app_id, workflow_id=app_config.workflow_id, workflow_execution_id=self.application_generate_entity.workflow_execution_id, @@ -107,21 +88,21 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): variable_pool = VariablePool( system_variables=system_inputs, user_inputs=inputs, - environment_variables=workflow.environment_variables, + environment_variables=self._workflow.environment_variables, conversation_variables=[], ) # init graph - graph = self._init_graph(graph_config=workflow.graph_dict) + graph = self._init_graph(graph_config=self._workflow.graph_dict) # RUN WORKFLOW workflow_entry = WorkflowEntry( - tenant_id=workflow.tenant_id, - app_id=workflow.app_id, - workflow_id=workflow.id, - workflow_type=WorkflowType.value_of(workflow.type), + tenant_id=self._workflow.tenant_id, + app_id=self._workflow.app_id, + workflow_id=self._workflow.id, + workflow_type=WorkflowType.value_of(self._workflow.type), graph=graph, - graph_config=workflow.graph_dict, + graph_config=self._workflow.graph_dict, user_id=self.application_generate_entity.user_id, user_from=( UserFrom.ACCOUNT diff --git a/api/core/app/apps/workflow/generate_task_pipeline.py b/api/core/app/apps/workflow/generate_task_pipeline.py index 9a39b2e01e..e31a316c56 100644 --- a/api/core/app/apps/workflow/generate_task_pipeline.py +++ b/api/core/app/apps/workflow/generate_task_pipeline.py @@ -490,6 +490,7 @@ class WorkflowAppGenerateTaskPipeline: outputs=event.outputs, conversation_id=None, trace_manager=trace_manager, + external_trace_id=self._application_generate_entity.extras.get("external_trace_id"), ) # save workflow app log @@ -524,6 +525,7 @@ class WorkflowAppGenerateTaskPipeline: exceptions_count=event.exceptions_count, conversation_id=None, trace_manager=trace_manager, + external_trace_id=self._application_generate_entity.extras.get("external_trace_id"), ) # save workflow app log @@ -561,6 +563,7 @@ class WorkflowAppGenerateTaskPipeline: conversation_id=None, trace_manager=trace_manager, exceptions_count=event.exceptions_count if isinstance(event, QueueWorkflowFailedEvent) else 0, + external_trace_id=self._application_generate_entity.extras.get("external_trace_id"), ) # save workflow app log diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index 2f4d234ecd..948ea95e63 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -1,8 +1,7 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom -from core.app.apps.base_app_runner import AppRunner from core.app.entities.queue_entities import ( AppQueueEvent, QueueAgentLogEvent, @@ -65,18 +64,20 @@ from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING from core.workflow.system_variable import SystemVariable from core.workflow.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool from core.workflow.workflow_entry import WorkflowEntry -from extensions.ext_database import db -from models.model import App from models.workflow import Workflow -class WorkflowBasedAppRunner(AppRunner): - def __init__(self, queue_manager: AppQueueManager, variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER) -> None: - self.queue_manager = queue_manager +class WorkflowBasedAppRunner: + def __init__( + self, + *, + queue_manager: AppQueueManager, + variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER, + app_id: str, + ) -> None: + self._queue_manager = queue_manager self._variable_loader = variable_loader - - def _get_app_id(self) -> str: - raise NotImplementedError("not implemented") + self._app_id = app_id def _init_graph(self, graph_config: Mapping[str, Any]) -> Graph: """ @@ -693,21 +694,5 @@ class WorkflowBasedAppRunner(AppRunner): ) ) - def get_workflow(self, app_model: App, workflow_id: str) -> Optional[Workflow]: - """ - Get workflow - """ - # fetch workflow by workflow_id - workflow = ( - db.session.query(Workflow) - .filter( - Workflow.tenant_id == app_model.tenant_id, Workflow.app_id == app_model.id, Workflow.id == workflow_id - ) - .first() - ) - - # return workflow - return workflow - def _publish_event(self, event: AppQueueEvent) -> None: - self.queue_manager.publish(event, PublishFrom.APPLICATION_MANAGER) + self._queue_manager.publish(event, PublishFrom.APPLICATION_MANAGER) diff --git a/api/core/file/file_manager.py b/api/core/file/file_manager.py index ada19ef8ce..f8c050c2ac 100644 --- a/api/core/file/file_manager.py +++ b/api/core/file/file_manager.py @@ -7,6 +7,7 @@ from core.model_runtime.entities import ( AudioPromptMessageContent, DocumentPromptMessageContent, ImagePromptMessageContent, + TextPromptMessageContent, VideoPromptMessageContent, ) from core.model_runtime.entities.message_entities import PromptMessageContentUnionTypes @@ -44,11 +45,44 @@ def to_prompt_message_content( *, image_detail_config: ImagePromptMessageContent.DETAIL | None = None, ) -> PromptMessageContentUnionTypes: + """ + Convert a file to prompt message content. + + This function converts files to their appropriate prompt message content types. + For supported file types (IMAGE, AUDIO, VIDEO, DOCUMENT), it creates the + corresponding message content with proper encoding/URL. + + For unsupported file types, instead of raising an error, it returns a + TextPromptMessageContent with a descriptive message about the file. + + Args: + f: The file to convert + image_detail_config: Optional detail configuration for image files + + Returns: + PromptMessageContentUnionTypes: The appropriate message content type + + Raises: + ValueError: If file extension or mime_type is missing + """ if f.extension is None: raise ValueError("Missing file extension") if f.mime_type is None: raise ValueError("Missing file mime_type") + prompt_class_map: Mapping[FileType, type[PromptMessageContentUnionTypes]] = { + FileType.IMAGE: ImagePromptMessageContent, + FileType.AUDIO: AudioPromptMessageContent, + FileType.VIDEO: VideoPromptMessageContent, + FileType.DOCUMENT: DocumentPromptMessageContent, + } + + # Check if file type is supported + if f.type not in prompt_class_map: + # For unsupported file types, return a text description + return TextPromptMessageContent(data=f"[Unsupported file type: {f.filename} ({f.type.value})]") + + # Process supported file types params = { "base64_data": _get_encoded_string(f) if dify_config.MULTIMODAL_SEND_FORMAT == "base64" else "", "url": _to_url(f) if dify_config.MULTIMODAL_SEND_FORMAT == "url" else "", @@ -58,17 +92,7 @@ def to_prompt_message_content( if f.type == FileType.IMAGE: params["detail"] = image_detail_config or ImagePromptMessageContent.DETAIL.LOW - prompt_class_map: Mapping[FileType, type[PromptMessageContentUnionTypes]] = { - FileType.IMAGE: ImagePromptMessageContent, - FileType.AUDIO: AudioPromptMessageContent, - FileType.VIDEO: VideoPromptMessageContent, - FileType.DOCUMENT: DocumentPromptMessageContent, - } - - try: - return prompt_class_map[f.type].model_validate(params) - except KeyError: - raise ValueError(f"file type {f.type} is not supported") + return prompt_class_map[f.type].model_validate(params) def download(f: File, /): 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/trace_id_helper.py b/api/core/helper/trace_id_helper.py new file mode 100644 index 0000000000..e90c3194f2 --- /dev/null +++ b/api/core/helper/trace_id_helper.py @@ -0,0 +1,42 @@ +import re +from collections.abc import Mapping +from typing import Any, Optional + + +def is_valid_trace_id(trace_id: str) -> bool: + """ + Check if the trace_id is valid. + + Requirements: 1-128 characters, only letters, numbers, '-', and '_'. + """ + return bool(re.match(r"^[a-zA-Z0-9\-_]{1,128}$", trace_id)) + + +def get_external_trace_id(request: Any) -> Optional[str]: + """ + Retrieve the trace_id from the request. + + Priority: header ('X-Trace-Id'), then parameters, then JSON body. Returns None if not provided or invalid. + """ + trace_id = request.headers.get("X-Trace-Id") + if not trace_id: + trace_id = request.args.get("trace_id") + if not trace_id and getattr(request, "is_json", False): + json_data = getattr(request, "json", None) + if json_data: + trace_id = json_data.get("trace_id") + if isinstance(trace_id, str) and is_valid_trace_id(trace_id): + return trace_id + return None + + +def extract_external_trace_id_from_args(args: Mapping[str, Any]) -> dict: + """ + Extract 'external_trace_id' from args. + + Returns a dict suitable for use in extras. Returns an empty dict if not found. + """ + trace_id = args.get("external_trace_id") + if trace_id: + return {"external_trace_id": trace_id} + return {} diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index 305a9190d5..e5976f4c9a 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -672,8 +672,7 @@ class IndexingRunner: if extra_update_params: update_params.update(extra_update_params) - - db.session.query(DatasetDocument).filter_by(id=document_id).update(update_params) + db.session.query(DatasetDocument).filter_by(id=document_id).update(update_params) # type: ignore db.session.commit() @staticmethod diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index f7fd93be4a..331ac933c8 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -114,7 +114,8 @@ class LLMGenerator: ), ) - questions = output_parser.parse(cast(str, response.message.content)) + text_content = response.message.get_text_content() + questions = output_parser.parse(text_content) if text_content else [] except InvokeError: questions = [] except Exception: diff --git a/api/core/llm_generator/output_parser/suggested_questions_after_answer.py b/api/core/llm_generator/output_parser/suggested_questions_after_answer.py index c451bf514c..98cdc4c8b7 100644 --- a/api/core/llm_generator/output_parser/suggested_questions_after_answer.py +++ b/api/core/llm_generator/output_parser/suggested_questions_after_answer.py @@ -15,5 +15,4 @@ class SuggestedQuestionsAfterAnswerOutputParser: json_obj = json.loads(action_match.group(0).strip()) else: json_obj = [] - return json_obj diff --git a/api/core/mcp/auth/auth_provider.py b/api/core/mcp/auth/auth_provider.py index cd55dbf64f..00d5a25956 100644 --- a/api/core/mcp/auth/auth_provider.py +++ b/api/core/mcp/auth/auth_provider.py @@ -8,7 +8,7 @@ from core.mcp.types import ( OAuthTokens, ) from models.tools import MCPToolProvider -from services.tools.mcp_tools_mange_service import MCPToolManageService +from services.tools.mcp_tools_manage_service import MCPToolManageService LATEST_PROTOCOL_VERSION = "1.0" diff --git a/api/core/mcp/mcp_client.py b/api/core/mcp/mcp_client.py index e9036de8c6..5fe52c008a 100644 --- a/api/core/mcp/mcp_client.py +++ b/api/core/mcp/mcp_client.py @@ -68,15 +68,17 @@ class MCPClient: } parsed_url = urlparse(self.server_url) - path = parsed_url.path + path = parsed_url.path or "" method_name = path.rstrip("/").split("/")[-1] if path else "" - try: + if method_name in connection_methods: client_factory = connection_methods[method_name] self.connect_server(client_factory, method_name) - except KeyError: + else: try: + logger.debug(f"Not supported method {method_name} found in URL path, trying default 'mcp' method.") self.connect_server(sse_client, "sse") except MCPConnectionError: + logger.debug("MCP connection failed with 'sse', falling back to 'mcp' method.") self.connect_server(streamablehttp_client, "mcp") def connect_server( @@ -91,7 +93,7 @@ class MCPClient: else {} ) self._streams_context = client_factory(url=self.server_url, headers=headers) - if self._streams_context is None: + if not self._streams_context: raise MCPConnectionError("Failed to create connection context") # Use exit_stack to manage context managers properly @@ -141,10 +143,11 @@ class MCPClient: try: # ExitStack will handle proper cleanup of all managed context managers self.exit_stack.close() + except Exception as e: + logging.exception("Error during cleanup") + raise ValueError(f"Error during cleanup: {e}") + finally: self._session = None self._session_context = None self._streams_context = None self._initialized = False - except Exception as e: - logging.exception("Error during cleanup") - raise ValueError(f"Error during cleanup: {e}") diff --git a/api/core/model_runtime/entities/message_entities.py b/api/core/model_runtime/entities/message_entities.py index 9d010ae28d..83dc7f0525 100644 --- a/api/core/model_runtime/entities/message_entities.py +++ b/api/core/model_runtime/entities/message_entities.py @@ -156,6 +156,23 @@ class PromptMessage(ABC, BaseModel): """ return not self.content + def get_text_content(self) -> str: + """ + Get text content from prompt message. + + :return: Text content as string, empty string if no text content + """ + if isinstance(self.content, str): + return self.content + elif isinstance(self.content, list): + text_parts = [] + for item in self.content: + if isinstance(item, TextPromptMessageContent): + text_parts.append(item.data) + return "".join(text_parts) + else: + return "" + @field_validator("content", mode="before") @classmethod def validate_content(cls, v): diff --git a/api/core/ops/aliyun_trace/aliyun_trace.py b/api/core/ops/aliyun_trace/aliyun_trace.py index db8fec4ee9..bbbc12a2c8 100644 --- a/api/core/ops/aliyun_trace/aliyun_trace.py +++ b/api/core/ops/aliyun_trace/aliyun_trace.py @@ -101,7 +101,8 @@ class AliyunDataTrace(BaseTraceInstance): raise ValueError(f"Aliyun get run url failed: {str(e)}") def workflow_trace(self, trace_info: WorkflowTraceInfo): - trace_id = convert_to_trace_id(trace_info.workflow_run_id) + external_trace_id = trace_info.metadata.get("external_trace_id") + trace_id = external_trace_id or convert_to_trace_id(trace_info.workflow_run_id) workflow_span_id = convert_to_span_id(trace_info.workflow_run_id, "workflow") self.add_workflow_span(trace_id, workflow_span_id, trace_info) diff --git a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py index 8b3ce0c448..14dba44237 100644 --- a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py +++ b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py @@ -153,7 +153,8 @@ class ArizePhoenixDataTrace(BaseTraceInstance): } workflow_metadata.update(trace_info.metadata) - trace_id = uuid_to_trace_id(trace_info.workflow_run_id) + external_trace_id = trace_info.metadata.get("external_trace_id") + trace_id = external_trace_id or uuid_to_trace_id(trace_info.workflow_run_id) span_id = RandomIdGenerator().generate_span_id() context = SpanContext( trace_id=trace_id, diff --git a/api/core/ops/langfuse_trace/langfuse_trace.py b/api/core/ops/langfuse_trace/langfuse_trace.py index 4a7e66d27c..6dadb2897e 100644 --- a/api/core/ops/langfuse_trace/langfuse_trace.py +++ b/api/core/ops/langfuse_trace/langfuse_trace.py @@ -67,13 +67,14 @@ class LangFuseDataTrace(BaseTraceInstance): self.generate_name_trace(trace_info) def workflow_trace(self, trace_info: WorkflowTraceInfo): - trace_id = trace_info.workflow_run_id + external_trace_id = trace_info.metadata.get("external_trace_id") + trace_id = external_trace_id or trace_info.workflow_run_id user_id = trace_info.metadata.get("user_id") metadata = trace_info.metadata metadata["workflow_app_log_id"] = trace_info.workflow_app_log_id if trace_info.message_id: - trace_id = trace_info.message_id + trace_id = external_trace_id or trace_info.message_id name = TraceTaskName.MESSAGE_TRACE.value trace_data = LangfuseTrace( id=trace_id, diff --git a/api/core/ops/langsmith_trace/langsmith_trace.py b/api/core/ops/langsmith_trace/langsmith_trace.py index 8a559c4929..3246782278 100644 --- a/api/core/ops/langsmith_trace/langsmith_trace.py +++ b/api/core/ops/langsmith_trace/langsmith_trace.py @@ -65,7 +65,8 @@ class LangSmithDataTrace(BaseTraceInstance): self.generate_name_trace(trace_info) def workflow_trace(self, trace_info: WorkflowTraceInfo): - trace_id = trace_info.message_id or trace_info.workflow_run_id + external_trace_id = trace_info.metadata.get("external_trace_id") + trace_id = external_trace_id or trace_info.message_id or trace_info.workflow_run_id if trace_info.start_time is None: trace_info.start_time = datetime.now() message_dotted_order = ( diff --git a/api/core/ops/opik_trace/opik_trace.py b/api/core/ops/opik_trace/opik_trace.py index be4997a5bf..dfa7052c36 100644 --- a/api/core/ops/opik_trace/opik_trace.py +++ b/api/core/ops/opik_trace/opik_trace.py @@ -96,7 +96,8 @@ class OpikDataTrace(BaseTraceInstance): self.generate_name_trace(trace_info) def workflow_trace(self, trace_info: WorkflowTraceInfo): - dify_trace_id = trace_info.workflow_run_id + external_trace_id = trace_info.metadata.get("external_trace_id") + dify_trace_id = external_trace_id or trace_info.workflow_run_id opik_trace_id = prepare_opik_uuid(trace_info.start_time, dify_trace_id) workflow_metadata = wrap_metadata( trace_info.metadata, message_id=trace_info.message_id, workflow_app_log_id=trace_info.workflow_app_log_id @@ -104,7 +105,7 @@ class OpikDataTrace(BaseTraceInstance): root_span_id = None if trace_info.message_id: - dify_trace_id = trace_info.message_id + dify_trace_id = external_trace_id or trace_info.message_id opik_trace_id = prepare_opik_uuid(trace_info.start_time, dify_trace_id) trace_data = { diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index 5c9b9d27b7..34963efab3 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -520,6 +520,10 @@ class TraceTask: "app_id": workflow_run.app_id, } + external_trace_id = self.kwargs.get("external_trace_id") + if external_trace_id: + metadata["external_trace_id"] = external_trace_id + workflow_trace_info = WorkflowTraceInfo( workflow_data=workflow_run.to_dict(), conversation_id=conversation_id, diff --git a/api/core/ops/weave_trace/weave_trace.py b/api/core/ops/weave_trace/weave_trace.py index 445c6a8741..4bd41ce4a6 100644 --- a/api/core/ops/weave_trace/weave_trace.py +++ b/api/core/ops/weave_trace/weave_trace.py @@ -87,7 +87,8 @@ class WeaveDataTrace(BaseTraceInstance): self.generate_name_trace(trace_info) def workflow_trace(self, trace_info: WorkflowTraceInfo): - trace_id = trace_info.message_id or trace_info.workflow_run_id + external_trace_id = trace_info.metadata.get("external_trace_id") + trace_id = external_trace_id or trace_info.message_id or trace_info.workflow_run_id if trace_info.start_time is None: trace_info.start_time = datetime.now() diff --git a/api/core/plugin/entities/marketplace.py b/api/core/plugin/entities/marketplace.py index a19a44aa3c..1c13a621d4 100644 --- a/api/core/plugin/entities/marketplace.py +++ b/api/core/plugin/entities/marketplace.py @@ -32,6 +32,13 @@ class MarketplacePluginDeclaration(BaseModel): latest_package_identifier: str = Field( ..., description="Unique identifier for the latest package release of the plugin" ) + status: str = Field(..., description="Indicate the status of marketplace plugin, enum from `active` `deleted`") + deprecated_reason: str = Field( + ..., description="Not empty when status='deleted', indicates the reason why this plugin is deleted(deprecated)" + ) + alternative_plugin_id: str = Field( + ..., description="Optional, indicates the alternative plugin for user to switch to" + ) @model_validator(mode="before") @classmethod diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index 00253b8a11..16ab661092 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -182,6 +182,10 @@ class PluginOAuthAuthorizationUrlResponse(BaseModel): class PluginOAuthCredentialsResponse(BaseModel): + metadata: Mapping[str, Any] = Field( + default_factory=dict, description="The metadata of the OAuth, like avatar url, name, etc." + ) + expires_at: int = Field(default=-1, description="The expires at time of the credentials. UTC timestamp.") credentials: Mapping[str, Any] = Field(description="The credentials of the OAuth.") diff --git a/api/core/plugin/impl/oauth.py b/api/core/plugin/impl/oauth.py index d73e5d9f9e..7f022992ff 100644 --- a/api/core/plugin/impl/oauth.py +++ b/api/core/plugin/impl/oauth.py @@ -84,6 +84,41 @@ class OAuthHandler(BasePluginClient): except Exception as e: raise ValueError(f"Error getting credentials: {e}") + def refresh_credentials( + self, + tenant_id: str, + user_id: str, + plugin_id: str, + provider: str, + redirect_uri: str, + system_credentials: Mapping[str, Any], + credentials: Mapping[str, Any], + ) -> PluginOAuthCredentialsResponse: + try: + response = self._request_with_plugin_daemon_response_stream( + "POST", + f"plugin/{tenant_id}/dispatch/oauth/refresh_credentials", + PluginOAuthCredentialsResponse, + data={ + "user_id": user_id, + "data": { + "provider": provider, + "redirect_uri": redirect_uri, + "system_credentials": system_credentials, + "credentials": credentials, + }, + }, + headers={ + "X-Plugin-ID": plugin_id, + "Content-Type": "application/json", + }, + ) + for resp in response: + return resp + raise ValueError("No response received from plugin daemon for refresh credentials request.") + except Exception as e: + raise ValueError(f"Error refreshing credentials: {e}") + def _convert_request_to_raw_data(self, request: Request) -> bytes: """ Convert a Request object to raw HTTP data. diff --git a/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py b/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py index 095752ea8e..6f3e15d166 100644 --- a/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py +++ b/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py @@ -233,6 +233,12 @@ class AnalyticdbVectorOpenAPI: def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]: from alibabacloud_gpdb20160503 import models as gpdb_20160503_models + document_ids_filter = kwargs.get("document_ids_filter") + where_clause = "" + if document_ids_filter: + document_ids = ", ".join(f"'{id}'" for id in document_ids_filter) + where_clause += f"metadata_->>'document_id' IN ({document_ids})" + score_threshold = kwargs.get("score_threshold") or 0.0 request = gpdb_20160503_models.QueryCollectionDataRequest( dbinstance_id=self.config.instance_id, @@ -245,7 +251,7 @@ class AnalyticdbVectorOpenAPI: vector=query_vector, content=None, top_k=kwargs.get("top_k", 4), - filter=None, + filter=where_clause, ) response = self._client.query_collection_data(request) documents = [] @@ -265,6 +271,11 @@ class AnalyticdbVectorOpenAPI: def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: from alibabacloud_gpdb20160503 import models as gpdb_20160503_models + document_ids_filter = kwargs.get("document_ids_filter") + where_clause = "" + if document_ids_filter: + document_ids = ", ".join(f"'{id}'" for id in document_ids_filter) + where_clause += f"metadata_->>'document_id' IN ({document_ids})" score_threshold = float(kwargs.get("score_threshold") or 0.0) request = gpdb_20160503_models.QueryCollectionDataRequest( dbinstance_id=self.config.instance_id, @@ -277,7 +288,7 @@ class AnalyticdbVectorOpenAPI: vector=None, content=query, top_k=kwargs.get("top_k", 4), - filter=None, + filter=where_clause, ) response = self._client.query_collection_data(request) documents = [] diff --git a/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py b/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py index 44cc5d3e98..ad39717183 100644 --- a/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py +++ b/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py @@ -147,10 +147,17 @@ class ElasticSearchVector(BaseVector): return docs def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: - query_str = {"match": {Field.CONTENT_KEY.value: query}} + query_str: dict[str, Any] = {"match": {Field.CONTENT_KEY.value: query}} document_ids_filter = kwargs.get("document_ids_filter") + if document_ids_filter: - query_str["filter"] = {"terms": {"metadata.document_id": document_ids_filter}} # type: ignore + query_str = { + "bool": { + "must": {"match": {Field.CONTENT_KEY.value: query}}, + "filter": {"terms": {"metadata.document_id": document_ids_filter}}, + } + } + results = self._client.search(index=self._collection_name, query=query_str, size=kwargs.get("top_k", 4)) docs = [] for hit in results["hits"]["hits"]: diff --git a/api/core/rag/datasource/vdb/pgvecto_rs/pgvecto_rs.py b/api/core/rag/datasource/vdb/pgvecto_rs/pgvecto_rs.py index 46aefef11d..b0f0eeca38 100644 --- a/api/core/rag/datasource/vdb/pgvecto_rs/pgvecto_rs.py +++ b/api/core/rag/datasource/vdb/pgvecto_rs/pgvecto_rs.py @@ -6,7 +6,7 @@ from uuid import UUID, uuid4 from numpy import ndarray from pgvecto_rs.sqlalchemy import VECTOR # type: ignore from pydantic import BaseModel, model_validator -from sqlalchemy import Float, String, create_engine, insert, select, text +from sqlalchemy import Float, create_engine, insert, select, text from sqlalchemy import text as sql_text from sqlalchemy.dialects import postgresql from sqlalchemy.orm import Mapped, Session, mapped_column @@ -67,7 +67,7 @@ class PGVectoRS(BaseVector): postgresql.UUID(as_uuid=True), primary_key=True, ) - text: Mapped[str] = mapped_column(String) + text: Mapped[str] meta: Mapped[dict] = mapped_column(postgresql.JSONB) vector: Mapped[ndarray] = mapped_column(VECTOR(dim)) diff --git a/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py b/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py index 552068c99e..55326fd60f 100644 --- a/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py +++ b/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py @@ -118,10 +118,21 @@ class TableStoreVector(BaseVector): def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]: top_k = kwargs.get("top_k", 4) - return self._search_by_vector(query_vector, top_k) + document_ids_filter = kwargs.get("document_ids_filter") + filtered_list = None + if document_ids_filter: + filtered_list = ["document_id=" + item for item in document_ids_filter] + score_threshold = float(kwargs.get("score_threshold") or 0.0) + return self._search_by_vector(query_vector, filtered_list, top_k, score_threshold) def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: - return self._search_by_full_text(query) + top_k = kwargs.get("top_k", 4) + document_ids_filter = kwargs.get("document_ids_filter") + filtered_list = None + if document_ids_filter: + filtered_list = ["document_id=" + item for item in document_ids_filter] + + return self._search_by_full_text(query, filtered_list, top_k) def delete(self) -> None: self._delete_table_if_exist() @@ -230,32 +241,51 @@ class TableStoreVector(BaseVector): primary_key = [("id", id)] row = tablestore.Row(primary_key) self._tablestore_client.delete_row(self._table_name, row, None) - logging.info("Tablestore delete row successfully. id:%s", id) def _search_by_metadata(self, key: str, value: str) -> list[str]: query = tablestore.SearchQuery( tablestore.TermQuery(self._tags_field, str(key) + "=" + str(value)), - limit=100, + limit=1000, get_total_count=False, ) + rows: list[str] = [] + next_token = None + while True: + if next_token is not None: + query.next_token = next_token + + search_response = self._tablestore_client.search( + table_name=self._table_name, + index_name=self._index_name, + search_query=query, + columns_to_get=tablestore.ColumnsToGet( + column_names=[Field.PRIMARY_KEY.value], return_type=tablestore.ColumnReturnType.SPECIFIED + ), + ) - search_response = self._tablestore_client.search( - table_name=self._table_name, - index_name=self._index_name, - search_query=query, - columns_to_get=tablestore.ColumnsToGet(return_type=tablestore.ColumnReturnType.ALL_FROM_INDEX), - ) + if search_response is not None: + rows.extend([row[0][0][1] for row in search_response.rows]) - return [row[0][0][1] for row in search_response.rows] + if search_response is None or search_response.next_token == b"": + break + else: + next_token = search_response.next_token - def _search_by_vector(self, query_vector: list[float], top_k: int) -> list[Document]: - ots_query = tablestore.KnnVectorQuery( + return rows + + def _search_by_vector( + self, query_vector: list[float], document_ids_filter: list[str] | None, top_k: int, score_threshold: float + ) -> list[Document]: + knn_vector_query = tablestore.KnnVectorQuery( field_name=Field.VECTOR.value, top_k=top_k, float32_query_vector=query_vector, ) + if document_ids_filter: + knn_vector_query.filter = tablestore.TermsQuery(self._tags_field, document_ids_filter) + sort = tablestore.Sort(sorters=[tablestore.ScoreSort(sort_order=tablestore.SortOrder.DESC)]) - search_query = tablestore.SearchQuery(ots_query, limit=top_k, get_total_count=False, sort=sort) + search_query = tablestore.SearchQuery(knn_vector_query, limit=top_k, get_total_count=False, sort=sort) search_response = self._tablestore_client.search( table_name=self._table_name, @@ -263,30 +293,32 @@ class TableStoreVector(BaseVector): search_query=search_query, columns_to_get=tablestore.ColumnsToGet(return_type=tablestore.ColumnReturnType.ALL_FROM_INDEX), ) - logging.info( - "Tablestore search successfully. request_id:%s", - search_response.request_id, - ) - return self._to_query_result(search_response) - - def _to_query_result(self, search_response: tablestore.SearchResponse) -> list[Document]: documents = [] - for row in search_response.rows: - documents.append( - Document( - page_content=row[1][2][1], - vector=json.loads(row[1][3][1]), - metadata=json.loads(row[1][0][1]), + for search_hit in search_response.search_hits: + if search_hit.score > score_threshold: + metadata = json.loads(search_hit.row[1][0][1]) + metadata["score"] = search_hit.score + documents.append( + Document( + page_content=search_hit.row[1][2][1], + vector=json.loads(search_hit.row[1][3][1]), + metadata=metadata, + ) ) - ) - + documents = sorted(documents, key=lambda x: x.metadata["score"] if x.metadata else 0, reverse=True) return documents - def _search_by_full_text(self, query: str) -> list[Document]: + def _search_by_full_text(self, query: str, document_ids_filter: list[str] | None, top_k: int) -> list[Document]: + bool_query = tablestore.BoolQuery() + bool_query.must_queries.append(tablestore.MatchQuery(text=query, field_name=Field.CONTENT_KEY.value)) + + if document_ids_filter: + bool_query.filter_queries.append(tablestore.TermsQuery(self._tags_field, document_ids_filter)) + search_query = tablestore.SearchQuery( - query=tablestore.MatchQuery(text=query, field_name=Field.CONTENT_KEY.value), + query=bool_query, sort=tablestore.Sort(sorters=[tablestore.ScoreSort(sort_order=tablestore.SortOrder.DESC)]), - limit=100, + limit=top_k, ) search_response = self._tablestore_client.search( table_name=self._table_name, @@ -295,7 +327,16 @@ class TableStoreVector(BaseVector): columns_to_get=tablestore.ColumnsToGet(return_type=tablestore.ColumnReturnType.ALL_FROM_INDEX), ) - return self._to_query_result(search_response) + documents = [] + for search_hit in search_response.search_hits: + documents.append( + Document( + page_content=search_hit.row[1][2][1], + vector=json.loads(search_hit.row[1][3][1]), + metadata=json.loads(search_hit.row[1][0][1]), + ) + ) + return documents class TableStoreVectorFactory(AbstractVectorFactory): diff --git a/api/core/rag/datasource/vdb/tencent/tencent_vector.py b/api/core/rag/datasource/vdb/tencent/tencent_vector.py index 75afe0cdb8..23ed8a3344 100644 --- a/api/core/rag/datasource/vdb/tencent/tencent_vector.py +++ b/api/core/rag/datasource/vdb/tencent/tencent_vector.py @@ -206,9 +206,19 @@ class TencentVector(BaseVector): def delete_by_ids(self, ids: list[str]) -> None: if not ids: return - self._client.delete( - database_name=self._client_config.database, collection_name=self.collection_name, document_ids=ids - ) + + total_count = len(ids) + batch_size = self._client_config.max_upsert_batch_size + batch = math.ceil(total_count / batch_size) + + for j in range(batch): + start_idx = j * batch_size + end_idx = min(total_count, (j + 1) * batch_size) + batch_ids = ids[start_idx:end_idx] + + self._client.delete( + database_name=self._client_config.database, collection_name=self.collection_name, document_ids=batch_ids + ) def delete_by_metadata_field(self, key: str, value: str) -> None: self._client.delete( @@ -274,7 +284,8 @@ class TencentVector(BaseVector): # Compatible with version 1.1.3 and below. meta = json.loads(meta) score = 1 - result.get("score", 0.0) - score = result.get("score", 0.0) + else: + score = result.get("score", 0.0) if score > score_threshold: meta["score"] = score doc = Document(page_content=result.get(self.field_text), metadata=meta) diff --git a/api/core/rag/extractor/notion_extractor.py b/api/core/rag/extractor/notion_extractor.py index 032b30c34d..4dc72ce302 100644 --- a/api/core/rag/extractor/notion_extractor.py +++ b/api/core/rag/extractor/notion_extractor.py @@ -334,9 +334,10 @@ class NotionExtractor(BaseExtractor): last_edited_time = self.get_notion_last_edited_time() data_source_info = document_model.data_source_info_dict data_source_info["last_edited_time"] = last_edited_time - update_params = {DocumentModel.data_source_info: json.dumps(data_source_info)} - db.session.query(DocumentModel).filter_by(id=document_model.id).update(update_params) + db.session.query(DocumentModel).filter_by(id=document_model.id).update( + {DocumentModel.data_source_info: json.dumps(data_source_info)} + ) # type: ignore db.session.commit() def get_notion_last_edited_time(self) -> str: diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index d61856a8f5..abbdf8de3f 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -1,16 +1,19 @@ import json import logging import mimetypes -from collections.abc import Generator +import time +from collections.abc import Generator, Mapping from os import listdir, path from threading import Lock from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast +from pydantic import TypeAdapter from yarl import URL import contexts from core.helper.provider_cache import ToolProviderCredentialsCache from core.plugin.entities.plugin import ToolProviderID +from core.plugin.impl.oauth import OAuthHandler from core.plugin.impl.tool import PluginToolManager from core.tools.__base.tool_provider import ToolProviderController from core.tools.__base.tool_runtime import ToolRuntime @@ -21,7 +24,7 @@ from core.tools.plugin_tool.tool import PluginTool from core.tools.utils.uuid_utils import is_valid_uuid from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from core.workflow.entities.variable_pool import VariablePool -from services.tools.mcp_tools_mange_service import MCPToolManageService +from services.tools.mcp_tools_manage_service import MCPToolManageService if TYPE_CHECKING: from core.workflow.nodes.tool.entities import ToolEntity @@ -244,12 +247,47 @@ class ToolManager: tenant_id=tenant_id, provider=provider_id, credential_id=builtin_provider.id ), ) + + # decrypt the credentials + decrypted_credentials: Mapping[str, Any] = encrypter.decrypt(builtin_provider.credentials) + + # check if the credentials is expired + if builtin_provider.expires_at != -1 and (builtin_provider.expires_at - 60) < int(time.time()): + # TODO: circular import + from services.tools.builtin_tools_manage_service import BuiltinToolManageService + + # refresh the credentials + tool_provider = ToolProviderID(provider_id) + provider_name = tool_provider.provider_name + redirect_uri = f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider_id}/tool/callback" + system_credentials = BuiltinToolManageService.get_oauth_client(tenant_id, provider_id) + oauth_handler = OAuthHandler() + # refresh the credentials + refreshed_credentials = oauth_handler.refresh_credentials( + tenant_id=tenant_id, + user_id=builtin_provider.user_id, + plugin_id=tool_provider.plugin_id, + provider=provider_name, + redirect_uri=redirect_uri, + system_credentials=system_credentials or {}, + credentials=decrypted_credentials, + ) + # update the credentials + builtin_provider.encrypted_credentials = ( + TypeAdapter(dict[str, Any]) + .dump_json(encrypter.encrypt(dict(refreshed_credentials.credentials))) + .decode("utf-8") + ) + builtin_provider.expires_at = refreshed_credentials.expires_at + db.session.commit() + decrypted_credentials = refreshed_credentials.credentials + return cast( BuiltinTool, builtin_tool.fork_tool_runtime( runtime=ToolRuntime( tenant_id=tenant_id, - credentials=encrypter.decrypt(builtin_provider.credentials), + credentials=dict(decrypted_credentials), credential_type=CredentialType.of(builtin_provider.credential_type), runtime_parameters={}, invoke_from=invoke_from, diff --git a/api/core/tools/utils/dataset_retriever/dataset_retriever_base_tool.py b/api/core/tools/utils/dataset_retriever/dataset_retriever_base_tool.py index a4d2de3b1c..567275531e 100644 --- a/api/core/tools/utils/dataset_retriever/dataset_retriever_base_tool.py +++ b/api/core/tools/utils/dataset_retriever/dataset_retriever_base_tool.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Any, Optional +from typing import Optional from msal_extensions.persistence import ABC # type: ignore from pydantic import BaseModel, ConfigDict @@ -21,11 +21,7 @@ class DatasetRetrieverBaseTool(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) @abstractmethod - def _run( - self, - *args: Any, - **kwargs: Any, - ) -> Any: + def _run(self, query: str) -> str: """Use the tool. Add run_manager: Optional[CallbackManagerForToolRun] = None diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index a4616eda69..8cf33ac81e 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -270,7 +270,14 @@ class AgentNode(BaseNode): ) extra = tool.get("extra", {}) - runtime_variable_pool = variable_pool if self._node_data.version != "1" else None + + # This is an issue that caused problems before. + # Logically, we shouldn't use the node_data.version field for judgment + # But for backward compatibility with historical data + # this version field judgment is still preserved here. + runtime_variable_pool: VariablePool | None = None + if node_data.version != "1" or node_data.tool_node_version != "1": + runtime_variable_pool = variable_pool tool_runtime = ToolManager.get_agent_tool_runtime( self.tenant_id, self.app_id, entity, self.invoke_from, runtime_variable_pool ) @@ -479,7 +486,7 @@ class AgentNode(BaseNode): text = "" files: list[File] = [] - json: list[dict] = [] + json_list: list[dict] = [] agent_logs: list[AgentLogEvent] = [] agent_execution_metadata: Mapping[WorkflowNodeExecutionMetadataKey, Any] = {} @@ -557,7 +564,7 @@ class AgentNode(BaseNode): if key in WorkflowNodeExecutionMetadataKey.__members__.values() } if message.message.json_object is not None: - json.append(message.message.json_object) + json_list.append(message.message.json_object) elif message.type == ToolInvokeMessage.MessageType.LINK: assert isinstance(message.message, ToolInvokeMessage.TextMessage) stream_text = f"Link: {message.message.text}\n" @@ -669,8 +676,8 @@ class AgentNode(BaseNode): } ) # Step 2: normalize JSON into {"data": [...]}.change json to list[dict] - if json: - json_output.extend(json) + if json_list: + json_output.extend(json_list) else: json_output.append({"data": []}) diff --git a/api/core/workflow/nodes/agent/entities.py b/api/core/workflow/nodes/agent/entities.py index 075a41fb2f..11b11068e7 100644 --- a/api/core/workflow/nodes/agent/entities.py +++ b/api/core/workflow/nodes/agent/entities.py @@ -13,6 +13,10 @@ class AgentNodeData(BaseNodeData): agent_strategy_name: str agent_strategy_label: str # redundancy memory: MemoryConfig | None = None + # The version of the tool parameter. + # If this value is None, it indicates this is a previous version + # and requires using the legacy parameter parsing rules. + tool_node_version: str | None = None class AgentInput(BaseModel): value: Union[list[str], list[ToolSelector], Any] diff --git a/api/core/workflow/nodes/knowledge_retrieval/entities.py b/api/core/workflow/nodes/knowledge_retrieval/entities.py index e9122b1eec..f1767bdf9e 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/entities.py +++ b/api/core/workflow/nodes/knowledge_retrieval/entities.py @@ -118,7 +118,7 @@ class KnowledgeRetrievalNodeData(BaseNodeData): multiple_retrieval_config: Optional[MultipleRetrievalConfig] = None single_retrieval_config: Optional[SingleRetrievalConfig] = None metadata_filtering_mode: Optional[Literal["disabled", "automatic", "manual"]] = "disabled" - metadata_model_config: ModelConfig + metadata_model_config: Optional[ModelConfig] = None metadata_filtering_conditions: Optional[MetadataFilteringCondition] = None vision: VisionConfig = Field(default_factory=VisionConfig) diff --git a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py index 4e9a38f552..be8fa4d226 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py +++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py @@ -462,7 +462,7 @@ class KnowledgeRetrievalNode(BaseNode): expected_value = self.graph_runtime_state.variable_pool.convert_template( expected_value ).value[0] - if expected_value.value_type == "number": # type: ignore + if expected_value.value_type in {"number", "integer", "float"}: # type: ignore expected_value = expected_value.value # type: ignore elif expected_value.value_type == "string": # type: ignore expected_value = re.sub(r"[\r\n\t]+", " ", expected_value.text).strip() # type: ignore @@ -509,6 +509,8 @@ class KnowledgeRetrievalNode(BaseNode): # get all metadata field metadata_fields = db.session.query(DatasetMetadata).filter(DatasetMetadata.dataset_id.in_(dataset_ids)).all() all_metadata_fields = [metadata_field.name for metadata_field in metadata_fields] + if node_data.metadata_model_config is None: + raise ValueError("metadata_model_config is required") # get metadata model instance and fetch model config model_instance, model_config = self.get_model_config(node_data.metadata_model_config) # fetch prompt messages @@ -701,7 +703,7 @@ class KnowledgeRetrievalNode(BaseNode): ) def _get_prompt_template(self, node_data: KnowledgeRetrievalNodeData, metadata_fields: list, query: str): - model_mode = ModelMode(node_data.metadata_model_config.mode) + model_mode = ModelMode(node_data.metadata_model_config.mode) # type: ignore input_text = query prompt_messages: list[LLMNodeChatModelMessage] = [] diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 91e7312805..90a0397b67 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -565,7 +565,7 @@ class LLMNode(BaseNode): retriever_resources=original_retriever_resource, context=context_str.strip() ) - def _convert_to_original_retriever_resource(self, context_dict: dict): + def _convert_to_original_retriever_resource(self, context_dict: dict) -> RetrievalSourceMetadata | None: if ( "metadata" in context_dict and "_source" in context_dict["metadata"] diff --git a/api/core/workflow/nodes/node_mapping.py b/api/core/workflow/nodes/node_mapping.py index ccfaec4a8c..294b47670b 100644 --- a/api/core/workflow/nodes/node_mapping.py +++ b/api/core/workflow/nodes/node_mapping.py @@ -73,6 +73,9 @@ NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = { }, NodeType.TOOL: { LATEST_VERSION: ToolNode, + # This is an issue that caused problems before. + # Logically, we shouldn't use two different versions to point to the same class here, + # but in order to maintain compatibility with historical data, this approach has been retained. "2": ToolNode, "1": ToolNode, }, @@ -123,6 +126,9 @@ NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = { }, NodeType.AGENT: { LATEST_VERSION: AgentNode, + # This is an issue that caused problems before. + # Logically, we shouldn't use two different versions to point to the same class here, + # but in order to maintain compatibility with historical data, this approach has been retained. "2": AgentNode, "1": AgentNode, }, diff --git a/api/core/workflow/nodes/tool/entities.py b/api/core/workflow/nodes/tool/entities.py index 88c5160d14..f0a44d919b 100644 --- a/api/core/workflow/nodes/tool/entities.py +++ b/api/core/workflow/nodes/tool/entities.py @@ -59,6 +59,10 @@ class ToolNodeData(BaseNodeData, ToolEntity): return typ tool_parameters: dict[str, ToolInput] + # The version of the tool parameter. + # If this value is None, it indicates this is a previous version + # and requires using the legacy parameter parsing rules. + tool_node_version: str | None = None @field_validator("tool_parameters", mode="before") @classmethod diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index c565ad15c1..f437ac841d 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -70,7 +70,13 @@ class ToolNode(BaseNode): try: from core.tools.tool_manager import ToolManager - variable_pool = self.graph_runtime_state.variable_pool if self._node_data.version != "1" else None + # This is an issue that caused problems before. + # Logically, we shouldn't use the node_data.version field for judgment + # But for backward compatibility with historical data + # this version field judgment is still preserved here. + variable_pool: VariablePool | None = None + if node_data.version != "1" or node_data.tool_node_version != "1": + variable_pool = self.graph_runtime_state.variable_pool tool_runtime = ToolManager.get_workflow_tool_runtime( self.tenant_id, self.app_id, self.node_id, self._node_data, self.invoke_from, variable_pool ) @@ -310,7 +316,14 @@ class ToolNode(BaseNode): variables[variable_name] = variable_value elif message.type == ToolInvokeMessage.MessageType.FILE: assert message.meta is not None - assert isinstance(message.meta, File) + assert isinstance(message.meta, dict) + # Validate that meta contains a 'file' key + if "file" not in message.meta: + raise ToolNodeError("File message is missing 'file' key in meta") + + # Validate that the file is an instance of File + if not isinstance(message.meta["file"], File): + raise ToolNodeError(f"Expected File object but got {type(message.meta['file']).__name__}") files.append(message.meta["file"]) elif message.type == ToolInvokeMessage.MessageType.LOG: assert isinstance(message.message, ToolInvokeMessage.LogMessage) diff --git a/api/core/workflow/workflow_cycle_manager.py b/api/core/workflow/workflow_cycle_manager.py index 3e591ef885..03f670707e 100644 --- a/api/core/workflow/workflow_cycle_manager.py +++ b/api/core/workflow/workflow_cycle_manager.py @@ -1,6 +1,6 @@ from collections.abc import Mapping from dataclasses import dataclass -from datetime import UTC, datetime +from datetime import datetime from typing import Any, Optional, Union from uuid import uuid4 @@ -71,7 +71,7 @@ class WorkflowCycleManager: workflow_version=self._workflow_info.version, graph=self._workflow_info.graph_data, inputs=inputs, - started_at=datetime.now(UTC).replace(tzinfo=None), + started_at=naive_utc_now(), ) return self._save_and_cache_workflow_execution(execution) @@ -85,6 +85,7 @@ class WorkflowCycleManager: outputs: Mapping[str, Any] | None = None, conversation_id: Optional[str] = None, trace_manager: Optional[TraceQueueManager] = None, + external_trace_id: Optional[str] = None, ) -> WorkflowExecution: workflow_execution = self._get_workflow_execution_or_raise_error(workflow_run_id) @@ -96,7 +97,7 @@ class WorkflowCycleManager: total_steps=total_steps, ) - self._add_trace_task_if_needed(trace_manager, workflow_execution, conversation_id) + self._add_trace_task_if_needed(trace_manager, workflow_execution, conversation_id, external_trace_id) self._workflow_execution_repository.save(workflow_execution) return workflow_execution @@ -111,6 +112,7 @@ class WorkflowCycleManager: exceptions_count: int = 0, conversation_id: Optional[str] = None, trace_manager: Optional[TraceQueueManager] = None, + external_trace_id: Optional[str] = None, ) -> WorkflowExecution: execution = self._get_workflow_execution_or_raise_error(workflow_run_id) @@ -123,7 +125,7 @@ class WorkflowCycleManager: exceptions_count=exceptions_count, ) - self._add_trace_task_if_needed(trace_manager, execution, conversation_id) + self._add_trace_task_if_needed(trace_manager, execution, conversation_id, external_trace_id) self._workflow_execution_repository.save(execution) return execution @@ -139,6 +141,7 @@ class WorkflowCycleManager: conversation_id: Optional[str] = None, trace_manager: Optional[TraceQueueManager] = None, exceptions_count: int = 0, + external_trace_id: Optional[str] = None, ) -> WorkflowExecution: workflow_execution = self._get_workflow_execution_or_raise_error(workflow_run_id) now = naive_utc_now() @@ -154,7 +157,7 @@ class WorkflowCycleManager: ) self._fail_running_node_executions(workflow_execution.id_, error_message, now) - self._add_trace_task_if_needed(trace_manager, workflow_execution, conversation_id) + self._add_trace_task_if_needed(trace_manager, workflow_execution, conversation_id, external_trace_id) self._workflow_execution_repository.save(workflow_execution) return workflow_execution @@ -312,6 +315,7 @@ class WorkflowCycleManager: trace_manager: Optional[TraceQueueManager], workflow_execution: WorkflowExecution, conversation_id: Optional[str], + external_trace_id: Optional[str], ) -> None: """Add trace task if trace manager is provided.""" if trace_manager: @@ -321,6 +325,7 @@ class WorkflowCycleManager: workflow_execution=workflow_execution, conversation_id=conversation_id, user_id=trace_manager.user_id, + external_trace_id=external_trace_id, ) ) @@ -356,7 +361,7 @@ class WorkflowCycleManager: created_at: Optional[datetime] = None, ) -> WorkflowNodeExecution: """Create a node execution from an event.""" - now = datetime.now(UTC).replace(tzinfo=None) + now = naive_utc_now() created_at = created_at or now metadata = { @@ -403,7 +408,7 @@ class WorkflowCycleManager: handle_special_values: bool = False, ) -> None: """Update node execution with completion data.""" - finished_at = datetime.now(UTC).replace(tzinfo=None) + finished_at = naive_utc_now() elapsed_time = (finished_at - event.start_at).total_seconds() # Process data diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index d2375da39c..c8082ebf50 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -163,6 +163,7 @@ class WorkflowEntry: graph=graph, graph_runtime_state=GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()), ) + node.init_node_data(node_config_data) try: # variable selector to variable mapping @@ -273,6 +274,7 @@ class WorkflowEntry: graph=graph, graph_runtime_state=GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()), ) + node.init_node_data(node_data) try: # variable selector to variable mapping 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/events/event_handlers/create_document_index.py b/api/events/event_handlers/create_document_index.py index 8a677f6b6f..cb48bd92a0 100644 --- a/api/events/event_handlers/create_document_index.py +++ b/api/events/event_handlers/create_document_index.py @@ -1,4 +1,3 @@ -import datetime import logging import time @@ -8,6 +7,7 @@ from werkzeug.exceptions import NotFound from core.indexing_runner import DocumentIsPausedError, IndexingRunner from events.event_handlers.document_index_event import document_index_created from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from models.dataset import Document @@ -33,7 +33,7 @@ def handle(sender, **kwargs): raise NotFound("Document not found") document.indexing_status = "parsing" - document.processing_started_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + document.processing_started_at = naive_utc_now() documents.append(document) db.session.add(document) db.session.commit() 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/extensions/storage/azure_blob_storage.py b/api/extensions/storage/azure_blob_storage.py index 7448fd4a6b..81eec94da4 100644 --- a/api/extensions/storage/azure_blob_storage.py +++ b/api/extensions/storage/azure_blob_storage.py @@ -1,5 +1,5 @@ from collections.abc import Generator -from datetime import UTC, datetime, timedelta +from datetime import timedelta from typing import Optional from azure.identity import ChainedTokenCredential, DefaultAzureCredential @@ -8,6 +8,7 @@ from azure.storage.blob import AccountSasPermissions, BlobServiceClient, Resourc from configs import dify_config from extensions.ext_redis import redis_client from extensions.storage.base_storage import BaseStorage +from libs.datetime_utils import naive_utc_now class AzureBlobStorage(BaseStorage): @@ -78,7 +79,7 @@ class AzureBlobStorage(BaseStorage): account_key=self.account_key or "", resource_types=ResourceTypes(service=True, container=True, object=True), permission=AccountSasPermissions(read=True, write=True, delete=True, list=True, add=True, create=True), - expiry=datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1), + expiry=naive_utc_now() + timedelta(hours=1), ) redis_client.set(cache_key, sas_token, ex=3000) return BlobServiceClient(account_url=self.account_url or "", credential=sas_token) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index 25d1390492..c974dbb700 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -148,9 +148,7 @@ def _build_from_local_file( if strict_type_validation and detected_file_type.value != specified_type: raise ValueError("Detected file type does not match the specified type. Please verify the file.") - file_type = ( - FileType(specified_type) if specified_type and specified_type != FileType.CUSTOM.value else detected_file_type - ) + file_type = FileType(specified_type) if specified_type and specified_type != FileType.CUSTOM else detected_file_type return File( id=mapping.get("id"), @@ -199,9 +197,7 @@ def _build_from_remote_url( raise ValueError("Detected file type does not match the specified type. Please verify the file.") file_type = ( - FileType(specified_type) - if specified_type and specified_type != FileType.CUSTOM.value - else detected_file_type + FileType(specified_type) if specified_type and specified_type != FileType.CUSTOM else detected_file_type ) return File( @@ -286,9 +282,7 @@ def _build_from_tool_file( if strict_type_validation and specified_type and detected_file_type.value != specified_type: raise ValueError("Detected file type does not match the specified type. Please verify the file.") - file_type = ( - FileType(specified_type) if specified_type and specified_type != FileType.CUSTOM.value else detected_file_type - ) + file_type = FileType(specified_type) if specified_type and specified_type != FileType.CUSTOM else detected_file_type return File( id=mapping.get("id"), diff --git a/api/libs/email_i18n.py b/api/libs/email_i18n.py new file mode 100644 index 0000000000..bfbf41a073 --- /dev/null +++ b/api/libs/email_i18n.py @@ -0,0 +1,461 @@ +""" +Email Internationalization Module + +This module provides a centralized, elegant way to handle email internationalization +in Dify. It follows Domain-Driven Design principles with proper type hints and +eliminates the need for repetitive language switching logic. +""" + +from dataclasses import dataclass +from enum import Enum +from typing import Any, Optional, Protocol + +from flask import render_template +from pydantic import BaseModel, Field + +from extensions.ext_mail import mail +from services.feature_service import BrandingModel, FeatureService + + +class EmailType(Enum): + """Enumeration of supported email types.""" + + RESET_PASSWORD = "reset_password" + INVITE_MEMBER = "invite_member" + EMAIL_CODE_LOGIN = "email_code_login" + CHANGE_EMAIL_OLD = "change_email_old" + CHANGE_EMAIL_NEW = "change_email_new" + OWNER_TRANSFER_CONFIRM = "owner_transfer_confirm" + OWNER_TRANSFER_OLD_NOTIFY = "owner_transfer_old_notify" + OWNER_TRANSFER_NEW_NOTIFY = "owner_transfer_new_notify" + ACCOUNT_DELETION_SUCCESS = "account_deletion_success" + ACCOUNT_DELETION_VERIFICATION = "account_deletion_verification" + ENTERPRISE_CUSTOM = "enterprise_custom" + QUEUE_MONITOR_ALERT = "queue_monitor_alert" + DOCUMENT_CLEAN_NOTIFY = "document_clean_notify" + + +class EmailLanguage(Enum): + """Supported email languages with fallback handling.""" + + EN_US = "en-US" + ZH_HANS = "zh-Hans" + + @classmethod + def from_language_code(cls, language_code: str) -> "EmailLanguage": + """Convert a language code to EmailLanguage with fallback to English.""" + if language_code == "zh-Hans": + return cls.ZH_HANS + return cls.EN_US + + +@dataclass(frozen=True) +class EmailTemplate: + """Immutable value object representing an email template configuration.""" + + subject: str + template_path: str + branded_template_path: str + + +@dataclass(frozen=True) +class EmailContent: + """Immutable value object containing rendered email content.""" + + subject: str + html_content: str + template_context: dict[str, Any] + + +class EmailI18nConfig(BaseModel): + """Configuration for email internationalization.""" + + model_config = {"frozen": True, "extra": "forbid"} + + templates: dict[EmailType, dict[EmailLanguage, EmailTemplate]] = Field( + default_factory=dict, description="Mapping of email types to language-specific templates" + ) + + def get_template(self, email_type: EmailType, language: EmailLanguage) -> EmailTemplate: + """Get template configuration for specific email type and language.""" + type_templates = self.templates.get(email_type) + if not type_templates: + raise ValueError(f"No templates configured for email type: {email_type}") + + template = type_templates.get(language) + if not template: + # Fallback to English if specific language not found + template = type_templates.get(EmailLanguage.EN_US) + if not template: + raise ValueError(f"No template found for {email_type} in {language} or English") + + return template + + +class EmailRenderer(Protocol): + """Protocol for email template renderers.""" + + def render_template(self, template_path: str, **context: Any) -> str: + """Render email template with given context.""" + ... + + +class FlaskEmailRenderer: + """Flask-based email template renderer.""" + + def render_template(self, template_path: str, **context: Any) -> str: + """Render email template using Flask's render_template.""" + return render_template(template_path, **context) + + +class BrandingService(Protocol): + """Protocol for branding service abstraction.""" + + def get_branding_config(self) -> BrandingModel: + """Get current branding configuration.""" + ... + + +class FeatureBrandingService: + """Feature service based branding implementation.""" + + def get_branding_config(self) -> BrandingModel: + """Get branding configuration from feature service.""" + return FeatureService.get_system_features().branding + + +class EmailSender(Protocol): + """Protocol for email sending abstraction.""" + + def send_email(self, to: str, subject: str, html_content: str) -> None: + """Send email with given parameters.""" + ... + + +class FlaskMailSender: + """Flask-Mail based email sender.""" + + def send_email(self, to: str, subject: str, html_content: str) -> None: + """Send email using Flask-Mail.""" + if mail.is_inited(): + mail.send(to=to, subject=subject, html=html_content) + + +class EmailI18nService: + """ + Main service for internationalized email handling. + + This service provides a clean API for sending internationalized emails + with proper branding support and template management. + """ + + def __init__( + self, + config: EmailI18nConfig, + renderer: EmailRenderer, + branding_service: BrandingService, + sender: EmailSender, + ) -> None: + self._config = config + self._renderer = renderer + self._branding_service = branding_service + self._sender = sender + + def send_email( + self, + email_type: EmailType, + language_code: str, + to: str, + template_context: Optional[dict[str, Any]] = None, + ) -> None: + """ + Send internationalized email with branding support. + + Args: + email_type: Type of email to send + language_code: Target language code + to: Recipient email address + template_context: Additional context for template rendering + """ + if template_context is None: + template_context = {} + + language = EmailLanguage.from_language_code(language_code) + email_content = self._render_email_content(email_type, language, template_context) + + self._sender.send_email(to=to, subject=email_content.subject, html_content=email_content.html_content) + + def send_change_email( + self, + language_code: str, + to: str, + code: str, + phase: str, + ) -> None: + """ + Send change email notification with phase-specific handling. + + Args: + language_code: Target language code + to: Recipient email address + code: Verification code + phase: Either 'old_email' or 'new_email' + """ + if phase == "old_email": + email_type = EmailType.CHANGE_EMAIL_OLD + elif phase == "new_email": + email_type = EmailType.CHANGE_EMAIL_NEW + else: + raise ValueError(f"Invalid phase: {phase}. Must be 'old_email' or 'new_email'") + + self.send_email( + email_type=email_type, + language_code=language_code, + to=to, + template_context={ + "to": to, + "code": code, + }, + ) + + def send_raw_email( + self, + to: str | list[str], + subject: str, + html_content: str, + ) -> None: + """ + Send a raw email directly without template processing. + + This method is provided for backward compatibility with legacy email + sending that uses pre-rendered HTML content (e.g., enterprise emails + with custom templates). + + Args: + to: Recipient email address(es) + subject: Email subject + html_content: Pre-rendered HTML content + """ + if isinstance(to, list): + for recipient in to: + self._sender.send_email(to=recipient, subject=subject, html_content=html_content) + else: + self._sender.send_email(to=to, subject=subject, html_content=html_content) + + def _render_email_content( + self, + email_type: EmailType, + language: EmailLanguage, + template_context: dict[str, Any], + ) -> EmailContent: + """Render email content with branding and internationalization.""" + template_config = self._config.get_template(email_type, language) + branding = self._branding_service.get_branding_config() + + # Determine template path based on branding + template_path = template_config.branded_template_path if branding.enabled else template_config.template_path + + # Prepare template context with branding information + full_context = { + **template_context, + "branding_enabled": branding.enabled, + "application_title": branding.application_title if branding.enabled else "Dify", + } + + # Render template + html_content = self._renderer.render_template(template_path, **full_context) + + # Apply templating to subject with all context variables + subject = template_config.subject + try: + subject = subject.format(**full_context) + except KeyError: + # If template variables are missing, fall back to basic formatting + if branding.enabled and "{application_title}" in subject: + subject = subject.format(application_title=branding.application_title) + + return EmailContent( + subject=subject, + html_content=html_content, + template_context=full_context, + ) + + +def create_default_email_config() -> EmailI18nConfig: + """Create default email i18n configuration with all supported templates.""" + templates: dict[EmailType, dict[EmailLanguage, EmailTemplate]] = { + EmailType.RESET_PASSWORD: { + EmailLanguage.EN_US: EmailTemplate( + subject="Set Your {application_title} Password", + template_path="reset_password_mail_template_en-US.html", + branded_template_path="without-brand/reset_password_mail_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="设置您的 {application_title} 密码", + template_path="reset_password_mail_template_zh-CN.html", + branded_template_path="without-brand/reset_password_mail_template_zh-CN.html", + ), + }, + EmailType.INVITE_MEMBER: { + EmailLanguage.EN_US: EmailTemplate( + subject="Join {application_title} Workspace Now", + template_path="invite_member_mail_template_en-US.html", + branded_template_path="without-brand/invite_member_mail_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="立即加入 {application_title} 工作空间", + template_path="invite_member_mail_template_zh-CN.html", + branded_template_path="without-brand/invite_member_mail_template_zh-CN.html", + ), + }, + EmailType.EMAIL_CODE_LOGIN: { + EmailLanguage.EN_US: EmailTemplate( + subject="{application_title} Login Code", + template_path="email_code_login_mail_template_en-US.html", + branded_template_path="without-brand/email_code_login_mail_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="{application_title} 登录验证码", + template_path="email_code_login_mail_template_zh-CN.html", + branded_template_path="without-brand/email_code_login_mail_template_zh-CN.html", + ), + }, + EmailType.CHANGE_EMAIL_OLD: { + EmailLanguage.EN_US: EmailTemplate( + subject="Check your current email", + template_path="change_mail_confirm_old_template_en-US.html", + branded_template_path="without-brand/change_mail_confirm_old_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="检测您现在的邮箱", + template_path="change_mail_confirm_old_template_zh-CN.html", + branded_template_path="without-brand/change_mail_confirm_old_template_zh-CN.html", + ), + }, + EmailType.CHANGE_EMAIL_NEW: { + EmailLanguage.EN_US: EmailTemplate( + subject="Confirm your new email address", + template_path="change_mail_confirm_new_template_en-US.html", + branded_template_path="without-brand/change_mail_confirm_new_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="确认您的邮箱地址变更", + template_path="change_mail_confirm_new_template_zh-CN.html", + branded_template_path="without-brand/change_mail_confirm_new_template_zh-CN.html", + ), + }, + EmailType.OWNER_TRANSFER_CONFIRM: { + EmailLanguage.EN_US: EmailTemplate( + subject="Verify Your Request to Transfer Workspace Ownership", + template_path="transfer_workspace_owner_confirm_template_en-US.html", + branded_template_path="without-brand/transfer_workspace_owner_confirm_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="验证您转移工作空间所有权的请求", + template_path="transfer_workspace_owner_confirm_template_zh-CN.html", + branded_template_path="without-brand/transfer_workspace_owner_confirm_template_zh-CN.html", + ), + }, + EmailType.OWNER_TRANSFER_OLD_NOTIFY: { + EmailLanguage.EN_US: EmailTemplate( + subject="Workspace ownership has been transferred", + template_path="transfer_workspace_old_owner_notify_template_en-US.html", + branded_template_path="without-brand/transfer_workspace_old_owner_notify_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="工作区所有权已转移", + template_path="transfer_workspace_old_owner_notify_template_zh-CN.html", + branded_template_path="without-brand/transfer_workspace_old_owner_notify_template_zh-CN.html", + ), + }, + EmailType.OWNER_TRANSFER_NEW_NOTIFY: { + EmailLanguage.EN_US: EmailTemplate( + subject="You are now the owner of {WorkspaceName}", + template_path="transfer_workspace_new_owner_notify_template_en-US.html", + branded_template_path="without-brand/transfer_workspace_new_owner_notify_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="您现在是 {WorkspaceName} 的所有者", + template_path="transfer_workspace_new_owner_notify_template_zh-CN.html", + branded_template_path="without-brand/transfer_workspace_new_owner_notify_template_zh-CN.html", + ), + }, + EmailType.ACCOUNT_DELETION_SUCCESS: { + EmailLanguage.EN_US: EmailTemplate( + subject="Your Dify.AI Account Has Been Successfully Deleted", + template_path="delete_account_success_template_en-US.html", + branded_template_path="delete_account_success_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="您的 Dify.AI 账户已成功删除", + template_path="delete_account_success_template_zh-CN.html", + branded_template_path="delete_account_success_template_zh-CN.html", + ), + }, + EmailType.ACCOUNT_DELETION_VERIFICATION: { + EmailLanguage.EN_US: EmailTemplate( + subject="Dify.AI Account Deletion and Verification", + template_path="delete_account_code_email_template_en-US.html", + branded_template_path="delete_account_code_email_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="Dify.AI 账户删除和验证", + template_path="delete_account_code_email_template_zh-CN.html", + branded_template_path="delete_account_code_email_template_zh-CN.html", + ), + }, + EmailType.QUEUE_MONITOR_ALERT: { + EmailLanguage.EN_US: EmailTemplate( + subject="Alert: Dataset Queue pending tasks exceeded the limit", + template_path="queue_monitor_alert_email_template_en-US.html", + branded_template_path="queue_monitor_alert_email_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="警报:数据集队列待处理任务超过限制", + template_path="queue_monitor_alert_email_template_zh-CN.html", + branded_template_path="queue_monitor_alert_email_template_zh-CN.html", + ), + }, + EmailType.DOCUMENT_CLEAN_NOTIFY: { + EmailLanguage.EN_US: EmailTemplate( + subject="Dify Knowledge base auto disable notification", + template_path="clean_document_job_mail_template-US.html", + branded_template_path="clean_document_job_mail_template-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="Dify 知识库自动禁用通知", + template_path="clean_document_job_mail_template_zh-CN.html", + branded_template_path="clean_document_job_mail_template_zh-CN.html", + ), + }, + } + + return EmailI18nConfig(templates=templates) + + +# Singleton instance for application-wide use +def get_default_email_i18n_service() -> EmailI18nService: + """Get configured email i18n service with default dependencies.""" + config = create_default_email_config() + renderer = FlaskEmailRenderer() + branding_service = FeatureBrandingService() + sender = FlaskMailSender() + + return EmailI18nService( + config=config, + renderer=renderer, + branding_service=branding_service, + sender=sender, + ) + + +# Global instance +_email_i18n_service: Optional[EmailI18nService] = None + + +def get_email_i18n_service() -> EmailI18nService: + """Get global email i18n service instance.""" + global _email_i18n_service + if _email_i18n_service is None: + _email_i18n_service = get_default_email_i18n_service() + return _email_i18n_service diff --git a/api/libs/oauth_data_source.py b/api/libs/oauth_data_source.py index 218109522d..78f827584c 100644 --- a/api/libs/oauth_data_source.py +++ b/api/libs/oauth_data_source.py @@ -1,4 +1,3 @@ -import datetime import urllib.parse from typing import Any @@ -6,6 +5,7 @@ import requests from flask_login import current_user from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from models.source import DataSourceOauthBinding @@ -75,7 +75,7 @@ class NotionOAuth(OAuthDataSource): if data_source_binding: data_source_binding.source_info = source_info data_source_binding.disabled = False - data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + data_source_binding.updated_at = naive_utc_now() db.session.commit() else: new_data_source_binding = DataSourceOauthBinding( @@ -115,7 +115,7 @@ class NotionOAuth(OAuthDataSource): if data_source_binding: data_source_binding.source_info = source_info data_source_binding.disabled = False - data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + data_source_binding.updated_at = naive_utc_now() db.session.commit() else: new_data_source_binding = DataSourceOauthBinding( @@ -154,7 +154,7 @@ class NotionOAuth(OAuthDataSource): } data_source_binding.source_info = new_source_info data_source_binding.disabled = False - data_source_binding.updated_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + data_source_binding.updated_at = naive_utc_now() db.session.commit() else: raise ValueError("Data source binding not found") 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 47ac27511e..d7a5d116c9 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 = '16081485540c' +down_revision = '2adcbe1f5dfb' branch_labels = None depends_on = None diff --git a/api/migrations/versions/2025_07_21_0935-1a83934ad6d1_update_models.py b/api/migrations/versions/2025_07_21_0935-1a83934ad6d1_update_models.py new file mode 100644 index 0000000000..3bdbafda7c --- /dev/null +++ b/api/migrations/versions/2025_07_21_0935-1a83934ad6d1_update_models.py @@ -0,0 +1,51 @@ +"""update models + +Revision ID: 1a83934ad6d1 +Revises: 71f5020c6470 +Create Date: 2025-07-21 09:35:48.774794 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1a83934ad6d1' +down_revision = '71f5020c6470' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tool_mcp_providers', schema=None) as batch_op: + batch_op.alter_column('server_identifier', + existing_type=sa.VARCHAR(length=24), + type_=sa.String(length=64), + existing_nullable=False) + + with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op: + batch_op.alter_column('tool_name', + existing_type=sa.VARCHAR(length=40), + type_=sa.String(length=128), + existing_nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op: + batch_op.alter_column('tool_name', + existing_type=sa.String(length=128), + type_=sa.VARCHAR(length=40), + existing_nullable=False) + + with op.batch_alter_table('tool_mcp_providers', schema=None) as batch_op: + batch_op.alter_column('server_identifier', + existing_type=sa.String(length=64), + type_=sa.VARCHAR(length=24), + existing_nullable=False) + + # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_07_22_0019-375fe79ead14_oauth_refresh_token.py b/api/migrations/versions/2025_07_22_0019-375fe79ead14_oauth_refresh_token.py new file mode 100644 index 0000000000..76d0cb2940 --- /dev/null +++ b/api/migrations/versions/2025_07_22_0019-375fe79ead14_oauth_refresh_token.py @@ -0,0 +1,34 @@ +"""oauth_refresh_token + +Revision ID: 375fe79ead14 +Revises: 1a83934ad6d1 +Create Date: 2025-07-22 00:19:45.599636 + +""" +from alembic import op +import models as models +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '375fe79ead14' +down_revision = '1a83934ad6d1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tool_builtin_providers', schema=None) as batch_op: + batch_op.add_column(sa.Column('expires_at', sa.BigInteger(), server_default=sa.text('-1'), nullable=False)) + + # ### 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_column('expires_at') + + # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_05_15_1635-16081485540c_.py b/api/migrations/versions/2025_07_23_1508-8bcc02c9bd07_add_tenant_plugin_autoupgrade_table.py similarity index 86% rename from api/migrations/versions/2025_05_15_1635-16081485540c_.py rename to api/migrations/versions/2025_07_23_1508-8bcc02c9bd07_add_tenant_plugin_autoupgrade_table.py index f55730bfb2..4ff0402a97 100644 --- a/api/migrations/versions/2025_05_15_1635-16081485540c_.py +++ b/api/migrations/versions/2025_07_23_1508-8bcc02c9bd07_add_tenant_plugin_autoupgrade_table.py @@ -1,18 +1,18 @@ -"""empty message +"""add_tenant_plugin_autoupgrade_table -Revision ID: 16081485540c -Revises: d28f2004b072 -Create Date: 2025-05-15 16:35:39.113777 +Revision ID: 8bcc02c9bd07 +Revises: 375fe79ead14 +Create Date: 2025-07-23 15:08:50.161441 """ from alembic import op import models as models import sqlalchemy as sa - +from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = '16081485540c' -down_revision = '2adcbe1f5dfb' +revision = '8bcc02c9bd07' +down_revision = '375fe79ead14' branch_labels = None depends_on = None @@ -37,5 +37,6 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('tenant_plugin_auto_upgrade_strategies') # ### end Alembic commands ### diff --git a/api/models/account.py b/api/models/account.py index 1af571bc01..01d1625dbd 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -1,5 +1,6 @@ import enum import json +from datetime import datetime from typing import Optional, cast from flask_login import UserMixin # type: ignore @@ -85,21 +86,23 @@ class Account(UserMixin, Base): __table_args__ = (db.PrimaryKeyConstraint("id", name="account_pkey"), db.Index("account_email_idx", "email")) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) - name = db.Column(db.String(255), nullable=False) - email = db.Column(db.String(255), nullable=False) - password = db.Column(db.String(255), nullable=True) - password_salt = db.Column(db.String(255), nullable=True) - avatar = db.Column(db.String(255)) - interface_language = db.Column(db.String(255)) - interface_theme = db.Column(db.String(255)) - timezone = db.Column(db.String(255)) - last_login_at = db.Column(db.DateTime) - last_login_ip = db.Column(db.String(255)) - last_active_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - status = db.Column(db.String(16), nullable=False, server_default=db.text("'active'::character varying")) - initialized_at = db.Column(db.DateTime) - 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()) + name: Mapped[str] = mapped_column(db.String(255)) + email: Mapped[str] = mapped_column(db.String(255)) + password: Mapped[Optional[str]] = mapped_column(db.String(255)) + password_salt: Mapped[Optional[str]] = mapped_column(db.String(255)) + avatar: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) + interface_language: Mapped[Optional[str]] = mapped_column(db.String(255)) + interface_theme: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) + timezone: Mapped[Optional[str]] = mapped_column(db.String(255)) + last_login_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) + last_login_ip: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) + last_active_at: Mapped[datetime] = mapped_column( + db.DateTime, server_default=func.current_timestamp(), nullable=False + ) + status: Mapped[str] = mapped_column(db.String(16), server_default=db.text("'active'::character varying")) + initialized_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) + created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False) + updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False) @reconstructor def init_on_load(self): @@ -143,7 +146,7 @@ class Account(UserMixin, Base): return tenant, join = tenant_account_join - self.role = join.role + self.role = TenantAccountRole(join.role) self._current_tenant = tenant @property @@ -196,14 +199,14 @@ class Tenant(Base): __tablename__ = "tenants" __table_args__ = (db.PrimaryKeyConstraint("id", name="tenant_pkey"),) - id: Mapped[str] = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - name = db.Column(db.String(255), nullable=False) + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + name: Mapped[str] = mapped_column(db.String(255)) encrypt_public_key = db.Column(db.Text) - plan = db.Column(db.String(255), nullable=False, server_default=db.text("'basic'::character varying")) - status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) - custom_config = db.Column(db.Text) - 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()) + plan: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'basic'::character varying")) + status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'normal'::character varying")) + custom_config: Mapped[Optional[str]] = mapped_column(db.Text) + created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False) + updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) def get_accounts(self) -> list[Account]: return ( @@ -230,14 +233,14 @@ class TenantAccountJoin(Base): db.UniqueConstraint("tenant_id", "account_id", name="unique_tenant_account_join"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - account_id = db.Column(StringUUID, nullable=False) - current = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - role = db.Column(db.String(16), nullable=False, server_default="normal") - invited_by = db.Column(StringUUID, nullable=True) - 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()) + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID) + account_id: Mapped[str] = mapped_column(StringUUID) + current: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false")) + role: Mapped[str] = mapped_column(db.String(16), server_default="normal") + invited_by: Mapped[Optional[str]] = mapped_column(StringUUID) + created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) class AccountIntegrate(Base): @@ -248,13 +251,13 @@ class AccountIntegrate(Base): db.UniqueConstraint("provider", "open_id", name="unique_provider_open_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - account_id = db.Column(StringUUID, nullable=False) - provider = db.Column(db.String(16), nullable=False) - open_id = db.Column(db.String(255), nullable=False) - encrypted_token = db.Column(db.String(255), nullable=False) - 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()) + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + account_id: Mapped[str] = mapped_column(StringUUID) + provider: Mapped[str] = mapped_column(db.String(16)) + open_id: Mapped[str] = mapped_column(db.String(255)) + encrypted_token: Mapped[str] = mapped_column(db.String(255)) + created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) class InvitationCode(Base): @@ -265,15 +268,15 @@ class InvitationCode(Base): db.Index("invitation_codes_code_idx", "code", "status"), ) - id = db.Column(db.Integer, nullable=False) - batch = db.Column(db.String(255), nullable=False) - code = db.Column(db.String(32), nullable=False) - status = db.Column(db.String(16), nullable=False, server_default=db.text("'unused'::character varying")) - used_at = db.Column(db.DateTime) - used_by_tenant_id = db.Column(StringUUID) - used_by_account_id = db.Column(StringUUID) - deprecated_at = db.Column(db.DateTime) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + id: Mapped[int] = mapped_column(db.Integer) + batch: Mapped[str] = mapped_column(db.String(255)) + code: Mapped[str] = mapped_column(db.String(32)) + status: Mapped[str] = mapped_column(db.String(16), server_default=db.text("'unused'::character varying")) + used_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) + used_by_tenant_id: Mapped[Optional[str]] = mapped_column(StringUUID) + used_by_account_id: Mapped[Optional[str]] = mapped_column(StringUUID) + deprecated_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) + created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=db.text("CURRENT_TIMESTAMP(0)")) class TenantPluginPermission(Base): @@ -299,3 +302,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/api_based_extension.py b/api/models/api_based_extension.py index 5a70e18622..3cef5a0fb2 100644 --- a/api/models/api_based_extension.py +++ b/api/models/api_based_extension.py @@ -1,6 +1,7 @@ import enum from sqlalchemy import func +from sqlalchemy.orm import mapped_column from .base import Base from .engine import db @@ -21,9 +22,9 @@ class APIBasedExtension(Base): db.Index("api_based_extension_tenant_idx", "tenant_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - name = db.Column(db.String(255), nullable=False) - api_endpoint = db.Column(db.String(255), nullable=False) - api_key = db.Column(db.Text, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + name = mapped_column(db.String(255), nullable=False) + api_endpoint = mapped_column(db.String(255), nullable=False) + api_key = mapped_column(db.Text, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) diff --git a/api/models/dataset.py b/api/models/dataset.py index 1ec27203a0..d5a13efb90 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -8,12 +8,13 @@ import os import pickle import re import time +from datetime import datetime from json import JSONDecodeError -from typing import Any, cast +from typing import Any, Optional, cast from sqlalchemy import func from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.orm import Mapped +from sqlalchemy.orm import Mapped, mapped_column from configs import dify_config from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource @@ -45,24 +46,24 @@ class Dataset(Base): INDEXING_TECHNIQUE_LIST = ["high_quality", "economy", None] PROVIDER_LIST = ["vendor", "external", None] - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - name = db.Column(db.String(255), nullable=False) - description = db.Column(db.Text, nullable=True) - provider = db.Column(db.String(255), nullable=False, server_default=db.text("'vendor'::character varying")) - permission = db.Column(db.String(255), nullable=False, server_default=db.text("'only_me'::character varying")) - data_source_type = db.Column(db.String(255)) - indexing_technique = db.Column(db.String(255), nullable=True) - index_struct = db.Column(db.Text, nullable=True) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_by = db.Column(StringUUID, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - embedding_model = db.Column(db.String(255), nullable=True) - embedding_model_provider = db.Column(db.String(255), nullable=True) - collection_binding_id = db.Column(StringUUID, nullable=True) - retrieval_model = db.Column(JSONB, nullable=True) - built_in_field_enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID) + name: Mapped[str] = mapped_column(db.String(255)) + description = mapped_column(db.Text, nullable=True) + provider: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'vendor'::character varying")) + permission: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'only_me'::character varying")) + data_source_type = mapped_column(db.String(255)) + indexing_technique: Mapped[Optional[str]] = mapped_column(db.String(255)) + index_struct = mapped_column(db.Text, nullable=True) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_by = mapped_column(StringUUID, nullable=True) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + embedding_model = db.Column(db.String(255), nullable=True) # TODO: mapped_column + embedding_model_provider = db.Column(db.String(255), nullable=True) # TODO: mapped_column + collection_binding_id = mapped_column(StringUUID, nullable=True) + retrieval_model = mapped_column(JSONB, nullable=True) + built_in_field_enabled = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) @property def dataset_keyword_table(self): @@ -255,7 +256,7 @@ class Dataset(Base): @staticmethod def gen_collection_name_by_id(dataset_id: str) -> str: normalized_dataset_id = dataset_id.replace("-", "_") - return f"Vector_index_{normalized_dataset_id}_Node" + return f"{dify_config.VECTOR_INDEX_NAME_PREFIX}_{normalized_dataset_id}_Node" class DatasetProcessRule(Base): @@ -265,12 +266,12 @@ class DatasetProcessRule(Base): db.Index("dataset_process_rule_dataset_id_idx", "dataset_id"), ) - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - dataset_id = db.Column(StringUUID, nullable=False) - mode = db.Column(db.String(255), nullable=False, server_default=db.text("'automatic'::character varying")) - rules = db.Column(db.Text, nullable=True) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + dataset_id = mapped_column(StringUUID, nullable=False) + mode = mapped_column(db.String(255), nullable=False, server_default=db.text("'automatic'::character varying")) + rules = mapped_column(db.Text, nullable=True) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) MODES = ["automatic", "custom", "hierarchical"] PRE_PROCESSING_RULES = ["remove_stopwords", "remove_extra_spaces", "remove_urls_emails"] @@ -309,62 +310,64 @@ class Document(Base): ) # initial fields - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - position = db.Column(db.Integer, nullable=False) - data_source_type = db.Column(db.String(255), nullable=False) - data_source_info = db.Column(db.Text, nullable=True) - dataset_process_rule_id = db.Column(StringUUID, nullable=True) - batch = db.Column(db.String(255), nullable=False) - name = db.Column(db.String(255), nullable=False) - created_from = db.Column(db.String(255), nullable=False) - created_by = db.Column(StringUUID, nullable=False) - created_api_request_id = db.Column(StringUUID, nullable=True) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + position = mapped_column(db.Integer, nullable=False) + data_source_type = mapped_column(db.String(255), nullable=False) + data_source_info = mapped_column(db.Text, nullable=True) + dataset_process_rule_id = mapped_column(StringUUID, nullable=True) + batch = mapped_column(db.String(255), nullable=False) + name = mapped_column(db.String(255), nullable=False) + created_from = mapped_column(db.String(255), nullable=False) + created_by = mapped_column(StringUUID, nullable=False) + created_api_request_id = mapped_column(StringUUID, nullable=True) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) # start processing - processing_started_at = db.Column(db.DateTime, nullable=True) + processing_started_at = mapped_column(db.DateTime, nullable=True) # parsing - file_id = db.Column(db.Text, nullable=True) - word_count = db.Column(db.Integer, nullable=True) - parsing_completed_at = db.Column(db.DateTime, nullable=True) + file_id = mapped_column(db.Text, nullable=True) + word_count = mapped_column(db.Integer, nullable=True) + parsing_completed_at = mapped_column(db.DateTime, nullable=True) # cleaning - cleaning_completed_at = db.Column(db.DateTime, nullable=True) + cleaning_completed_at = mapped_column(db.DateTime, nullable=True) # split - splitting_completed_at = db.Column(db.DateTime, nullable=True) + splitting_completed_at = mapped_column(db.DateTime, nullable=True) # indexing - tokens = db.Column(db.Integer, nullable=True) - indexing_latency = db.Column(db.Float, nullable=True) - completed_at = db.Column(db.DateTime, nullable=True) + tokens = mapped_column(db.Integer, nullable=True) + indexing_latency = mapped_column(db.Float, nullable=True) + completed_at = mapped_column(db.DateTime, nullable=True) # pause - is_paused = db.Column(db.Boolean, nullable=True, server_default=db.text("false")) - paused_by = db.Column(StringUUID, nullable=True) - paused_at = db.Column(db.DateTime, nullable=True) + is_paused = mapped_column(db.Boolean, nullable=True, server_default=db.text("false")) + paused_by = mapped_column(StringUUID, nullable=True) + paused_at = mapped_column(db.DateTime, nullable=True) # error - error = db.Column(db.Text, nullable=True) - stopped_at = db.Column(db.DateTime, nullable=True) + error = mapped_column(db.Text, nullable=True) + stopped_at = mapped_column(db.DateTime, nullable=True) # basic fields - indexing_status = db.Column(db.String(255), nullable=False, server_default=db.text("'waiting'::character varying")) - enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) - disabled_at = db.Column(db.DateTime, nullable=True) - disabled_by = db.Column(StringUUID, nullable=True) - archived = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - archived_reason = db.Column(db.String(255), nullable=True) - archived_by = db.Column(StringUUID, nullable=True) - archived_at = db.Column(db.DateTime, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - doc_type = db.Column(db.String(40), nullable=True) - doc_metadata = db.Column(JSONB, nullable=True) - doc_form = db.Column(db.String(255), nullable=False, server_default=db.text("'text_model'::character varying")) - doc_language = db.Column(db.String(255), nullable=True) + indexing_status = mapped_column( + db.String(255), nullable=False, server_default=db.text("'waiting'::character varying") + ) + enabled = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) + disabled_at = mapped_column(db.DateTime, nullable=True) + disabled_by = mapped_column(StringUUID, nullable=True) + archived = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + archived_reason = mapped_column(db.String(255), nullable=True) + archived_by = mapped_column(StringUUID, nullable=True) + archived_at = mapped_column(db.DateTime, nullable=True) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + doc_type = mapped_column(db.String(40), nullable=True) + doc_metadata = mapped_column(JSONB, nullable=True) + doc_form = mapped_column(db.String(255), nullable=False, server_default=db.text("'text_model'::character varying")) + doc_language = mapped_column(db.String(255), nullable=True) DATA_SOURCES = ["upload_file", "notion_import", "website_crawl"] @@ -652,35 +655,35 @@ class DocumentSegment(Base): ) # initial fields - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - document_id = db.Column(StringUUID, nullable=False) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + document_id = mapped_column(StringUUID, nullable=False) position: Mapped[int] - content = db.Column(db.Text, nullable=False) - answer = db.Column(db.Text, nullable=True) - word_count = db.Column(db.Integer, nullable=False) - tokens = db.Column(db.Integer, nullable=False) + content = mapped_column(db.Text, nullable=False) + answer = mapped_column(db.Text, nullable=True) + word_count: Mapped[int] + tokens: Mapped[int] # indexing fields - keywords = db.Column(db.JSON, nullable=True) - index_node_id = db.Column(db.String(255), nullable=True) - index_node_hash = db.Column(db.String(255), nullable=True) + keywords = mapped_column(db.JSON, nullable=True) + index_node_id = mapped_column(db.String(255), nullable=True) + index_node_hash = mapped_column(db.String(255), nullable=True) # basic fields - hit_count = db.Column(db.Integer, nullable=False, default=0) - enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) - disabled_at = db.Column(db.DateTime, nullable=True) - disabled_by = db.Column(StringUUID, nullable=True) - status = db.Column(db.String(255), nullable=False, server_default=db.text("'waiting'::character varying")) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_by = db.Column(StringUUID, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - indexing_at = db.Column(db.DateTime, nullable=True) - completed_at = db.Column(db.DateTime, nullable=True) - error = db.Column(db.Text, nullable=True) - stopped_at = db.Column(db.DateTime, nullable=True) + hit_count = mapped_column(db.Integer, nullable=False, default=0) + enabled = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) + disabled_at = mapped_column(db.DateTime, nullable=True) + disabled_by = mapped_column(StringUUID, nullable=True) + status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'waiting'::character varying")) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_by = mapped_column(StringUUID, nullable=True) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + indexing_at = mapped_column(db.DateTime, nullable=True) + completed_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) + error = mapped_column(db.Text, nullable=True) + stopped_at = mapped_column(db.DateTime, nullable=True) @property def dataset(self): @@ -800,25 +803,25 @@ class ChildChunk(Base): ) # initial fields - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - document_id = db.Column(StringUUID, nullable=False) - segment_id = db.Column(StringUUID, nullable=False) - position = db.Column(db.Integer, nullable=False) - content = db.Column(db.Text, nullable=False) - word_count = db.Column(db.Integer, nullable=False) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + document_id = mapped_column(StringUUID, nullable=False) + segment_id = mapped_column(StringUUID, nullable=False) + position = mapped_column(db.Integer, nullable=False) + content = mapped_column(db.Text, nullable=False) + word_count = mapped_column(db.Integer, nullable=False) # indexing fields - index_node_id = db.Column(db.String(255), nullable=True) - index_node_hash = db.Column(db.String(255), nullable=True) - type = db.Column(db.String(255), nullable=False, server_default=db.text("'automatic'::character varying")) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - updated_by = db.Column(StringUUID, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - indexing_at = db.Column(db.DateTime, nullable=True) - completed_at = db.Column(db.DateTime, nullable=True) - error = db.Column(db.Text, nullable=True) + index_node_id = mapped_column(db.String(255), nullable=True) + index_node_hash = mapped_column(db.String(255), nullable=True) + type = mapped_column(db.String(255), nullable=False, server_default=db.text("'automatic'::character varying")) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + updated_by = mapped_column(StringUUID, nullable=True) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + indexing_at = mapped_column(db.DateTime, nullable=True) + completed_at = mapped_column(db.DateTime, nullable=True) + error = mapped_column(db.Text, nullable=True) @property def dataset(self): @@ -840,10 +843,10 @@ class AppDatasetJoin(Base): db.Index("app_dataset_join_app_dataset_idx", "dataset_id", "app_id"), ) - id = db.Column(StringUUID, primary_key=True, nullable=False, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) @property def app(self): @@ -857,14 +860,14 @@ class DatasetQuery(Base): db.Index("dataset_query_dataset_id_idx", "dataset_id"), ) - id = db.Column(StringUUID, primary_key=True, nullable=False, server_default=db.text("uuid_generate_v4()")) - dataset_id = db.Column(StringUUID, nullable=False) - content = db.Column(db.Text, nullable=False) - source = db.Column(db.String(255), nullable=False) - source_app_id = db.Column(StringUUID, nullable=True) - created_by_role = db.Column(db.String, nullable=False) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=db.text("uuid_generate_v4()")) + dataset_id = mapped_column(StringUUID, nullable=False) + content = mapped_column(db.Text, nullable=False) + source = mapped_column(db.String(255), nullable=False) + source_app_id = mapped_column(StringUUID, nullable=True) + created_by_role = mapped_column(db.String, nullable=False) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) class DatasetKeywordTable(Base): @@ -874,10 +877,10 @@ class DatasetKeywordTable(Base): db.Index("dataset_keyword_table_dataset_id_idx", "dataset_id"), ) - id = db.Column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) - dataset_id = db.Column(StringUUID, nullable=False, unique=True) - keyword_table = db.Column(db.Text, nullable=False) - data_source_type = db.Column( + id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + dataset_id = mapped_column(StringUUID, nullable=False, unique=True) + keyword_table = mapped_column(db.Text, nullable=False) + data_source_type = mapped_column( db.String(255), nullable=False, server_default=db.text("'database'::character varying") ) @@ -920,14 +923,14 @@ class Embedding(Base): db.Index("created_at_idx", "created_at"), ) - id = db.Column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) - model_name = db.Column( + id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + model_name = mapped_column( db.String(255), nullable=False, server_default=db.text("'text-embedding-ada-002'::character varying") ) - hash = db.Column(db.String(64), nullable=False) - embedding = db.Column(db.LargeBinary, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - provider_name = db.Column(db.String(255), nullable=False, server_default=db.text("''::character varying")) + hash = mapped_column(db.String(64), nullable=False) + embedding = mapped_column(db.LargeBinary, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + provider_name = mapped_column(db.String(255), nullable=False, server_default=db.text("''::character varying")) def set_embedding(self, embedding_data: list[float]): self.embedding = pickle.dumps(embedding_data, protocol=pickle.HIGHEST_PROTOCOL) @@ -943,12 +946,12 @@ class DatasetCollectionBinding(Base): db.Index("provider_model_name_idx", "provider_name", "model_name"), ) - id = db.Column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) - provider_name = db.Column(db.String(255), nullable=False) - model_name = db.Column(db.String(255), nullable=False) - type = db.Column(db.String(40), server_default=db.text("'dataset'::character varying"), nullable=False) - collection_name = db.Column(db.String(64), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + provider_name = mapped_column(db.String(255), nullable=False) + model_name = mapped_column(db.String(255), nullable=False) + type = mapped_column(db.String(40), server_default=db.text("'dataset'::character varying"), nullable=False) + collection_name = mapped_column(db.String(64), nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class TidbAuthBinding(Base): @@ -960,15 +963,15 @@ class TidbAuthBinding(Base): db.Index("tidb_auth_bindings_created_at_idx", "created_at"), db.Index("tidb_auth_bindings_status_idx", "status"), ) - id = db.Column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=True) - cluster_id = db.Column(db.String(255), nullable=False) - cluster_name = db.Column(db.String(255), nullable=False) - active = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - status = db.Column(db.String(255), nullable=False, server_default=db.text("CREATING")) - account = db.Column(db.String(255), nullable=False) - password = db.Column(db.String(255), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=True) + cluster_id = mapped_column(db.String(255), nullable=False) + cluster_name = mapped_column(db.String(255), nullable=False) + active = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + status = mapped_column(db.String(255), nullable=False, server_default=db.text("CREATING")) + account = mapped_column(db.String(255), nullable=False) + password = mapped_column(db.String(255), nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class Whitelist(Base): @@ -977,10 +980,10 @@ class Whitelist(Base): db.PrimaryKeyConstraint("id", name="whitelists_pkey"), db.Index("whitelists_tenant_idx", "tenant_id"), ) - id = db.Column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=True) - category = db.Column(db.String(255), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=True) + category = mapped_column(db.String(255), nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class DatasetPermission(Base): @@ -992,12 +995,12 @@ class DatasetPermission(Base): db.Index("idx_dataset_permissions_tenant_id", "tenant_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"), primary_key=True) - dataset_id = db.Column(StringUUID, nullable=False) - account_id = db.Column(StringUUID, nullable=False) - tenant_id = db.Column(StringUUID, nullable=False) - has_permission = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"), primary_key=True) + dataset_id = mapped_column(StringUUID, nullable=False) + account_id = mapped_column(StringUUID, nullable=False) + tenant_id = mapped_column(StringUUID, nullable=False) + has_permission = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class ExternalKnowledgeApis(Base): @@ -1008,15 +1011,15 @@ class ExternalKnowledgeApis(Base): db.Index("external_knowledge_apis_name_idx", "name"), ) - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - name = db.Column(db.String(255), nullable=False) - description = db.Column(db.String(255), nullable=False) - tenant_id = db.Column(StringUUID, nullable=False) - settings = db.Column(db.Text, nullable=True) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_by = db.Column(StringUUID, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + name = mapped_column(db.String(255), nullable=False) + description = mapped_column(db.String(255), nullable=False) + tenant_id = mapped_column(StringUUID, nullable=False) + settings = mapped_column(db.Text, nullable=True) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_by = mapped_column(StringUUID, nullable=True) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) def to_dict(self): return { @@ -1063,15 +1066,15 @@ class ExternalKnowledgeBindings(Base): db.Index("external_knowledge_bindings_external_knowledge_api_idx", "external_knowledge_api_id"), ) - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - external_knowledge_api_id = db.Column(StringUUID, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - external_knowledge_id = db.Column(db.Text, nullable=False) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_by = db.Column(StringUUID, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + external_knowledge_api_id = mapped_column(StringUUID, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + external_knowledge_id = mapped_column(db.Text, nullable=False) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_by = mapped_column(StringUUID, nullable=True) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class DatasetAutoDisableLog(Base): @@ -1083,12 +1086,12 @@ class DatasetAutoDisableLog(Base): db.Index("dataset_auto_disable_log_created_atx", "created_at"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - document_id = db.Column(StringUUID, nullable=False) - notified = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + document_id = mapped_column(StringUUID, nullable=False) + notified = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) class RateLimitLog(Base): @@ -1099,11 +1102,11 @@ class RateLimitLog(Base): db.Index("rate_limit_log_operation_idx", "operation"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - subscription_plan = db.Column(db.String(255), nullable=False) - operation = db.Column(db.String(255), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + subscription_plan = mapped_column(db.String(255), nullable=False) + operation = mapped_column(db.String(255), nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) class DatasetMetadata(Base): @@ -1114,15 +1117,15 @@ class DatasetMetadata(Base): db.Index("dataset_metadata_dataset_idx", "dataset_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - type = db.Column(db.String(255), nullable=False) - name = db.Column(db.String(255), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - created_by = db.Column(StringUUID, nullable=False) - updated_by = db.Column(StringUUID, nullable=True) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + type = mapped_column(db.String(255), nullable=False) + name = mapped_column(db.String(255), nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + created_by = mapped_column(StringUUID, nullable=False) + updated_by = mapped_column(StringUUID, nullable=True) class DatasetMetadataBinding(Base): @@ -1135,10 +1138,10 @@ class DatasetMetadataBinding(Base): db.Index("dataset_metadata_binding_document_idx", "document_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - metadata_id = db.Column(StringUUID, nullable=False) - document_id = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - created_by = db.Column(StringUUID, nullable=False) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + metadata_id = mapped_column(StringUUID, nullable=False) + document_id = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_by = mapped_column(StringUUID, nullable=False) diff --git a/api/models/model.py b/api/models/model.py index 2377aeed8a..b8e8b78018 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -40,8 +40,8 @@ class DifySetup(Base): __tablename__ = "dify_setups" __table_args__ = (db.PrimaryKeyConstraint("version", name="dify_setup_pkey"),) - version = db.Column(db.String(255), nullable=False) - setup_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + version = mapped_column(db.String(255), nullable=False) + setup_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class AppMode(StrEnum): @@ -74,31 +74,31 @@ class App(Base): __tablename__ = "apps" __table_args__ = (db.PrimaryKeyConstraint("id", name="app_pkey"), db.Index("app_tenant_id_idx", "tenant_id")) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id: Mapped[str] = db.Column(StringUUID, nullable=False) - name = db.Column(db.String(255), nullable=False) - description = db.Column(db.Text, nullable=False, server_default=db.text("''::character varying")) - mode: Mapped[str] = mapped_column(db.String(255), nullable=False) - icon_type = db.Column(db.String(255), nullable=True) # image, emoji + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID) + name: Mapped[str] = mapped_column(db.String(255)) + description: Mapped[str] = mapped_column(db.Text, server_default=db.text("''::character varying")) + mode: Mapped[str] = mapped_column(db.String(255)) + icon_type: Mapped[Optional[str]] = mapped_column(db.String(255)) # image, emoji icon = db.Column(db.String(255)) - icon_background = db.Column(db.String(255)) - app_model_config_id = db.Column(StringUUID, nullable=True) - workflow_id = db.Column(StringUUID, nullable=True) - status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) - enable_site = db.Column(db.Boolean, nullable=False) - enable_api = db.Column(db.Boolean, nullable=False) - api_rpm = db.Column(db.Integer, nullable=False, server_default=db.text("0")) - api_rph = db.Column(db.Integer, nullable=False, server_default=db.text("0")) - is_demo = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - is_public = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - is_universal = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - tracing = db.Column(db.Text, nullable=True) - max_active_requests: Mapped[Optional[int]] = mapped_column(nullable=True) - created_by = db.Column(StringUUID, nullable=True) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_by = db.Column(StringUUID, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) + icon_background: Mapped[Optional[str]] = mapped_column(db.String(255)) + app_model_config_id = mapped_column(StringUUID, nullable=True) + workflow_id = mapped_column(StringUUID, nullable=True) + status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'normal'::character varying")) + enable_site: Mapped[bool] = mapped_column(db.Boolean) + enable_api: Mapped[bool] = mapped_column(db.Boolean) + api_rpm: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0")) + api_rph: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0")) + is_demo: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false")) + is_public: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false")) + is_universal: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false")) + tracing = mapped_column(db.Text, nullable=True) + max_active_requests: Mapped[Optional[int]] + created_by = mapped_column(StringUUID, nullable=True) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_by = mapped_column(StringUUID, nullable=True) + updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + use_icon_as_answer_icon: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) @property def desc_or_prompt(self): @@ -307,34 +307,34 @@ class AppModelConfig(Base): __tablename__ = "app_model_configs" __table_args__ = (db.PrimaryKeyConstraint("id", name="app_model_config_pkey"), db.Index("app_app_id_idx", "app_id")) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - provider = db.Column(db.String(255), nullable=True) - model_id = db.Column(db.String(255), nullable=True) - configs = db.Column(db.JSON, nullable=True) - created_by = db.Column(StringUUID, nullable=True) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_by = db.Column(StringUUID, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - opening_statement = db.Column(db.Text) - suggested_questions = db.Column(db.Text) - suggested_questions_after_answer = db.Column(db.Text) - speech_to_text = db.Column(db.Text) - text_to_speech = db.Column(db.Text) - more_like_this = db.Column(db.Text) - model = db.Column(db.Text) - user_input_form = db.Column(db.Text) - dataset_query_variable = db.Column(db.String(255)) - pre_prompt = db.Column(db.Text) - agent_mode = db.Column(db.Text) - sensitive_word_avoidance = db.Column(db.Text) - retriever_resource = db.Column(db.Text) - prompt_type = db.Column(db.String(255), nullable=False, server_default=db.text("'simple'::character varying")) - chat_prompt_config = db.Column(db.Text) - completion_prompt_config = db.Column(db.Text) - dataset_configs = db.Column(db.Text) - external_data_tools = db.Column(db.Text) - file_upload = db.Column(db.Text) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + provider = mapped_column(db.String(255), nullable=True) + model_id = mapped_column(db.String(255), nullable=True) + configs = mapped_column(db.JSON, nullable=True) + created_by = mapped_column(StringUUID, nullable=True) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_by = mapped_column(StringUUID, nullable=True) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + opening_statement = mapped_column(db.Text) + suggested_questions = mapped_column(db.Text) + suggested_questions_after_answer = mapped_column(db.Text) + speech_to_text = mapped_column(db.Text) + text_to_speech = mapped_column(db.Text) + more_like_this = mapped_column(db.Text) + model = mapped_column(db.Text) + user_input_form = mapped_column(db.Text) + dataset_query_variable = mapped_column(db.String(255)) + pre_prompt = mapped_column(db.Text) + agent_mode = mapped_column(db.Text) + sensitive_word_avoidance = mapped_column(db.Text) + retriever_resource = mapped_column(db.Text) + prompt_type = mapped_column(db.String(255), nullable=False, server_default=db.text("'simple'::character varying")) + chat_prompt_config = mapped_column(db.Text) + completion_prompt_config = mapped_column(db.Text) + dataset_configs = mapped_column(db.Text) + external_data_tools = mapped_column(db.Text) + file_upload = mapped_column(db.Text) @property def app(self): @@ -561,19 +561,19 @@ class RecommendedApp(Base): db.Index("recommended_app_is_listed_idx", "is_listed", "language"), ) - id = db.Column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - description = db.Column(db.JSON, nullable=False) - copyright = db.Column(db.String(255), nullable=False) - privacy_policy = db.Column(db.String(255), nullable=False) + id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + description = mapped_column(db.JSON, nullable=False) + copyright = mapped_column(db.String(255), nullable=False) + privacy_policy = mapped_column(db.String(255), nullable=False) custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="") - category = db.Column(db.String(255), nullable=False) - position = db.Column(db.Integer, nullable=False, default=0) - is_listed = db.Column(db.Boolean, nullable=False, default=True) - install_count = db.Column(db.Integer, nullable=False, default=0) - language = db.Column(db.String(255), nullable=False, server_default=db.text("'en-US'::character varying")) - 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()) + category = mapped_column(db.String(255), nullable=False) + position = mapped_column(db.Integer, nullable=False, default=0) + is_listed = mapped_column(db.Boolean, nullable=False, default=True) + install_count = mapped_column(db.Integer, nullable=False, default=0) + language = mapped_column(db.String(255), nullable=False, server_default=db.text("'en-US'::character varying")) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @property def app(self): @@ -590,14 +590,14 @@ class InstalledApp(Base): db.UniqueConstraint("tenant_id", "app_id", name="unique_tenant_app"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - app_id = db.Column(StringUUID, nullable=False) - app_owner_tenant_id = db.Column(StringUUID, nullable=False) - position = db.Column(db.Integer, nullable=False, default=0) - is_pinned = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - last_used_at = db.Column(db.DateTime, nullable=True) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + app_id = mapped_column(StringUUID, nullable=False) + app_owner_tenant_id = mapped_column(StringUUID, nullable=False) + position = mapped_column(db.Integer, nullable=False, default=0) + is_pinned = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + last_used_at = mapped_column(db.DateTime, nullable=True) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @property def app(self): @@ -618,42 +618,42 @@ class Conversation(Base): ) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - app_model_config_id = db.Column(StringUUID, nullable=True) - model_provider = db.Column(db.String(255), nullable=True) - override_model_configs = db.Column(db.Text) - model_id = db.Column(db.String(255), nullable=True) + app_id = mapped_column(StringUUID, nullable=False) + app_model_config_id = mapped_column(StringUUID, nullable=True) + model_provider = mapped_column(db.String(255), nullable=True) + override_model_configs = mapped_column(db.Text) + model_id = mapped_column(db.String(255), nullable=True) mode: Mapped[str] = mapped_column(db.String(255)) - name = db.Column(db.String(255), nullable=False) - summary = db.Column(db.Text) + name = mapped_column(db.String(255), nullable=False) + summary = mapped_column(db.Text) _inputs: Mapped[dict] = mapped_column("inputs", db.JSON) - introduction = db.Column(db.Text) - system_instruction = db.Column(db.Text) - system_instruction_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0")) - status = db.Column(db.String(255), nullable=False) + introduction = mapped_column(db.Text) + system_instruction = mapped_column(db.Text) + system_instruction_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) + status = mapped_column(db.String(255), nullable=False) # The `invoke_from` records how the conversation is created. # # Its value corresponds to the members of `InvokeFrom`. # (api/core/app/entities/app_invoke_entities.py) - invoke_from = db.Column(db.String(255), nullable=True) + invoke_from = mapped_column(db.String(255), nullable=True) # ref: ConversationSource. - from_source = db.Column(db.String(255), nullable=False) - from_end_user_id = db.Column(StringUUID) - from_account_id = db.Column(StringUUID) - read_at = db.Column(db.DateTime) - read_account_id = db.Column(StringUUID) + from_source = mapped_column(db.String(255), nullable=False) + from_end_user_id = mapped_column(StringUUID) + from_account_id = mapped_column(StringUUID) + read_at = mapped_column(db.DateTime) + read_account_id = mapped_column(StringUUID) dialogue_count: Mapped[int] = mapped_column(default=0) - 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()) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) messages = db.relationship("Message", backref="conversation", lazy="select", passive_deletes="all") message_annotations = db.relationship( "MessageAnnotation", backref="conversation", lazy="select", passive_deletes="all" ) - is_deleted = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) + is_deleted = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) @property def inputs(self): @@ -896,36 +896,36 @@ class Message(Base): ) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - model_provider = db.Column(db.String(255), nullable=True) - model_id = db.Column(db.String(255), nullable=True) - override_model_configs = db.Column(db.Text) - conversation_id = db.Column(StringUUID, db.ForeignKey("conversations.id"), nullable=False) + app_id = mapped_column(StringUUID, nullable=False) + model_provider = mapped_column(db.String(255), nullable=True) + model_id = mapped_column(db.String(255), nullable=True) + override_model_configs = mapped_column(db.Text) + conversation_id = mapped_column(StringUUID, db.ForeignKey("conversations.id"), nullable=False) _inputs: Mapped[dict] = mapped_column("inputs", db.JSON) - query: Mapped[str] = db.Column(db.Text, nullable=False) - message = db.Column(db.JSON, nullable=False) - message_tokens: Mapped[int] = db.Column(db.Integer, nullable=False, server_default=db.text("0")) - message_unit_price = db.Column(db.Numeric(10, 4), nullable=False) - message_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - answer: Mapped[str] = db.Column(db.Text, nullable=False) - answer_tokens: Mapped[int] = db.Column(db.Integer, nullable=False, server_default=db.text("0")) - answer_unit_price = db.Column(db.Numeric(10, 4), nullable=False) - answer_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - parent_message_id = db.Column(StringUUID, nullable=True) - provider_response_latency = db.Column(db.Float, nullable=False, server_default=db.text("0")) - total_price = db.Column(db.Numeric(10, 7)) - currency = db.Column(db.String(255), nullable=False) - status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) - error = db.Column(db.Text) - message_metadata = db.Column(db.Text) - invoke_from: Mapped[Optional[str]] = db.Column(db.String(255), nullable=True) - from_source = db.Column(db.String(255), nullable=False) - from_end_user_id: Mapped[Optional[str]] = db.Column(StringUUID) - from_account_id: Mapped[Optional[str]] = db.Column(StringUUID) + query: Mapped[str] = mapped_column(db.Text, nullable=False) + message = mapped_column(db.JSON, nullable=False) + message_tokens: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) + message_unit_price = mapped_column(db.Numeric(10, 4), nullable=False) + message_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) + answer: Mapped[str] = db.Column(db.Text, nullable=False) # TODO make it mapped_column + answer_tokens: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) + answer_unit_price = mapped_column(db.Numeric(10, 4), nullable=False) + answer_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) + parent_message_id = mapped_column(StringUUID, nullable=True) + provider_response_latency = mapped_column(db.Float, nullable=False, server_default=db.text("0")) + total_price = mapped_column(db.Numeric(10, 7)) + currency = mapped_column(db.String(255), nullable=False) + status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) + error = mapped_column(db.Text) + message_metadata = mapped_column(db.Text) + invoke_from: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) + from_source = mapped_column(db.String(255), nullable=False) + from_end_user_id: Mapped[Optional[str]] = mapped_column(StringUUID) + from_account_id: Mapped[Optional[str]] = mapped_column(StringUUID) created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - agent_based = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - workflow_run_id: Mapped[str] = db.Column(StringUUID) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + agent_based = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + workflow_run_id: Mapped[Optional[str]] = mapped_column(StringUUID) @property def inputs(self): @@ -1239,17 +1239,17 @@ class MessageFeedback(Base): db.Index("message_feedback_conversation_idx", "conversation_id", "from_source", "rating"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - conversation_id = db.Column(StringUUID, nullable=False) - message_id = db.Column(StringUUID, nullable=False) - rating = db.Column(db.String(255), nullable=False) - content = db.Column(db.Text) - from_source = db.Column(db.String(255), nullable=False) - from_end_user_id = db.Column(StringUUID) - from_account_id = db.Column(StringUUID) - 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()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + conversation_id = mapped_column(StringUUID, nullable=False) + message_id = mapped_column(StringUUID, nullable=False) + rating = mapped_column(db.String(255), nullable=False) + content = mapped_column(db.Text) + from_source = mapped_column(db.String(255), nullable=False) + from_end_user_id = mapped_column(StringUUID) + from_account_id = mapped_column(StringUUID) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @property def from_account(self): @@ -1301,16 +1301,16 @@ class MessageFile(Base): self.created_by_role = created_by_role.value self.created_by = created_by - id: Mapped[str] = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - message_id: Mapped[str] = db.Column(StringUUID, nullable=False) - type: Mapped[str] = db.Column(db.String(255), nullable=False) - transfer_method: Mapped[str] = db.Column(db.String(255), nullable=False) - url: Mapped[Optional[str]] = db.Column(db.Text, nullable=True) - belongs_to: Mapped[Optional[str]] = db.Column(db.String(255), nullable=True) - upload_file_id: Mapped[Optional[str]] = db.Column(StringUUID, nullable=True) - created_by_role: Mapped[str] = db.Column(db.String(255), nullable=False) - created_by: Mapped[str] = db.Column(StringUUID, nullable=False) - created_at: Mapped[datetime] = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + message_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + type: Mapped[str] = mapped_column(db.String(255), nullable=False) + transfer_method: Mapped[str] = mapped_column(db.String(255), nullable=False) + url: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True) + belongs_to: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) + upload_file_id: Mapped[Optional[str]] = mapped_column(StringUUID, nullable=True) + created_by_role: Mapped[str] = mapped_column(db.String(255), nullable=False) + created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) + created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class MessageAnnotation(Base): @@ -1322,16 +1322,16 @@ class MessageAnnotation(Base): db.Index("message_annotation_message_idx", "message_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - conversation_id = db.Column(StringUUID, db.ForeignKey("conversations.id"), nullable=True) - message_id = db.Column(StringUUID, nullable=True) + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id: Mapped[str] = mapped_column(StringUUID) + conversation_id: Mapped[Optional[str]] = mapped_column(StringUUID, db.ForeignKey("conversations.id")) + message_id: Mapped[Optional[str]] = mapped_column(StringUUID) question = db.Column(db.Text, nullable=True) - content = db.Column(db.Text, nullable=False) - hit_count = db.Column(db.Integer, nullable=False, server_default=db.text("0")) - account_id = db.Column(StringUUID, nullable=False) - 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()) + content = mapped_column(db.Text, nullable=False) + hit_count = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) + account_id = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @property def account(self): @@ -1354,17 +1354,17 @@ class AppAnnotationHitHistory(Base): db.Index("app_annotation_hit_histories_message_idx", "message_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - annotation_id: Mapped[str] = db.Column(StringUUID, nullable=False) - source = db.Column(db.Text, nullable=False) - question = db.Column(db.Text, nullable=False) - account_id = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - score = db.Column(Float, nullable=False, server_default=db.text("0")) - message_id = db.Column(StringUUID, nullable=False) - annotation_question = db.Column(db.Text, nullable=False) - annotation_content = db.Column(db.Text, nullable=False) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + annotation_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + source = mapped_column(db.Text, nullable=False) + question = mapped_column(db.Text, nullable=False) + account_id = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + score = mapped_column(Float, nullable=False, server_default=db.text("0")) + message_id = mapped_column(StringUUID, nullable=False) + annotation_question = mapped_column(db.Text, nullable=False) + annotation_content = mapped_column(db.Text, nullable=False) @property def account(self): @@ -1389,14 +1389,14 @@ class AppAnnotationSetting(Base): db.Index("app_annotation_settings_app_idx", "app_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - score_threshold = db.Column(Float, nullable=False, server_default=db.text("0")) - collection_binding_id = db.Column(StringUUID, nullable=False) - created_user_id = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_user_id = db.Column(StringUUID, nullable=False) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + score_threshold = mapped_column(Float, nullable=False, server_default=db.text("0")) + collection_binding_id = mapped_column(StringUUID, nullable=False) + created_user_id = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_user_id = mapped_column(StringUUID, nullable=False) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @property def collection_binding_detail(self): @@ -1417,14 +1417,14 @@ class OperationLog(Base): db.Index("operation_log_account_action_idx", "tenant_id", "account_id", "action"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - account_id = db.Column(StringUUID, nullable=False) - action = db.Column(db.String(255), nullable=False) - content = db.Column(db.JSON) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - created_ip = db.Column(db.String(255), nullable=False) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + account_id = mapped_column(StringUUID, nullable=False) + action = mapped_column(db.String(255), nullable=False) + content = mapped_column(db.JSON) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_ip = mapped_column(db.String(255), nullable=False) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class EndUser(Base, UserMixin): @@ -1435,16 +1435,16 @@ class EndUser(Base, UserMixin): db.Index("end_user_tenant_session_id_idx", "tenant_id", "session_id", "type"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id: Mapped[str] = db.Column(StringUUID, nullable=False) - app_id = db.Column(StringUUID, nullable=True) - type = db.Column(db.String(255), nullable=False) - external_user_id = db.Column(db.String(255), nullable=True) - name = db.Column(db.String(255)) - is_anonymous = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + app_id = mapped_column(StringUUID, nullable=True) + type = mapped_column(db.String(255), nullable=False) + external_user_id = mapped_column(db.String(255), nullable=True) + name = mapped_column(db.String(255)) + is_anonymous = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) session_id: Mapped[str] = mapped_column() - 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()) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class AppMCPServer(Base): @@ -1454,17 +1454,17 @@ class AppMCPServer(Base): db.UniqueConstraint("tenant_id", "app_id", name="unique_app_mcp_server_tenant_app_id"), db.UniqueConstraint("server_code", name="unique_app_mcp_server_server_code"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - app_id = db.Column(StringUUID, nullable=False) - name = db.Column(db.String(255), nullable=False) - description = db.Column(db.String(255), nullable=False) - server_code = db.Column(db.String(255), nullable=False) - status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) - parameters = db.Column(db.Text, nullable=False) - - 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()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + app_id = mapped_column(StringUUID, nullable=False) + name = mapped_column(db.String(255), nullable=False) + description = mapped_column(db.String(255), nullable=False) + server_code = mapped_column(db.String(255), nullable=False) + status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) + parameters = mapped_column(db.Text, nullable=False) + + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @staticmethod def generate_server_code(n): @@ -1488,30 +1488,30 @@ class Site(Base): db.Index("site_code_idx", "code", "status"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - title = db.Column(db.String(255), nullable=False) - icon_type = db.Column(db.String(255), nullable=True) - icon = db.Column(db.String(255)) - icon_background = db.Column(db.String(255)) - description = db.Column(db.Text) - default_language = db.Column(db.String(255), nullable=False) - chat_color_theme = db.Column(db.String(255)) - chat_color_theme_inverted = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - copyright = db.Column(db.String(255)) - privacy_policy = db.Column(db.String(255)) - show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) - use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + title = mapped_column(db.String(255), nullable=False) + icon_type = mapped_column(db.String(255), nullable=True) + icon = mapped_column(db.String(255)) + icon_background = mapped_column(db.String(255)) + description = mapped_column(db.Text) + default_language = mapped_column(db.String(255), nullable=False) + chat_color_theme = mapped_column(db.String(255)) + chat_color_theme_inverted = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + copyright = mapped_column(db.String(255)) + privacy_policy = mapped_column(db.String(255)) + show_workflow_steps = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) + use_icon_as_answer_icon = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) _custom_disclaimer: Mapped[str] = mapped_column("custom_disclaimer", sa.TEXT, default="") - customize_domain = db.Column(db.String(255)) - customize_token_strategy = db.Column(db.String(255), nullable=False) - prompt_public = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) - created_by = db.Column(StringUUID, nullable=True) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_by = db.Column(StringUUID, nullable=True) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - code = db.Column(db.String(255)) + customize_domain = mapped_column(db.String(255)) + customize_token_strategy = mapped_column(db.String(255), nullable=False) + prompt_public = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) + created_by = mapped_column(StringUUID, nullable=True) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_by = mapped_column(StringUUID, nullable=True) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + code = mapped_column(db.String(255)) @property def custom_disclaimer(self): @@ -1546,13 +1546,13 @@ class ApiToken(Base): db.Index("api_token_tenant_idx", "tenant_id", "type"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=True) - tenant_id = db.Column(StringUUID, nullable=True) - type = db.Column(db.String(16), nullable=False) - token = db.Column(db.String(255), nullable=False) - last_used_at = db.Column(db.DateTime, nullable=True) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=True) + tenant_id = mapped_column(StringUUID, nullable=True) + type = mapped_column(db.String(16), nullable=False) + token = mapped_column(db.String(255), nullable=False) + last_used_at = mapped_column(db.DateTime, nullable=True) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @staticmethod def generate_api_key(prefix, n): @@ -1570,23 +1570,23 @@ class UploadFile(Base): db.Index("upload_file_tenant_idx", "tenant_id"), ) - id: Mapped[str] = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id: Mapped[str] = db.Column(StringUUID, nullable=False) - storage_type: Mapped[str] = db.Column(db.String(255), nullable=False) - key: Mapped[str] = db.Column(db.String(255), nullable=False) - name: Mapped[str] = db.Column(db.String(255), nullable=False) - size: Mapped[int] = db.Column(db.Integer, nullable=False) - extension: Mapped[str] = db.Column(db.String(255), nullable=False) - mime_type: Mapped[str] = db.Column(db.String(255), nullable=True) - created_by_role: Mapped[str] = db.Column( + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + storage_type: Mapped[str] = mapped_column(db.String(255), nullable=False) + key: Mapped[str] = mapped_column(db.String(255), nullable=False) + name: Mapped[str] = mapped_column(db.String(255), nullable=False) + size: Mapped[int] = mapped_column(db.Integer, nullable=False) + extension: Mapped[str] = mapped_column(db.String(255), nullable=False) + mime_type: Mapped[str] = mapped_column(db.String(255), nullable=True) + created_by_role: Mapped[str] = mapped_column( db.String(255), nullable=False, server_default=db.text("'account'::character varying") ) - created_by: Mapped[str] = db.Column(StringUUID, nullable=False) - created_at: Mapped[datetime] = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - used: Mapped[bool] = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - used_by: Mapped[str | None] = db.Column(StringUUID, nullable=True) - used_at: Mapped[datetime | None] = db.Column(db.DateTime, nullable=True) - hash: Mapped[str | None] = db.Column(db.String(255), nullable=True) + created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) + created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + used: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + used_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True) + used_at: Mapped[datetime | None] = mapped_column(db.DateTime, nullable=True) + hash: Mapped[str | None] = mapped_column(db.String(255), nullable=True) source_url: Mapped[str] = mapped_column(sa.TEXT, default="") def __init__( @@ -1632,14 +1632,14 @@ class ApiRequest(Base): db.Index("api_request_token_idx", "tenant_id", "api_token_id"), ) - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - api_token_id = db.Column(StringUUID, nullable=False) - path = db.Column(db.String(255), nullable=False) - request = db.Column(db.Text, nullable=True) - response = db.Column(db.Text, nullable=True) - ip = db.Column(db.String(255), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + api_token_id = mapped_column(StringUUID, nullable=False) + path = mapped_column(db.String(255), nullable=False) + request = mapped_column(db.Text, nullable=True) + response = mapped_column(db.Text, nullable=True) + ip = mapped_column(db.String(255), nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class MessageChain(Base): @@ -1649,12 +1649,12 @@ class MessageChain(Base): db.Index("message_chain_message_id_idx", "message_id"), ) - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - message_id = db.Column(StringUUID, nullable=False) - type = db.Column(db.String(255), nullable=False) - input = db.Column(db.Text, nullable=True) - output = db.Column(db.Text, nullable=True) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + message_id = mapped_column(StringUUID, nullable=False) + type = mapped_column(db.String(255), nullable=False) + input = mapped_column(db.Text, nullable=True) + output = mapped_column(db.Text, nullable=True) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) class MessageAgentThought(Base): @@ -1665,34 +1665,34 @@ class MessageAgentThought(Base): db.Index("message_agent_thought_message_chain_id_idx", "message_chain_id"), ) - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - message_id = db.Column(StringUUID, nullable=False) - message_chain_id = db.Column(StringUUID, nullable=True) - position = db.Column(db.Integer, nullable=False) - thought = db.Column(db.Text, nullable=True) - tool = db.Column(db.Text, nullable=True) - tool_labels_str = db.Column(db.Text, nullable=False, server_default=db.text("'{}'::text")) - tool_meta_str = db.Column(db.Text, nullable=False, server_default=db.text("'{}'::text")) - tool_input = db.Column(db.Text, nullable=True) - observation = db.Column(db.Text, nullable=True) - # plugin_id = db.Column(StringUUID, nullable=True) ## for future design - tool_process_data = db.Column(db.Text, nullable=True) - message = db.Column(db.Text, nullable=True) - message_token = db.Column(db.Integer, nullable=True) - message_unit_price = db.Column(db.Numeric, nullable=True) - message_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - message_files = db.Column(db.Text, nullable=True) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + message_id = mapped_column(StringUUID, nullable=False) + message_chain_id = mapped_column(StringUUID, nullable=True) + position = mapped_column(db.Integer, nullable=False) + thought = mapped_column(db.Text, nullable=True) + tool = mapped_column(db.Text, nullable=True) + tool_labels_str = mapped_column(db.Text, nullable=False, server_default=db.text("'{}'::text")) + tool_meta_str = mapped_column(db.Text, nullable=False, server_default=db.text("'{}'::text")) + tool_input = mapped_column(db.Text, nullable=True) + observation = mapped_column(db.Text, nullable=True) + # plugin_id = mapped_column(StringUUID, nullable=True) ## for future design + tool_process_data = mapped_column(db.Text, nullable=True) + message = mapped_column(db.Text, nullable=True) + message_token = mapped_column(db.Integer, nullable=True) + message_unit_price = mapped_column(db.Numeric, nullable=True) + message_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) + message_files = mapped_column(db.Text, nullable=True) answer = db.Column(db.Text, nullable=True) - answer_token = db.Column(db.Integer, nullable=True) - answer_unit_price = db.Column(db.Numeric, nullable=True) - answer_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - tokens = db.Column(db.Integer, nullable=True) - total_price = db.Column(db.Numeric, nullable=True) - currency = db.Column(db.String, nullable=True) - latency = db.Column(db.Float, nullable=True) - created_by_role = db.Column(db.String, nullable=False) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + answer_token = mapped_column(db.Integer, nullable=True) + answer_unit_price = mapped_column(db.Numeric, nullable=True) + answer_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) + tokens = mapped_column(db.Integer, nullable=True) + total_price = mapped_column(db.Numeric, nullable=True) + currency = mapped_column(db.String, nullable=True) + latency = mapped_column(db.Float, nullable=True) + created_by_role = mapped_column(db.String, nullable=False) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) @property def files(self) -> list: @@ -1778,24 +1778,24 @@ class DatasetRetrieverResource(Base): db.Index("dataset_retriever_resource_message_id_idx", "message_id"), ) - id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - message_id = db.Column(StringUUID, nullable=False) - position = db.Column(db.Integer, nullable=False) - dataset_id = db.Column(StringUUID, nullable=False) - dataset_name = db.Column(db.Text, nullable=False) - document_id = db.Column(StringUUID, nullable=True) - document_name = db.Column(db.Text, nullable=False) - data_source_type = db.Column(db.Text, nullable=True) - segment_id = db.Column(StringUUID, nullable=True) - score = db.Column(db.Float, nullable=True) - content = db.Column(db.Text, nullable=False) - hit_count = db.Column(db.Integer, nullable=True) - word_count = db.Column(db.Integer, nullable=True) - segment_position = db.Column(db.Integer, nullable=True) - index_node_hash = db.Column(db.Text, nullable=True) - retriever_from = db.Column(db.Text, nullable=False) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + message_id = mapped_column(StringUUID, nullable=False) + position = mapped_column(db.Integer, nullable=False) + dataset_id = mapped_column(StringUUID, nullable=False) + dataset_name = mapped_column(db.Text, nullable=False) + document_id = mapped_column(StringUUID, nullable=True) + document_name = mapped_column(db.Text, nullable=False) + data_source_type = mapped_column(db.Text, nullable=True) + segment_id = mapped_column(StringUUID, nullable=True) + score = mapped_column(db.Float, nullable=True) + content = mapped_column(db.Text, nullable=False) + hit_count = mapped_column(db.Integer, nullable=True) + word_count = mapped_column(db.Integer, nullable=True) + segment_position = mapped_column(db.Integer, nullable=True) + index_node_hash = mapped_column(db.Text, nullable=True) + retriever_from = mapped_column(db.Text, nullable=False) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) class Tag(Base): @@ -1808,12 +1808,12 @@ class Tag(Base): TAG_TYPE_LIST = ["knowledge", "app"] - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=True) - type = db.Column(db.String(16), nullable=False) - name = db.Column(db.String(255), nullable=False) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=True) + type = mapped_column(db.String(16), nullable=False) + name = mapped_column(db.String(255), nullable=False) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class TagBinding(Base): @@ -1824,12 +1824,12 @@ class TagBinding(Base): db.Index("tag_bind_tag_id_idx", "tag_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=True) - tag_id = db.Column(StringUUID, nullable=True) - target_id = db.Column(StringUUID, nullable=True) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=True) + tag_id = mapped_column(StringUUID, nullable=True) + target_id = mapped_column(StringUUID, nullable=True) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) class TraceAppConfig(Base): @@ -1839,15 +1839,15 @@ class TraceAppConfig(Base): db.Index("trace_app_config_app_id_idx", "app_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - tracing_provider = db.Column(db.String(255), nullable=True) - tracing_config = db.Column(db.JSON, nullable=True) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = db.Column( + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + tracing_provider = mapped_column(db.String(255), nullable=True) + tracing_config = mapped_column(db.JSON, nullable=True) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column( db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() ) - is_active = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) + is_active = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) @property def tracing_config_dict(self): diff --git a/api/models/source.py b/api/models/source.py index f6e0900ae6..100e0d96ef 100644 --- a/api/models/source.py +++ b/api/models/source.py @@ -2,6 +2,7 @@ import json from sqlalchemy import func from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import mapped_column from models.base import Base @@ -17,14 +18,14 @@ class DataSourceOauthBinding(Base): db.Index("source_info_idx", "source_info", postgresql_using="gin"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - access_token = db.Column(db.String(255), nullable=False) - provider = db.Column(db.String(255), nullable=False) - source_info = db.Column(JSONB, nullable=False) - 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()) - disabled = db.Column(db.Boolean, nullable=True, server_default=db.text("false")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + access_token = mapped_column(db.String(255), nullable=False) + provider = mapped_column(db.String(255), nullable=False) + source_info = mapped_column(JSONB, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + disabled = mapped_column(db.Boolean, nullable=True, server_default=db.text("false")) class DataSourceApiKeyAuthBinding(Base): @@ -35,14 +36,14 @@ class DataSourceApiKeyAuthBinding(Base): db.Index("data_source_api_key_auth_binding_provider_idx", "provider"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - category = db.Column(db.String(255), nullable=False) - provider = db.Column(db.String(255), nullable=False) - credentials = db.Column(db.Text, nullable=True) # JSON - 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()) - disabled = db.Column(db.Boolean, nullable=True, server_default=db.text("false")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = mapped_column(StringUUID, nullable=False) + category = mapped_column(db.String(255), nullable=False) + provider = mapped_column(db.String(255), nullable=False) + credentials = mapped_column(db.Text, nullable=True) # JSON + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + disabled = mapped_column(db.Boolean, nullable=True, server_default=db.text("false")) def to_dict(self): return { diff --git a/api/models/task.py b/api/models/task.py index d853c1dd9a..3e5ebd2099 100644 --- a/api/models/task.py +++ b/api/models/task.py @@ -1,7 +1,10 @@ -from datetime import UTC, datetime +from datetime import datetime +from typing import Optional from celery import states # type: ignore +from sqlalchemy.orm import Mapped, mapped_column +from libs.datetime_utils import naive_utc_now from models.base import Base from .engine import db @@ -12,23 +15,23 @@ class CeleryTask(Base): __tablename__ = "celery_taskmeta" - id = db.Column(db.Integer, db.Sequence("task_id_sequence"), primary_key=True, autoincrement=True) - task_id = db.Column(db.String(155), unique=True) - status = db.Column(db.String(50), default=states.PENDING) - result = db.Column(db.PickleType, nullable=True) - date_done = db.Column( + id = mapped_column(db.Integer, db.Sequence("task_id_sequence"), primary_key=True, autoincrement=True) + task_id = mapped_column(db.String(155), unique=True) + status = mapped_column(db.String(50), default=states.PENDING) + result = mapped_column(db.PickleType, nullable=True) + date_done = mapped_column( db.DateTime, - default=lambda: datetime.now(UTC).replace(tzinfo=None), - onupdate=lambda: datetime.now(UTC).replace(tzinfo=None), + default=lambda: naive_utc_now(), + onupdate=lambda: naive_utc_now(), nullable=True, ) - traceback = db.Column(db.Text, nullable=True) - name = db.Column(db.String(155), nullable=True) - args = db.Column(db.LargeBinary, nullable=True) - kwargs = db.Column(db.LargeBinary, nullable=True) - worker = db.Column(db.String(155), nullable=True) - retries = db.Column(db.Integer, nullable=True) - queue = db.Column(db.String(155), nullable=True) + traceback = mapped_column(db.Text, nullable=True) + name = mapped_column(db.String(155), nullable=True) + args = mapped_column(db.LargeBinary, nullable=True) + kwargs = mapped_column(db.LargeBinary, nullable=True) + worker = mapped_column(db.String(155), nullable=True) + retries = mapped_column(db.Integer, nullable=True) + queue = mapped_column(db.String(155), nullable=True) class CeleryTaskSet(Base): @@ -36,7 +39,9 @@ class CeleryTaskSet(Base): __tablename__ = "celery_tasksetmeta" - id = db.Column(db.Integer, db.Sequence("taskset_id_sequence"), autoincrement=True, primary_key=True) - taskset_id = db.Column(db.String(155), unique=True) - result = db.Column(db.PickleType, nullable=True) - date_done = db.Column(db.DateTime, default=lambda: datetime.now(UTC).replace(tzinfo=None), nullable=True) + id: Mapped[int] = mapped_column( + db.Integer, db.Sequence("taskset_id_sequence"), autoincrement=True, primary_key=True + ) + taskset_id = mapped_column(db.String(155), unique=True) + result = mapped_column(db.PickleType, nullable=True) + date_done: Mapped[Optional[datetime]] = mapped_column(db.DateTime, default=lambda: naive_utc_now(), nullable=True) diff --git a/api/models/tools.py b/api/models/tools.py index 7c8b5853ba..8c91e91f0e 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -93,6 +93,7 @@ class BuiltinToolProvider(Base): credential_type: Mapped[str] = mapped_column( db.String(32), nullable=False, server_default=db.text("'api-key'::character varying") ) + expires_at: Mapped[int] = mapped_column(db.BigInteger, nullable=False, server_default=db.text("-1")) @property def credentials(self) -> dict: @@ -110,26 +111,26 @@ class ApiToolProvider(Base): db.UniqueConstraint("name", "tenant_id", name="unique_api_tool_provider"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) # name of the api provider - name = db.Column(db.String(255), nullable=False, server_default=db.text("'API KEY 1'::character varying")) + name = mapped_column(db.String(255), nullable=False, server_default=db.text("'API KEY 1'::character varying")) # icon - icon = db.Column(db.String(255), nullable=False) + icon = mapped_column(db.String(255), nullable=False) # original schema - schema = db.Column(db.Text, nullable=False) - schema_type_str: Mapped[str] = db.Column(db.String(40), nullable=False) + schema = mapped_column(db.Text, nullable=False) + schema_type_str: Mapped[str] = mapped_column(db.String(40), nullable=False) # who created this tool - user_id = db.Column(StringUUID, nullable=False) + user_id = mapped_column(StringUUID, nullable=False) # tenant id - tenant_id = db.Column(StringUUID, nullable=False) + tenant_id = mapped_column(StringUUID, nullable=False) # description of the provider - description = db.Column(db.Text, nullable=False) + description = mapped_column(db.Text, nullable=False) # json format tools - tools_str = db.Column(db.Text, nullable=False) + tools_str = mapped_column(db.Text, nullable=False) # json format credentials - credentials_str = db.Column(db.Text, nullable=False) + credentials_str = mapped_column(db.Text, nullable=False) # privacy policy - privacy_policy = db.Column(db.String(255), nullable=True) + privacy_policy = mapped_column(db.String(255), nullable=True) # custom_disclaimer custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="") @@ -254,7 +255,7 @@ class MCPToolProvider(Base): # name of the mcp provider name: Mapped[str] = mapped_column(db.String(40), nullable=False) # server identifier of the mcp provider - server_identifier: Mapped[str] = mapped_column(db.String(24), nullable=False) + server_identifier: Mapped[str] = mapped_column(db.String(64), nullable=False) # encrypted url of the mcp provider server_url: Mapped[str] = mapped_column(db.Text, nullable=False) # hash of server_url for uniqueness check @@ -348,33 +349,33 @@ class ToolModelInvoke(Base): __tablename__ = "tool_model_invokes" __table_args__ = (db.PrimaryKeyConstraint("id", name="tool_model_invoke_pkey"),) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) # who invoke this tool - user_id = db.Column(StringUUID, nullable=False) + user_id = mapped_column(StringUUID, nullable=False) # tenant id - tenant_id = db.Column(StringUUID, nullable=False) + tenant_id = mapped_column(StringUUID, nullable=False) # provider - provider = db.Column(db.String(255), nullable=False) + provider = mapped_column(db.String(255), nullable=False) # type - tool_type = db.Column(db.String(40), nullable=False) + tool_type = mapped_column(db.String(40), nullable=False) # tool name - tool_name = db.Column(db.String(40), nullable=False) + tool_name = mapped_column(db.String(128), nullable=False) # invoke parameters - model_parameters = db.Column(db.Text, nullable=False) + model_parameters = mapped_column(db.Text, nullable=False) # prompt messages - prompt_messages = db.Column(db.Text, nullable=False) + prompt_messages = mapped_column(db.Text, nullable=False) # invoke response - model_response = db.Column(db.Text, nullable=False) + model_response = mapped_column(db.Text, nullable=False) - prompt_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0")) - answer_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0")) - answer_unit_price = db.Column(db.Numeric(10, 4), nullable=False) - answer_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - provider_response_latency = db.Column(db.Float, nullable=False, server_default=db.text("0")) - total_price = db.Column(db.Numeric(10, 7)) - currency = db.Column(db.String(255), nullable=False) - 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()) + prompt_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) + answer_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) + answer_unit_price = mapped_column(db.Numeric(10, 4), nullable=False) + answer_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) + provider_response_latency = mapped_column(db.Float, nullable=False, server_default=db.text("0")) + total_price = mapped_column(db.Numeric(10, 7)) + currency = mapped_column(db.String(255), nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @deprecated @@ -391,18 +392,18 @@ class ToolConversationVariables(Base): db.Index("conversation_id_idx", "conversation_id"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) # conversation user id - user_id = db.Column(StringUUID, nullable=False) + user_id = mapped_column(StringUUID, nullable=False) # tenant id - tenant_id = db.Column(StringUUID, nullable=False) + tenant_id = mapped_column(StringUUID, nullable=False) # conversation id - conversation_id = db.Column(StringUUID, nullable=False) + conversation_id = mapped_column(StringUUID, nullable=False) # variables pool - variables_str = db.Column(db.Text, nullable=False) + variables_str = mapped_column(db.Text, nullable=False) - 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()) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @property def variables(self) -> Any: @@ -451,26 +452,26 @@ class DeprecatedPublishedAppTool(Base): db.UniqueConstraint("app_id", "user_id", name="unique_published_app_tool"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) # id of the app - app_id = db.Column(StringUUID, ForeignKey("apps.id"), nullable=False) + app_id = mapped_column(StringUUID, ForeignKey("apps.id"), nullable=False) - user_id: Mapped[str] = db.Column(StringUUID, nullable=False) + user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # who published this tool - description = db.Column(db.Text, nullable=False) + description = mapped_column(db.Text, nullable=False) # llm_description of the tool, for LLM - llm_description = db.Column(db.Text, nullable=False) + llm_description = mapped_column(db.Text, nullable=False) # query description, query will be seem as a parameter of the tool, # to describe this parameter to llm, we need this field - query_description = db.Column(db.Text, nullable=False) + query_description = mapped_column(db.Text, nullable=False) # query name, the name of the query parameter - query_name = db.Column(db.String(40), nullable=False) + query_name = mapped_column(db.String(40), nullable=False) # name of the tool provider - tool_name = db.Column(db.String(40), nullable=False) + tool_name = mapped_column(db.String(40), nullable=False) # author - author = db.Column(db.String(40), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + author = mapped_column(db.String(40), nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) @property def description_i18n(self) -> I18nObject: diff --git a/api/models/web.py b/api/models/web.py index fe2f0c47f8..bcc95ddbc2 100644 --- a/api/models/web.py +++ b/api/models/web.py @@ -15,12 +15,14 @@ class SavedMessage(Base): db.Index("saved_message_message_idx", "app_id", "message_id", "created_by_role", "created_by"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) - message_id = db.Column(StringUUID, nullable=False) - created_by_role = db.Column(db.String(255), nullable=False, server_default=db.text("'end_user'::character varying")) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + message_id = mapped_column(StringUUID, nullable=False) + created_by_role = mapped_column( + db.String(255), nullable=False, server_default=db.text("'end_user'::character varying") + ) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) @property def message(self): @@ -34,9 +36,11 @@ class PinnedConversation(Base): db.Index("pinned_conversation_conversation_idx", "app_id", "conversation_id", "created_by_role", "created_by"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - app_id = db.Column(StringUUID, nullable=False) + id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) conversation_id: Mapped[str] = mapped_column(StringUUID) - created_by_role = db.Column(db.String(255), nullable=False, server_default=db.text("'end_user'::character varying")) - created_by = db.Column(StringUUID, nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_by_role = mapped_column( + db.String(255), nullable=False, server_default=db.text("'end_user'::character varying") + ) + created_by = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) diff --git a/api/models/workflow.py b/api/models/workflow.py index 9930859201..124fb3bb4c 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1,7 +1,7 @@ import json import logging from collections.abc import Mapping, Sequence -from datetime import UTC, datetime +from datetime import datetime from enum import Enum, StrEnum from typing import TYPE_CHECKING, Any, Optional, Union from uuid import uuid4 @@ -16,6 +16,7 @@ from core.variables.variables import FloatVariable, IntegerVariable, StringVaria from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from core.workflow.nodes.enums import NodeType from factories.variable_factory import TypeMismatchError, build_segment_with_type +from libs.datetime_utils import naive_utc_now from libs.helper import extract_tenant_id from ._workflow_exc import NodeNotFoundError, WorkflowDataError @@ -138,7 +139,7 @@ class Workflow(Base): updated_at: Mapped[datetime] = mapped_column( db.DateTime, nullable=False, - default=datetime.now(UTC).replace(tzinfo=None), + default=naive_utc_now(), server_onupdate=func.current_timestamp(), ) _environment_variables: Mapped[str] = mapped_column( @@ -179,7 +180,7 @@ class Workflow(Base): workflow.conversation_variables = conversation_variables or [] workflow.marked_name = marked_name workflow.marked_comment = marked_comment - workflow.created_at = datetime.now(UTC).replace(tzinfo=None) + workflow.created_at = naive_utc_now() workflow.updated_at = workflow.created_at return workflow @@ -907,7 +908,7 @@ _EDITABLE_SYSTEM_VARIABLE = frozenset(["query", "files"]) def _naive_utc_datetime(): - return datetime.now(UTC).replace(tzinfo=None) + return naive_utc_now() class WorkflowDraftVariable(Base): 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/schedule/clean_unused_datasets_task.py b/api/schedule/clean_unused_datasets_task.py index c0cd42a226..be228a6d95 100644 --- a/api/schedule/clean_unused_datasets_task.py +++ b/api/schedule/clean_unused_datasets_task.py @@ -99,9 +99,7 @@ def clean_unused_datasets_task(): index_processor.clean(dataset, None) # update document - update_params = {Document.enabled: False} - - db.session.query(Document).filter_by(dataset_id=dataset.id).update(update_params) + db.session.query(Document).filter_by(dataset_id=dataset.id).update({Document.enabled: False}) db.session.commit() click.echo(click.style("Cleaned unused dataset {} from db success!".format(dataset.id), fg="green")) except Exception as e: @@ -176,9 +174,7 @@ def clean_unused_datasets_task(): index_processor.clean(dataset, None) # update document - update_params = {Document.enabled: False} - - db.session.query(Document).filter_by(dataset_id=dataset.id).update(update_params) + db.session.query(Document).filter_by(dataset_id=dataset.id).update({Document.enabled: False}) db.session.commit() click.echo( click.style("Cleaned unused dataset {} from db success!".format(dataset.id), fg="green") diff --git a/api/schedule/mail_clean_document_notify_task.py b/api/schedule/mail_clean_document_notify_task.py index 5ee813e1de..12e4f6ebf3 100644 --- a/api/schedule/mail_clean_document_notify_task.py +++ b/api/schedule/mail_clean_document_notify_task.py @@ -3,12 +3,12 @@ import time from collections import defaultdict import click -from flask import render_template # type: ignore import app from configs import dify_config from extensions.ext_database import db from extensions.ext_mail import mail +from libs.email_i18n import EmailType, get_email_i18n_service from models.account import Account, Tenant, TenantAccountJoin from models.dataset import Dataset, DatasetAutoDisableLog from services.feature_service import FeatureService @@ -72,14 +72,16 @@ def mail_clean_document_notify_task(): document_count = len(document_ids) knowledge_details.append(rf"Knowledge base {dataset.name}: {document_count} documents") if knowledge_details: - html_content = render_template( - "clean_document_job_mail_template-US.html", - userName=account.email, - knowledge_details=knowledge_details, - url=url, - ) - mail.send( - to=account.email, subject="Dify Knowledge base auto disable notification", html=html_content + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.DOCUMENT_CLEAN_NOTIFY, + language_code="en-US", + to=account.email, + template_context={ + "userName": account.email, + "knowledge_details": knowledge_details, + "url": url, + }, ) # update notified to True diff --git a/api/schedule/queue_monitor_task.py b/api/schedule/queue_monitor_task.py index e3a7021b9d..a05e1358ed 100644 --- a/api/schedule/queue_monitor_task.py +++ b/api/schedule/queue_monitor_task.py @@ -3,13 +3,12 @@ from datetime import datetime from urllib.parse import urlparse import click -from flask import render_template from redis import Redis import app from configs import dify_config from extensions.ext_database import db -from extensions.ext_mail import mail +from libs.email_i18n import EmailType, get_email_i18n_service # Create a dedicated Redis connection (using the same configuration as Celery) celery_broker_url = dify_config.CELERY_BROKER_URL @@ -39,18 +38,20 @@ def queue_monitor_task(): alter_emails = dify_config.QUEUE_MONITOR_ALERT_EMAILS if alter_emails: to_list = alter_emails.split(",") + email_service = get_email_i18n_service() for to in to_list: try: current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - html_content = render_template( - "queue_monitor_alert_email_template_en-US.html", - queue_name=queue_name, - queue_length=queue_length, - threshold=threshold, - alert_time=current_time, - ) - mail.send( - to=to, subject="Alert: Dataset Queue pending tasks exceeded the limit", html=html_content + email_service.send_email( + email_type=EmailType.QUEUE_MONITOR_ALERT, + language_code="en-US", + to=to, + template_context={ + "queue_name": queue_name, + "queue_length": queue_length, + "threshold": threshold, + "alert_time": current_time, + }, ) except Exception as e: logging.exception(click.style("Exception occurred during sending email", fg="red")) diff --git a/api/services/account_service.py b/api/services/account_service.py index 4d5366f47f..4c4510395e 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -17,6 +17,7 @@ from constants.languages import language_timezone_mapping, languages from events.tenant_event import tenant_was_created from extensions.ext_database import db from extensions.ext_redis import redis_client, redis_fallback +from libs.datetime_utils import naive_utc_now from libs.helper import RateLimiter, TokenManager from libs.passport import PassportService from libs.password import compare_password, hash_password, valid_password @@ -28,6 +29,7 @@ from models.account import ( Tenant, TenantAccountJoin, TenantAccountRole, + TenantPluginAutoUpgradeStrategy, TenantStatus, ) from models.model import DifySetup @@ -135,8 +137,8 @@ class AccountService: available_ta.current = True db.session.commit() - if datetime.now(UTC).replace(tzinfo=None) - account.last_active_at > timedelta(minutes=10): - account.last_active_at = datetime.now(UTC).replace(tzinfo=None) + if naive_utc_now() - account.last_active_at > timedelta(minutes=10): + account.last_active_at = naive_utc_now() db.session.commit() return cast(Account, account) @@ -180,7 +182,7 @@ class AccountService: if account.status == AccountStatus.PENDING.value: account.status = AccountStatus.ACTIVE.value - account.initialized_at = datetime.now(UTC).replace(tzinfo=None) + account.initialized_at = naive_utc_now() db.session.commit() @@ -318,7 +320,7 @@ class AccountService: # If it exists, update the record account_integrate.open_id = open_id account_integrate.encrypted_token = "" # todo - account_integrate.updated_at = datetime.now(UTC).replace(tzinfo=None) + account_integrate.updated_at = naive_utc_now() else: # If it does not exist, create a new record account_integrate = AccountIntegrate( @@ -353,7 +355,7 @@ class AccountService: @staticmethod def update_login_info(account: Account, *, ip_address: str) -> None: """Update last login time and ip""" - account.last_login_at = datetime.now(UTC).replace(tzinfo=None) + account.last_login_at = naive_utc_now() account.last_login_ip = ip_address db.session.add(account) db.session.commit() @@ -827,6 +829,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 @@ -997,7 +1010,7 @@ class TenantService: .filter(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) .first() ) - return join.role if join else None + return TenantAccountRole(join.role) if join else None @staticmethod def get_tenant_count() -> int: @@ -1066,15 +1079,6 @@ class TenantService: target_member_join.role = new_role db.session.commit() - @staticmethod - def dissolve_tenant(tenant: Tenant, operator: Account) -> None: - """Dissolve tenant""" - if not TenantService.check_member_permission(tenant, operator, operator, "remove"): - raise NoPermissionError("No permission to dissolve tenant.") - db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id).delete() - db.session.delete(tenant) - db.session.commit() - @staticmethod def get_custom_config(tenant_id: str) -> dict: tenant = db.get_or_404(Tenant, tenant_id) @@ -1117,7 +1121,7 @@ class RegisterService: ) account.last_login_ip = ip_address - account.initialized_at = datetime.now(UTC).replace(tzinfo=None) + account.initialized_at = naive_utc_now() TenantService.create_owner_tenant_if_not_exist(account=account, is_setup=True) @@ -1158,7 +1162,7 @@ class RegisterService: is_setup=is_setup, ) account.status = AccountStatus.ACTIVE.value if not status else status.value - account.initialized_at = datetime.now(UTC).replace(tzinfo=None) + account.initialized_at = naive_utc_now() if open_id is not None and provider is not None: AccountService.link_account_integrate(provider, open_id, account) diff --git a/api/services/app_service.py b/api/services/app_service.py index 0a08f345df..cfcb414de0 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -1,7 +1,6 @@ import json import logging -from datetime import UTC, datetime -from typing import Optional, cast +from typing import Optional, TypedDict, cast from flask_login import current_user from flask_sqlalchemy.pagination import Pagination @@ -17,6 +16,7 @@ from core.tools.tool_manager import ToolManager from core.tools.utils.configuration import ToolParameterConfigurationManager from events.app_event import app_was_created from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from models.account import Account from models.model import App, AppMode, AppModelConfig, Site from models.tools import ApiToolProvider @@ -220,22 +220,31 @@ class AppService: return app - def update_app(self, app: App, args: dict) -> App: + class ArgsDict(TypedDict): + name: str + description: str + icon_type: str + icon: str + icon_background: str + use_icon_as_answer_icon: bool + max_active_requests: int + + def update_app(self, app: App, args: ArgsDict) -> App: """ Update app :param app: App instance :param args: request args :return: App instance """ - app.name = args.get("name") - app.description = args.get("description", "") - app.icon_type = args.get("icon_type", "emoji") - app.icon = args.get("icon") - app.icon_background = args.get("icon_background") + app.name = args["name"] + app.description = args["description"] + app.icon_type = args["icon_type"] + app.icon = args["icon"] + app.icon_background = args["icon_background"] app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False) app.max_active_requests = args.get("max_active_requests") app.updated_by = current_user.id - app.updated_at = datetime.now(UTC).replace(tzinfo=None) + app.updated_at = naive_utc_now() db.session.commit() return app @@ -249,7 +258,7 @@ class AppService: """ app.name = name app.updated_by = current_user.id - app.updated_at = datetime.now(UTC).replace(tzinfo=None) + app.updated_at = naive_utc_now() db.session.commit() return app @@ -265,7 +274,7 @@ class AppService: app.icon = icon app.icon_background = icon_background app.updated_by = current_user.id - app.updated_at = datetime.now(UTC).replace(tzinfo=None) + app.updated_at = naive_utc_now() db.session.commit() return app @@ -282,7 +291,7 @@ class AppService: app.enable_site = enable_site app.updated_by = current_user.id - app.updated_at = datetime.now(UTC).replace(tzinfo=None) + app.updated_at = naive_utc_now() db.session.commit() return app @@ -299,7 +308,7 @@ class AppService: app.enable_api = enable_api app.updated_by = current_user.id - app.updated_at = datetime.now(UTC).replace(tzinfo=None) + app.updated_at = naive_utc_now() db.session.commit() return app diff --git a/api/services/billing_service.py b/api/services/billing_service.py index d44483ad89..9fffde0739 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -82,7 +82,7 @@ class BillingService: if not join: raise ValueError("Tenant account join not found") - if not TenantAccountRole.is_privileged_role(join.role): + if not TenantAccountRole.is_privileged_role(TenantAccountRole(join.role)): raise ValueError("Only team owner or team admin can perform this action") @classmethod diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index afdaa49465..40097d5ed5 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -1,5 +1,4 @@ from collections.abc import Callable, Sequence -from datetime import UTC, datetime from typing import Optional, Union from sqlalchemy import asc, desc, func, or_, select @@ -8,6 +7,7 @@ from sqlalchemy.orm import Session from core.app.entities.app_invoke_entities import InvokeFrom from core.llm_generator.llm_generator import LLMGenerator from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import ConversationVariable from models.account import Account @@ -113,7 +113,7 @@ class ConversationService: return cls.auto_generate_name(app_model, conversation) else: conversation.name = name - conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) + conversation.updated_at = naive_utc_now() db.session.commit() return conversation @@ -169,7 +169,7 @@ class ConversationService: conversation = cls.get_conversation(app_model, conversation_id, user) conversation.is_deleted = True - conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) + conversation.updated_at = naive_utc_now() db.session.commit() @classmethod diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index e42b5ace75..ce597420d7 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -26,6 +26,7 @@ from events.document_event import document_was_deleted from extensions.ext_database import db from extensions.ext_redis import redis_client from libs import helper +from libs.datetime_utils import naive_utc_now from models.account import Account, TenantAccountRole from models.dataset import ( AppDatasetJoin, @@ -214,9 +215,9 @@ class DatasetService: dataset.created_by = account.id dataset.updated_by = account.id dataset.tenant_id = tenant_id - dataset.embedding_model_provider = embedding_model.provider if embedding_model else None - dataset.embedding_model = embedding_model.model if embedding_model else None - dataset.retrieval_model = retrieval_model.model_dump() if retrieval_model else None + dataset.embedding_model_provider = embedding_model.provider if embedding_model else None # type: ignore + dataset.embedding_model = embedding_model.model if embedding_model else None # type: ignore + dataset.retrieval_model = retrieval_model.model_dump() if retrieval_model else None # type: ignore dataset.permission = permission or DatasetPermissionEnum.ONLY_ME dataset.provider = provider db.session.add(dataset) @@ -428,7 +429,7 @@ class DatasetService: # Add metadata fields filtered_data["updated_by"] = user.id - filtered_data["updated_at"] = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + filtered_data["updated_at"] = naive_utc_now() # update Retrieval model filtered_data["retrieval_model"] = data["retrieval_model"] @@ -994,7 +995,7 @@ class DocumentService: # update document to be paused document.is_paused = True document.paused_by = current_user.id - document.paused_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + document.paused_at = naive_utc_now() db.session.add(document) db.session.commit() @@ -1539,8 +1540,10 @@ class DocumentService: db.session.add(document) db.session.commit() # update document segment - update_params = {DocumentSegment.status: "re_segment"} - db.session.query(DocumentSegment).filter_by(document_id=document.id).update(update_params) + + db.session.query(DocumentSegment).filter_by(document_id=document.id).update( + {DocumentSegment.status: "re_segment"} + ) # type: ignore db.session.commit() # trigger async task document_indexing_update_task.delay(document.dataset_id, document.id) @@ -2225,7 +2228,7 @@ class SegmentService: # calc embedding use tokens if document.doc_form == "qa_model": segment.answer = args.answer - tokens = embedding_model.get_text_embedding_num_tokens(texts=[content + segment.answer])[0] + tokens = embedding_model.get_text_embedding_num_tokens(texts=[content + segment.answer])[0] # type: ignore else: tokens = embedding_model.get_text_embedding_num_tokens(texts=[content])[0] segment.content = content diff --git a/api/services/external_knowledge_service.py b/api/services/external_knowledge_service.py index eb50d79494..06a4c22117 100644 --- a/api/services/external_knowledge_service.py +++ b/api/services/external_knowledge_service.py @@ -1,6 +1,5 @@ import json from copy import deepcopy -from datetime import UTC, datetime from typing import Any, Optional, Union, cast from urllib.parse import urlparse @@ -11,6 +10,7 @@ from constants import HIDDEN_VALUE from core.helper import ssrf_proxy from core.rag.entities.metadata_entities import MetadataCondition from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from models.dataset import ( Dataset, ExternalKnowledgeApis, @@ -120,7 +120,7 @@ class ExternalDatasetService: external_knowledge_api.description = args.get("description", "") external_knowledge_api.settings = json.dumps(args.get("settings"), ensure_ascii=False) external_knowledge_api.updated_by = user_id - external_knowledge_api.updated_at = datetime.now(UTC).replace(tzinfo=None) + external_knowledge_api.updated_at = naive_utc_now() db.session.commit() return external_knowledge_api 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_service.py b/api/services/plugin/plugin_service.py index 0a5bc44b64..9005f0669b 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -38,6 +38,9 @@ class PluginService: plugin_id: str version: str unique_identifier: str + status: str + deprecated_reason: str + alternative_plugin_id: str REDIS_KEY_PREFIX = "plugin_service:latest_plugin:" REDIS_TTL = 60 * 5 # 5 minutes @@ -71,6 +74,9 @@ class PluginService: plugin_id=plugin_id, version=manifest.latest_version, unique_identifier=manifest.latest_package_identifier, + status=manifest.status, + deprecated_reason=manifest.deprecated_reason, + alternative_plugin_id=manifest.alternative_plugin_id, ) # Store in Redis diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 430575b532..b8e3ce2650 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -38,6 +38,7 @@ logger = logging.getLogger(__name__) class BuiltinToolManageService: __MAX_BUILTIN_TOOL_PROVIDER_COUNT__ = 100 + __DEFAULT_EXPIRES_AT__ = 2147483647 @staticmethod def delete_custom_oauth_client_params(tenant_id: str, provider: str): @@ -212,6 +213,7 @@ class BuiltinToolManageService: tenant_id: str, provider: str, credentials: dict, + expires_at: int = -1, name: str | None = None, ): """ @@ -269,6 +271,9 @@ class BuiltinToolManageService: encrypted_credentials=json.dumps(encrypter.encrypt(credentials)), credential_type=api_type.value, name=name, + expires_at=expires_at + if expires_at is not None + else BuiltinToolManageService.__DEFAULT_EXPIRES_AT__, ) session.add(db_provider) diff --git a/api/services/tools/mcp_tools_mange_service.py b/api/services/tools/mcp_tools_manage_service.py similarity index 76% rename from api/services/tools/mcp_tools_mange_service.py rename to api/services/tools/mcp_tools_manage_service.py index fda6da5983..c0126a0f4f 100644 --- a/api/services/tools/mcp_tools_mange_service.py +++ b/api/services/tools/mcp_tools_manage_service.py @@ -70,16 +70,15 @@ class MCPToolManageService: MCPToolProvider.server_url_hash == server_url_hash, MCPToolProvider.server_identifier == server_identifier, ), - MCPToolProvider.tenant_id == tenant_id, ) .first() ) if existing_provider: if existing_provider.name == name: raise ValueError(f"MCP tool {name} already exists") - elif existing_provider.server_url_hash == server_url_hash: + if existing_provider.server_url_hash == server_url_hash: raise ValueError(f"MCP tool {server_url} already exists") - elif existing_provider.server_identifier == server_identifier: + if existing_provider.server_identifier == server_identifier: raise ValueError(f"MCP tool {server_identifier} already exists") encrypted_server_url = encrypter.encrypt_token(tenant_id, server_url) mcp_tool = MCPToolProvider( @@ -111,22 +110,29 @@ class MCPToolManageService: ] @classmethod - def list_mcp_tool_from_remote_server(cls, tenant_id: str, provider_id: str): + def list_mcp_tool_from_remote_server(cls, tenant_id: str, provider_id: str) -> ToolProviderApiEntity: mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) + server_url = mcp_provider.decrypted_server_url + authed = mcp_provider.authed try: - with MCPClient( - mcp_provider.decrypted_server_url, provider_id, tenant_id, authed=mcp_provider.authed, for_list=True - ) as mcp_client: + with MCPClient(server_url, provider_id, tenant_id, authed=authed, for_list=True) as mcp_client: tools = mcp_client.list_tools() - except MCPAuthError as e: + except MCPAuthError: raise ValueError("Please auth the tool first") except MCPError as e: raise ValueError(f"Failed to connect to MCP server: {e}") - mcp_provider.tools = json.dumps([tool.model_dump() for tool in tools]) - mcp_provider.authed = True - mcp_provider.updated_at = datetime.now() - db.session.commit() + + try: + mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) + mcp_provider.tools = json.dumps([tool.model_dump() for tool in tools]) + mcp_provider.authed = True + mcp_provider.updated_at = datetime.now() + db.session.commit() + except Exception: + db.session.rollback() + raise + user = mcp_provider.load_user() return ToolProviderApiEntity( id=mcp_provider.id, @@ -162,34 +168,49 @@ class MCPToolManageService: server_identifier: str, ): mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) - mcp_provider.updated_at = datetime.now() - mcp_provider.name = name - mcp_provider.icon = ( - json.dumps({"content": icon, "background": icon_background}) if icon_type == "emoji" else icon - ) - mcp_provider.server_identifier = server_identifier + + reconnect_result = None + encrypted_server_url = None + server_url_hash = None if UNCHANGED_SERVER_URL_PLACEHOLDER not in server_url: encrypted_server_url = encrypter.encrypt_token(tenant_id, server_url) - mcp_provider.server_url = encrypted_server_url server_url_hash = hashlib.sha256(server_url.encode()).hexdigest() if server_url_hash != mcp_provider.server_url_hash: - cls._re_connect_mcp_provider(mcp_provider, provider_id, tenant_id) - mcp_provider.server_url_hash = server_url_hash + reconnect_result = cls._re_connect_mcp_provider(server_url, provider_id, tenant_id) + try: + mcp_provider.updated_at = datetime.now() + mcp_provider.name = name + mcp_provider.icon = ( + json.dumps({"content": icon, "background": icon_background}) if icon_type == "emoji" else icon + ) + mcp_provider.server_identifier = server_identifier + + if encrypted_server_url is not None and server_url_hash is not None: + mcp_provider.server_url = encrypted_server_url + mcp_provider.server_url_hash = server_url_hash + + if reconnect_result: + mcp_provider.authed = reconnect_result["authed"] + mcp_provider.tools = reconnect_result["tools"] + mcp_provider.encrypted_credentials = reconnect_result["encrypted_credentials"] + db.session.commit() except IntegrityError as e: db.session.rollback() error_msg = str(e.orig) if "unique_mcp_provider_name" in error_msg: raise ValueError(f"MCP tool {name} already exists") - elif "unique_mcp_provider_server_url" in error_msg: + if "unique_mcp_provider_server_url" in error_msg: raise ValueError(f"MCP tool {server_url} already exists") - elif "unique_mcp_provider_server_identifier" in error_msg: + if "unique_mcp_provider_server_identifier" in error_msg: raise ValueError(f"MCP tool {server_identifier} already exists") - else: - raise + raise + except Exception: + db.session.rollback() + raise @classmethod def update_mcp_provider_credentials( @@ -210,23 +231,22 @@ class MCPToolManageService: db.session.commit() @classmethod - def _re_connect_mcp_provider(cls, mcp_provider: MCPToolProvider, provider_id: str, tenant_id: str): - """re-connect mcp provider""" + def _re_connect_mcp_provider(cls, server_url: str, provider_id: str, tenant_id: str): try: with MCPClient( - mcp_provider.decrypted_server_url, + server_url, provider_id, tenant_id, authed=False, for_list=True, ) as mcp_client: tools = mcp_client.list_tools() - mcp_provider.authed = True - mcp_provider.tools = json.dumps([tool.model_dump() for tool in tools]) + return { + "authed": True, + "tools": json.dumps([tool.model_dump() for tool in tools]), + "encrypted_credentials": "{}", + } except MCPAuthError: - mcp_provider.authed = False - mcp_provider.tools = "[]" + return {"authed": False, "tools": "[]", "encrypted_credentials": "{}"} except MCPError as e: raise ValueError(f"Failed to re-connect MCP server: {e}") from e - # reset credentials - mcp_provider.encrypted_credentials = "{}" diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index e31f77607a..403e559743 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -2,7 +2,6 @@ import json import time import uuid from collections.abc import Callable, Generator, Mapping, Sequence -from datetime import UTC, datetime from typing import Any, Optional, cast from uuid import uuid4 @@ -33,6 +32,7 @@ from core.workflow.workflow_entry import WorkflowEntry from events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated from extensions.ext_database import db from factories.file_factory import build_from_mapping, build_from_mappings +from libs.datetime_utils import naive_utc_now from models.account import Account from models.model import App, AppMode from models.tools import WorkflowToolProvider @@ -232,7 +232,7 @@ class WorkflowService: workflow.graph = json.dumps(graph) workflow.features = json.dumps(features) workflow.updated_by = account.id - workflow.updated_at = datetime.now(UTC).replace(tzinfo=None) + workflow.updated_at = naive_utc_now() workflow.environment_variables = environment_variables workflow.conversation_variables = conversation_variables @@ -268,7 +268,7 @@ class WorkflowService: tenant_id=app_model.tenant_id, app_id=app_model.id, type=draft_workflow.type, - version=Workflow.version_from_datetime(datetime.now(UTC).replace(tzinfo=None)), + version=Workflow.version_from_datetime(naive_utc_now()), graph=draft_workflow.graph, features=draft_workflow.features, created_by=account.id, @@ -523,8 +523,8 @@ class WorkflowService: node_type=node.type_, title=node.title, elapsed_time=time.perf_counter() - start_at, - created_at=datetime.now(UTC).replace(tzinfo=None), - finished_at=datetime.now(UTC).replace(tzinfo=None), + created_at=naive_utc_now(), + finished_at=naive_utc_now(), ) if run_succeeded and node_run_result: @@ -621,7 +621,7 @@ class WorkflowService: setattr(workflow, field, value) workflow.updated_by = account_id - workflow.updated_at = datetime.now(UTC).replace(tzinfo=None) + workflow.updated_at = naive_utc_now() return workflow diff --git a/api/tasks/create_segment_to_index_task.py b/api/tasks/create_segment_to_index_task.py index a3f811faa1..5710d660be 100644 --- a/api/tasks/create_segment_to_index_task.py +++ b/api/tasks/create_segment_to_index_task.py @@ -37,11 +37,12 @@ def create_segment_to_index_task(segment_id: str, keywords: Optional[list[str]] try: # update segment status to indexing - update_params = { - DocumentSegment.status: "indexing", - DocumentSegment.indexing_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), - } - db.session.query(DocumentSegment).filter_by(id=segment.id).update(update_params) + db.session.query(DocumentSegment).filter_by(id=segment.id).update( + { + DocumentSegment.status: "indexing", + DocumentSegment.indexing_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), + } + ) db.session.commit() document = Document( page_content=segment.content, @@ -74,11 +75,12 @@ def create_segment_to_index_task(segment_id: str, keywords: Optional[list[str]] index_processor.load(dataset, [document]) # update segment to completed - update_params = { - DocumentSegment.status: "completed", - DocumentSegment.completed_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), - } - db.session.query(DocumentSegment).filter_by(id=segment.id).update(update_params) + db.session.query(DocumentSegment).filter_by(id=segment.id).update( + { + DocumentSegment.status: "completed", + DocumentSegment.completed_at: datetime.datetime.now(datetime.UTC).replace(tzinfo=None), + } + ) db.session.commit() end_at = time.perf_counter() diff --git a/api/tasks/document_indexing_task.py b/api/tasks/document_indexing_task.py index 55cac6a9af..a85aab0bb7 100644 --- a/api/tasks/document_indexing_task.py +++ b/api/tasks/document_indexing_task.py @@ -1,4 +1,3 @@ -import datetime import logging import time @@ -8,6 +7,7 @@ from celery import shared_task # type: ignore from configs import dify_config from core.indexing_runner import DocumentIsPausedError, IndexingRunner from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now from models.dataset import Dataset, Document from services.feature_service import FeatureService @@ -53,7 +53,7 @@ def document_indexing_task(dataset_id: str, document_ids: list): if document: document.indexing_status = "error" document.error = str(e) - document.stopped_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + document.stopped_at = naive_utc_now() db.session.add(document) db.session.commit() db.session.close() @@ -68,7 +68,7 @@ def document_indexing_task(dataset_id: str, document_ids: list): if document: document.indexing_status = "parsing" - document.processing_started_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + document.processing_started_at = naive_utc_now() documents.append(document) db.session.add(document) db.session.commit() diff --git a/api/tasks/mail_account_deletion_task.py b/api/tasks/mail_account_deletion_task.py index 0c60ae53d5..a6f8ce2f0b 100644 --- a/api/tasks/mail_account_deletion_task.py +++ b/api/tasks/mail_account_deletion_task.py @@ -3,14 +3,20 @@ import time import click from celery import shared_task # type: ignore -from flask import render_template from extensions.ext_mail import mail +from libs.email_i18n import EmailType, get_email_i18n_service @shared_task(queue="mail") -def send_deletion_success_task(to): - """Send email to user regarding account deletion.""" +def send_deletion_success_task(to: str, language: str = "en-US") -> None: + """ + Send account deletion success email with internationalization support. + + Args: + to: Recipient email address + language: Language code for email localization + """ if not mail.is_inited(): return @@ -18,12 +24,16 @@ def send_deletion_success_task(to): start_at = time.perf_counter() try: - html_content = render_template( - "delete_account_success_template_en-US.html", + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.ACCOUNT_DELETION_SUCCESS, + language_code=language, to=to, - email=to, + template_context={ + "to": to, + "email": to, + }, ) - mail.send(to=to, subject="Your Dify.AI Account Has Been Successfully Deleted", html=html_content) end_at = time.perf_counter() logging.info( @@ -36,12 +46,14 @@ def send_deletion_success_task(to): @shared_task(queue="mail") -def send_account_deletion_verification_code(to, code): - """Send email to user regarding account deletion verification code. +def send_account_deletion_verification_code(to: str, code: str, language: str = "en-US") -> None: + """ + Send account deletion verification code email with internationalization support. Args: - to (str): Recipient email address - code (str): Verification code + to: Recipient email address + code: Verification code + language: Language code for email localization """ if not mail.is_inited(): return @@ -50,8 +62,16 @@ def send_account_deletion_verification_code(to, code): start_at = time.perf_counter() try: - html_content = render_template("delete_account_code_email_template_en-US.html", to=to, code=code) - mail.send(to=to, subject="Dify.AI Account Deletion and Verification", html=html_content) + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.ACCOUNT_DELETION_VERIFICATION, + language_code=language, + to=to, + template_context={ + "to": to, + "code": code, + }, + ) end_at = time.perf_counter() logging.info( diff --git a/api/tasks/mail_change_mail_task.py b/api/tasks/mail_change_mail_task.py index da44040b7d..ea1875901c 100644 --- a/api/tasks/mail_change_mail_task.py +++ b/api/tasks/mail_change_mail_task.py @@ -3,20 +3,21 @@ import time import click from celery import shared_task # type: ignore -from flask import render_template from extensions.ext_mail import mail -from services.feature_service import FeatureService +from libs.email_i18n import get_email_i18n_service @shared_task(queue="mail") -def send_change_mail_task(language: str, to: str, code: str, phase: str): +def send_change_mail_task(language: str, to: str, code: str, phase: str) -> None: """ - Async Send change email mail - :param language: Language in which the email should be sent (e.g., 'en', 'zh') - :param to: Recipient email address - :param code: Change email code - :param phase: Change email phase (new_email, old_email) + Send change email notification with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + code: Email verification code + phase: Change email phase ('old_email' or 'new_email') """ if not mail.is_inited(): return @@ -24,51 +25,14 @@ def send_change_mail_task(language: str, to: str, code: str, phase: str): logging.info(click.style("Start change email mail to {}".format(to), fg="green")) start_at = time.perf_counter() - email_config = { - "zh-Hans": { - "old_email": { - "subject": "检测您现在的邮箱", - "template_with_brand": "change_mail_confirm_old_template_zh-CN.html", - "template_without_brand": "without-brand/change_mail_confirm_old_template_zh-CN.html", - }, - "new_email": { - "subject": "确认您的邮箱地址变更", - "template_with_brand": "change_mail_confirm_new_template_zh-CN.html", - "template_without_brand": "without-brand/change_mail_confirm_new_template_zh-CN.html", - }, - }, - "en": { - "old_email": { - "subject": "Check your current email", - "template_with_brand": "change_mail_confirm_old_template_en-US.html", - "template_without_brand": "without-brand/change_mail_confirm_old_template_en-US.html", - }, - "new_email": { - "subject": "Confirm your new email address", - "template_with_brand": "change_mail_confirm_new_template_en-US.html", - "template_without_brand": "without-brand/change_mail_confirm_new_template_en-US.html", - }, - }, - } - - # send change email mail using different languages try: - system_features = FeatureService.get_system_features() - lang_key = "zh-Hans" if language == "zh-Hans" else "en" - - if phase not in ["old_email", "new_email"]: - raise ValueError("Invalid phase") - - config = email_config[lang_key][phase] - subject = config["subject"] - - if system_features.branding.enabled: - template = config["template_without_brand"] - else: - template = config["template_with_brand"] - - html_content = render_template(template, to=to, code=code) - mail.send(to=to, subject=subject, html=html_content) + email_service = get_email_i18n_service() + email_service.send_change_email( + language_code=language, + to=to, + code=code, + phase=phase, + ) end_at = time.perf_counter() logging.info( diff --git a/api/tasks/mail_email_code_login.py b/api/tasks/mail_email_code_login.py index ddad331725..34220784e9 100644 --- a/api/tasks/mail_email_code_login.py +++ b/api/tasks/mail_email_code_login.py @@ -3,19 +3,20 @@ import time import click from celery import shared_task # type: ignore -from flask import render_template from extensions.ext_mail import mail -from services.feature_service import FeatureService +from libs.email_i18n import EmailType, get_email_i18n_service @shared_task(queue="mail") -def send_email_code_login_mail_task(language: str, to: str, code: str): +def send_email_code_login_mail_task(language: str, to: str, code: str) -> None: """ - Async Send email code login mail - :param language: Language in which the email should be sent (e.g., 'en', 'zh') - :param to: Recipient email address - :param code: Email code to be included in the email + Send email code login email with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + code: Email verification code """ if not mail.is_inited(): return @@ -23,28 +24,17 @@ def send_email_code_login_mail_task(language: str, to: str, code: str): logging.info(click.style("Start email code login mail to {}".format(to), fg="green")) start_at = time.perf_counter() - # send email code login mail using different languages try: - if language == "zh-Hans": - template = "email_code_login_mail_template_zh-CN.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - application_title = system_features.branding.application_title - template = "without-brand/email_code_login_mail_template_zh-CN.html" - html_content = render_template(template, to=to, code=code, application_title=application_title) - else: - html_content = render_template(template, to=to, code=code) - mail.send(to=to, subject="邮箱验证码", html=html_content) - else: - template = "email_code_login_mail_template_en-US.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - application_title = system_features.branding.application_title - template = "without-brand/email_code_login_mail_template_en-US.html" - html_content = render_template(template, to=to, code=code, application_title=application_title) - else: - html_content = render_template(template, to=to, code=code) - mail.send(to=to, subject="Email Code", html=html_content) + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.EMAIL_CODE_LOGIN, + language_code=language, + to=to, + template_context={ + "to": to, + "code": code, + }, + ) end_at = time.perf_counter() logging.info( diff --git a/api/tasks/mail_enterprise_task.py b/api/tasks/mail_enterprise_task.py index b9d8fd55df..a1c2908624 100644 --- a/api/tasks/mail_enterprise_task.py +++ b/api/tasks/mail_enterprise_task.py @@ -1,15 +1,17 @@ import logging import time +from collections.abc import Mapping import click from celery import shared_task # type: ignore from flask import render_template_string from extensions.ext_mail import mail +from libs.email_i18n import get_email_i18n_service @shared_task(queue="mail") -def send_enterprise_email_task(to, subject, body, substitutions): +def send_enterprise_email_task(to: list[str], subject: str, body: str, substitutions: Mapping[str, str]): if not mail.is_inited(): return @@ -19,11 +21,8 @@ def send_enterprise_email_task(to, subject, body, substitutions): try: html_content = render_template_string(body, **substitutions) - if isinstance(to, list): - for t in to: - mail.send(to=t, subject=subject, html=html_content) - else: - mail.send(to=to, subject=subject, html=html_content) + email_service = get_email_i18n_service() + email_service.send_raw_email(to=to, subject=subject, html_content=html_content) end_at = time.perf_counter() logging.info( diff --git a/api/tasks/mail_invite_member_task.py b/api/tasks/mail_invite_member_task.py index 7ca85c7f2d..8c73de0111 100644 --- a/api/tasks/mail_invite_member_task.py +++ b/api/tasks/mail_invite_member_task.py @@ -3,24 +3,23 @@ import time import click from celery import shared_task # type: ignore -from flask import render_template from configs import dify_config from extensions.ext_mail import mail -from services.feature_service import FeatureService +from libs.email_i18n import EmailType, get_email_i18n_service @shared_task(queue="mail") -def send_invite_member_mail_task(language: str, to: str, token: str, inviter_name: str, workspace_name: str): +def send_invite_member_mail_task(language: str, to: str, token: str, inviter_name: str, workspace_name: str) -> None: """ - Async Send invite member mail - :param language - :param to - :param token - :param inviter_name - :param workspace_name - - Usage: send_invite_member_mail_task.delay(language, to, token, inviter_name, workspace_name) + Send invite member email with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + token: Invitation token + inviter_name: Name of the person sending the invitation + workspace_name: Name of the workspace """ if not mail.is_inited(): return @@ -30,49 +29,20 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam ) start_at = time.perf_counter() - # send invite member mail using different languages try: url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}" - if language == "zh-Hans": - template = "invite_member_mail_template_zh-CN.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - application_title = system_features.branding.application_title - template = "without-brand/invite_member_mail_template_zh-CN.html" - html_content = render_template( - template, - to=to, - inviter_name=inviter_name, - workspace_name=workspace_name, - url=url, - application_title=application_title, - ) - mail.send(to=to, subject=f"立即加入 {application_title} 工作空间", html=html_content) - else: - html_content = render_template( - template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url - ) - mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content) - else: - template = "invite_member_mail_template_en-US.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - application_title = system_features.branding.application_title - template = "without-brand/invite_member_mail_template_en-US.html" - html_content = render_template( - template, - to=to, - inviter_name=inviter_name, - workspace_name=workspace_name, - url=url, - application_title=application_title, - ) - mail.send(to=to, subject=f"Join {application_title} Workspace Now", html=html_content) - else: - html_content = render_template( - template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url - ) - mail.send(to=to, subject="Join Dify Workspace Now", html=html_content) + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.INVITE_MEMBER, + language_code=language, + to=to, + template_context={ + "to": to, + "inviter_name": inviter_name, + "workspace_name": workspace_name, + "url": url, + }, + ) end_at = time.perf_counter() logging.info( diff --git a/api/tasks/mail_owner_transfer_task.py b/api/tasks/mail_owner_transfer_task.py index 8d05c6dc0f..e566a6bc56 100644 --- a/api/tasks/mail_owner_transfer_task.py +++ b/api/tasks/mail_owner_transfer_task.py @@ -3,47 +3,40 @@ import time import click from celery import shared_task # type: ignore -from flask import render_template from extensions.ext_mail import mail -from services.feature_service import FeatureService +from libs.email_i18n import EmailType, get_email_i18n_service @shared_task(queue="mail") -def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspace: str): +def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspace: str) -> None: """ - Async Send owner transfer confirm mail - :param language: Language in which the email should be sent (e.g., 'en', 'zh') - :param to: Recipient email address - :param workspace: Workspace name + Send owner transfer confirmation email with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + code: Verification code + workspace: Workspace name """ if not mail.is_inited(): return - logging.info(click.style("Start change email mail to {}".format(to), fg="green")) + logging.info(click.style("Start owner transfer confirm mail to {}".format(to), fg="green")) start_at = time.perf_counter() - # send change email mail using different languages + try: - if language == "zh-Hans": - template = "transfer_workspace_owner_confirm_template_zh-CN.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - template = "without-brand/transfer_workspace_owner_confirm_template_zh-CN.html" - html_content = render_template(template, to=to, code=code, WorkspaceName=workspace) - mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content) - else: - html_content = render_template(template, to=to, code=code, WorkspaceName=workspace) - mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content) - else: - template = "transfer_workspace_owner_confirm_template_en-US.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - template = "without-brand/transfer_workspace_owner_confirm_template_en-US.html" - html_content = render_template(template, to=to, code=code, WorkspaceName=workspace) - mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content) - else: - html_content = render_template(template, to=to, code=code, WorkspaceName=workspace) - mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content) + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.OWNER_TRANSFER_CONFIRM, + language_code=language, + to=to, + template_context={ + "to": to, + "code": code, + "WorkspaceName": workspace, + }, + ) end_at = time.perf_counter() logging.info( @@ -57,96 +50,80 @@ def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspac @shared_task(queue="mail") -def send_old_owner_transfer_notify_email_task(language: str, to: str, workspace: str, new_owner_email: str): +def send_old_owner_transfer_notify_email_task(language: str, to: str, workspace: str, new_owner_email: str) -> None: """ - Async Send owner transfer confirm mail - :param language: Language in which the email should be sent (e.g., 'en', 'zh') - :param to: Recipient email address - :param workspace: Workspace name - :param new_owner_email: New owner email + Send old owner transfer notification email with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + workspace: Workspace name + new_owner_email: New owner email address """ if not mail.is_inited(): return - logging.info(click.style("Start change email mail to {}".format(to), fg="green")) + logging.info(click.style("Start old owner transfer notify mail to {}".format(to), fg="green")) start_at = time.perf_counter() - # send change email mail using different languages + try: - if language == "zh-Hans": - template = "transfer_workspace_old_owner_notify_template_zh-CN.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - template = "without-brand/transfer_workspace_old_owner_notify_template_zh-CN.html" - html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email) - mail.send(to=to, subject="工作区所有权已转移", html=html_content) - else: - html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email) - mail.send(to=to, subject="工作区所有权已转移", html=html_content) - else: - template = "transfer_workspace_old_owner_notify_template_en-US.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - template = "without-brand/transfer_workspace_old_owner_notify_template_en-US.html" - html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email) - mail.send(to=to, subject="Workspace ownership has been transferred", html=html_content) - else: - html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email) - mail.send(to=to, subject="Workspace ownership has been transferred", html=html_content) + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.OWNER_TRANSFER_OLD_NOTIFY, + language_code=language, + to=to, + template_context={ + "to": to, + "WorkspaceName": workspace, + "NewOwnerEmail": new_owner_email, + }, + ) end_at = time.perf_counter() logging.info( click.style( - "Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at), + "Send old owner transfer notify mail to {} succeeded: latency: {}".format(to, end_at - start_at), fg="green", ) ) except Exception: - logging.exception("owner transfer confirm email mail to {} failed".format(to)) + logging.exception("old owner transfer notify email mail to {} failed".format(to)) @shared_task(queue="mail") -def send_new_owner_transfer_notify_email_task(language: str, to: str, workspace: str): +def send_new_owner_transfer_notify_email_task(language: str, to: str, workspace: str) -> None: """ - Async Send owner transfer confirm mail - :param language: Language in which the email should be sent (e.g., 'en', 'zh') - :param to: Recipient email address - :param code: Change email code - :param workspace: Workspace name + Send new owner transfer notification email with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + workspace: Workspace name """ if not mail.is_inited(): return - logging.info(click.style("Start change email mail to {}".format(to), fg="green")) + logging.info(click.style("Start new owner transfer notify mail to {}".format(to), fg="green")) start_at = time.perf_counter() - # send change email mail using different languages + try: - if language == "zh-Hans": - template = "transfer_workspace_new_owner_notify_template_zh-CN.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - template = "without-brand/transfer_workspace_new_owner_notify_template_zh-CN.html" - html_content = render_template(template, to=to, WorkspaceName=workspace) - mail.send(to=to, subject=f"您现在是 {workspace} 的所有者", html=html_content) - else: - html_content = render_template(template, to=to, WorkspaceName=workspace) - mail.send(to=to, subject=f"您现在是 {workspace} 的所有者", html=html_content) - else: - template = "transfer_workspace_new_owner_notify_template_en-US.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - template = "without-brand/transfer_workspace_new_owner_notify_template_en-US.html" - html_content = render_template(template, to=to, WorkspaceName=workspace) - mail.send(to=to, subject=f"You are now the owner of {workspace}", html=html_content) - else: - html_content = render_template(template, to=to, WorkspaceName=workspace) - mail.send(to=to, subject=f"You are now the owner of {workspace}", html=html_content) + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.OWNER_TRANSFER_NEW_NOTIFY, + language_code=language, + to=to, + template_context={ + "to": to, + "WorkspaceName": workspace, + }, + ) end_at = time.perf_counter() logging.info( click.style( - "Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at), + "Send new owner transfer notify mail to {} succeeded: latency: {}".format(to, end_at - start_at), fg="green", ) ) except Exception: - logging.exception("owner transfer confirm email mail to {} failed".format(to)) + logging.exception("new owner transfer notify email mail to {} failed".format(to)) diff --git a/api/tasks/mail_reset_password_task.py b/api/tasks/mail_reset_password_task.py index d4f4482a48..e2482f2101 100644 --- a/api/tasks/mail_reset_password_task.py +++ b/api/tasks/mail_reset_password_task.py @@ -3,19 +3,20 @@ import time import click from celery import shared_task # type: ignore -from flask import render_template from extensions.ext_mail import mail -from services.feature_service import FeatureService +from libs.email_i18n import EmailType, get_email_i18n_service @shared_task(queue="mail") -def send_reset_password_mail_task(language: str, to: str, code: str): +def send_reset_password_mail_task(language: str, to: str, code: str) -> None: """ - Async Send reset password mail - :param language: Language in which the email should be sent (e.g., 'en', 'zh') - :param to: Recipient email address - :param code: Reset password code + Send reset password email with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + code: Reset password code """ if not mail.is_inited(): return @@ -23,30 +24,17 @@ def send_reset_password_mail_task(language: str, to: str, code: str): logging.info(click.style("Start password reset mail to {}".format(to), fg="green")) start_at = time.perf_counter() - # send reset password mail using different languages try: - if language == "zh-Hans": - template = "reset_password_mail_template_zh-CN.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - application_title = system_features.branding.application_title - template = "without-brand/reset_password_mail_template_zh-CN.html" - html_content = render_template(template, to=to, code=code, application_title=application_title) - mail.send(to=to, subject=f"设置您的 {application_title} 密码", html=html_content) - else: - html_content = render_template(template, to=to, code=code) - mail.send(to=to, subject="设置您的 Dify 密码", html=html_content) - else: - template = "reset_password_mail_template_en-US.html" - system_features = FeatureService.get_system_features() - if system_features.branding.enabled: - application_title = system_features.branding.application_title - template = "without-brand/reset_password_mail_template_en-US.html" - html_content = render_template(template, to=to, code=code, application_title=application_title) - mail.send(to=to, subject=f"Set Your {application_title} Password", html=html_content) - else: - html_content = render_template(template, to=to, code=code) - mail.send(to=to, subject="Set Your Dify Password", html=html_content) + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.RESET_PASSWORD, + language_code=language, + to=to, + template_context={ + "to": to, + "code": code, + }, + ) end_at = time.perf_counter() logging.info( 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..6fcdad0525 --- /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 + + # get plugin_ids to check + 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: + # get all plugins and remove excluded plugins + 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): + # execute upgrade + 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/templates/clean_document_job_mail_template-US.html b/api/templates/clean_document_job_mail_template-US.html index b26e494f80..97f3997c93 100644 --- a/api/templates/clean_document_job_mail_template-US.html +++ b/api/templates/clean_document_job_mail_template-US.html @@ -45,8 +45,7 @@ line-height: 120%; /* 28.8px */ } .button { - display: inline-block; - width: 480px; + display: block; padding: 8px 12px; color: white; text-decoration: none; diff --git a/api/templates/invite_member_mail_template_en-US.html b/api/templates/invite_member_mail_template_en-US.html index da29242869..a07c5f4b16 100644 --- a/api/templates/invite_member_mail_template_en-US.html +++ b/api/templates/invite_member_mail_template_en-US.html @@ -12,7 +12,7 @@ } .container { width: 504px; - height: 444px; + min-height: 444px; margin: 40px auto; padding: 0 48px; background-color: #fcfcfd; @@ -31,8 +31,7 @@ height: auto; } .button { - display: inline-block; - width: 480px; + display: block; padding: 8px 12px; color: white; text-decoration: none; diff --git a/api/templates/invite_member_mail_template_zh-CN.html b/api/templates/invite_member_mail_template_zh-CN.html index c7c321bf6f..27709a3c6d 100644 --- a/api/templates/invite_member_mail_template_zh-CN.html +++ b/api/templates/invite_member_mail_template_zh-CN.html @@ -28,11 +28,10 @@ .header img { max-width: 63px; - height: auto; + min-height: auto; } .button { - display: inline-block; - width: 480px; + display: block; padding: 8px 12px; color: white; text-decoration: none; diff --git a/api/templates/without-brand/invite_member_mail_template_en-US.html b/api/templates/without-brand/invite_member_mail_template_en-US.html index f6b1966c52..fc7f3679ba 100644 --- a/api/templates/without-brand/invite_member_mail_template_en-US.html +++ b/api/templates/without-brand/invite_member_mail_template_en-US.html @@ -12,7 +12,7 @@ } .container { width: 504px; - height: 444px; + min-height: 444px; margin: 40px auto; padding: 0 48px; background-color: #fcfcfd; @@ -31,8 +31,7 @@ height: auto; } .button { - display: inline-block; - width: 480px; + display: block; padding: 8px 12px; color: white; text-decoration: none; diff --git a/api/templates/without-brand/invite_member_mail_template_zh-CN.html b/api/templates/without-brand/invite_member_mail_template_zh-CN.html index fd2d6b873f..e787c90914 100644 --- a/api/templates/without-brand/invite_member_mail_template_zh-CN.html +++ b/api/templates/without-brand/invite_member_mail_template_zh-CN.html @@ -12,7 +12,7 @@ } .container { width: 504px; - height: 444px; + min-height: 444px; margin: 40px auto; padding: 0 48px; background-color: #fcfcfd; @@ -31,8 +31,7 @@ height: auto; } .button { - display: inline-block; - width: 480px; + display: block; padding: 8px 12px; color: white; text-decoration: none; diff --git a/api/tests/integration_tests/vdb/tablestore/test_tablestore.py b/api/tests/integration_tests/vdb/tablestore/test_tablestore.py index da890d0b7c..da549af1b6 100644 --- a/api/tests/integration_tests/vdb/tablestore/test_tablestore.py +++ b/api/tests/integration_tests/vdb/tablestore/test_tablestore.py @@ -1,4 +1,7 @@ import os +import uuid + +import tablestore from core.rag.datasource.vdb.tablestore.tablestore_vector import ( TableStoreConfig, @@ -6,6 +9,8 @@ from core.rag.datasource.vdb.tablestore.tablestore_vector import ( ) from tests.integration_tests.vdb.test_vector_store import ( AbstractVectorTest, + get_example_document, + get_example_text, setup_mock_redis, ) @@ -29,6 +34,49 @@ class TableStoreVectorTest(AbstractVectorTest): assert len(ids) == 1 assert ids[0] == self.example_doc_id + def create_vector(self): + self.vector.create( + texts=[get_example_document(doc_id=self.example_doc_id)], + embeddings=[self.example_embedding], + ) + while True: + search_response = self.vector._tablestore_client.search( + table_name=self.vector._table_name, + index_name=self.vector._index_name, + search_query=tablestore.SearchQuery(query=tablestore.MatchAllQuery(), get_total_count=True, limit=0), + columns_to_get=tablestore.ColumnsToGet(return_type=tablestore.ColumnReturnType.ALL_FROM_INDEX), + ) + if search_response.total_count == 1: + break + + def search_by_vector(self): + super().search_by_vector() + docs = self.vector.search_by_vector(self.example_embedding, document_ids_filter=[self.example_doc_id]) + assert len(docs) == 1 + assert docs[0].metadata["doc_id"] == self.example_doc_id + assert docs[0].metadata["score"] > 0 + + docs = self.vector.search_by_vector(self.example_embedding, document_ids_filter=[str(uuid.uuid4())]) + assert len(docs) == 0 + + def search_by_full_text(self): + super().search_by_full_text() + docs = self.vector.search_by_full_text(get_example_text(), document_ids_filter=[self.example_doc_id]) + assert len(docs) == 1 + assert docs[0].metadata["doc_id"] == self.example_doc_id + assert not hasattr(docs[0], "score") + + docs = self.vector.search_by_full_text(get_example_text(), document_ids_filter=[str(uuid.uuid4())]) + assert len(docs) == 0 + + def run_all_tests(self): + try: + self.vector.delete() + except Exception: + pass + + return super().run_all_tests() + def test_tablestore_vector(setup_mock_redis): TableStoreVectorTest().run_all_tests() diff --git a/api/tests/unit_tests/conftest.py b/api/tests/unit_tests/conftest.py index 077ffe3408..f484fb22d3 100644 --- a/api/tests/unit_tests/conftest.py +++ b/api/tests/unit_tests/conftest.py @@ -26,8 +26,15 @@ redis_mock.hgetall = MagicMock(return_value={}) redis_mock.hdel = MagicMock() redis_mock.incr = MagicMock(return_value=1) +# Add the API directory to Python path to ensure proper imports +import sys + +sys.path.insert(0, PROJECT_DIR) + # apply the mock to the Redis client in the Flask app -redis_patcher = patch("extensions.ext_redis.redis_client", redis_mock) +from extensions import ext_redis + +redis_patcher = patch.object(ext_redis, "redis_client", redis_mock) redis_patcher.start() diff --git a/api/tests/unit_tests/core/helper/test_trace_id_helper.py b/api/tests/unit_tests/core/helper/test_trace_id_helper.py new file mode 100644 index 0000000000..27bfe1af05 --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_trace_id_helper.py @@ -0,0 +1,86 @@ +import pytest + +from core.helper.trace_id_helper import extract_external_trace_id_from_args, get_external_trace_id, is_valid_trace_id + + +class DummyRequest: + def __init__(self, headers=None, args=None, json=None, is_json=False): + self.headers = headers or {} + self.args = args or {} + self.json = json + self.is_json = is_json + + +class TestTraceIdHelper: + """Test cases for trace_id_helper.py""" + + @pytest.mark.parametrize( + ("trace_id", "expected"), + [ + ("abc123", True), + ("A-B_C-123", True), + ("a" * 128, True), + ("", False), + ("a" * 129, False), + ("abc!@#", False), + ("空格", False), + ("with space", False), + ], + ) + def test_is_valid_trace_id(self, trace_id, expected): + """Test trace_id validation for various cases""" + assert is_valid_trace_id(trace_id) is expected + + def test_get_external_trace_id_from_header(self): + """Should extract valid trace_id from header""" + req = DummyRequest(headers={"X-Trace-Id": "abc123"}) + assert get_external_trace_id(req) == "abc123" + + def test_get_external_trace_id_from_args(self): + """Should extract valid trace_id from args if header missing""" + req = DummyRequest(args={"trace_id": "abc123"}) + assert get_external_trace_id(req) == "abc123" + + def test_get_external_trace_id_from_json(self): + """Should extract valid trace_id from JSON body if header and args missing""" + req = DummyRequest(is_json=True, json={"trace_id": "abc123"}) + assert get_external_trace_id(req) == "abc123" + + def test_get_external_trace_id_priority(self): + """Header > args > json priority""" + req = DummyRequest( + headers={"X-Trace-Id": "header_id"}, + args={"trace_id": "args_id"}, + is_json=True, + json={"trace_id": "json_id"}, + ) + assert get_external_trace_id(req) == "header_id" + req2 = DummyRequest(args={"trace_id": "args_id"}, is_json=True, json={"trace_id": "json_id"}) + assert get_external_trace_id(req2) == "args_id" + req3 = DummyRequest(is_json=True, json={"trace_id": "json_id"}) + assert get_external_trace_id(req3) == "json_id" + + @pytest.mark.parametrize( + "req", + [ + DummyRequest(headers={"X-Trace-Id": "!!!"}), + DummyRequest(args={"trace_id": "!!!"}), + DummyRequest(is_json=True, json={"trace_id": "!!!"}), + DummyRequest(), + ], + ) + def test_get_external_trace_id_invalid(self, req): + """Should return None for invalid or missing trace_id""" + assert get_external_trace_id(req) is None + + @pytest.mark.parametrize( + ("args", "expected"), + [ + ({"external_trace_id": "abc123"}, {"external_trace_id": "abc123"}), + ({"other": "value"}, {}), + ({}, {}), + ], + ) + def test_extract_external_trace_id_from_args(self, args, expected): + """Test extraction of external_trace_id from args mapping""" + assert extract_external_trace_id_from_args(args) == expected diff --git a/api/tests/unit_tests/libs/test_email_i18n.py b/api/tests/unit_tests/libs/test_email_i18n.py new file mode 100644 index 0000000000..aeb30438e0 --- /dev/null +++ b/api/tests/unit_tests/libs/test_email_i18n.py @@ -0,0 +1,539 @@ +""" +Unit tests for EmailI18nService + +Tests the email internationalization service with mocked dependencies +following Domain-Driven Design principles. +""" + +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from libs.email_i18n import ( + EmailI18nConfig, + EmailI18nService, + EmailLanguage, + EmailTemplate, + EmailType, + FlaskEmailRenderer, + FlaskMailSender, + create_default_email_config, + get_email_i18n_service, +) +from services.feature_service import BrandingModel + + +class MockEmailRenderer: + """Mock implementation of EmailRenderer protocol""" + + def __init__(self) -> None: + self.rendered_templates: list[tuple[str, dict[str, Any]]] = [] + + def render_template(self, template_path: str, **context: Any) -> str: + """Mock render_template that returns a formatted string""" + self.rendered_templates.append((template_path, context)) + return f"Rendered {template_path} with {context}" + + +class MockBrandingService: + """Mock implementation of BrandingService protocol""" + + def __init__(self, enabled: bool = False, application_title: str = "Dify") -> None: + self.enabled = enabled + self.application_title = application_title + + def get_branding_config(self) -> BrandingModel: + """Return mock branding configuration""" + branding_model = MagicMock(spec=BrandingModel) + branding_model.enabled = self.enabled + branding_model.application_title = self.application_title + return branding_model + + +class MockEmailSender: + """Mock implementation of EmailSender protocol""" + + def __init__(self) -> None: + self.sent_emails: list[dict[str, str]] = [] + + def send_email(self, to: str, subject: str, html_content: str) -> None: + """Mock send_email that records sent emails""" + self.sent_emails.append( + { + "to": to, + "subject": subject, + "html_content": html_content, + } + ) + + +class TestEmailI18nService: + """Test cases for EmailI18nService""" + + @pytest.fixture + def email_config(self) -> EmailI18nConfig: + """Create test email configuration""" + return EmailI18nConfig( + templates={ + EmailType.RESET_PASSWORD: { + EmailLanguage.EN_US: EmailTemplate( + subject="Reset Your {application_title} Password", + template_path="reset_password_en.html", + branded_template_path="branded/reset_password_en.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="重置您的 {application_title} 密码", + template_path="reset_password_zh.html", + branded_template_path="branded/reset_password_zh.html", + ), + }, + EmailType.INVITE_MEMBER: { + EmailLanguage.EN_US: EmailTemplate( + subject="Join {application_title} Workspace", + template_path="invite_member_en.html", + branded_template_path="branded/invite_member_en.html", + ), + }, + } + ) + + @pytest.fixture + def mock_renderer(self) -> MockEmailRenderer: + """Create mock email renderer""" + return MockEmailRenderer() + + @pytest.fixture + def mock_branding_service(self) -> MockBrandingService: + """Create mock branding service""" + return MockBrandingService() + + @pytest.fixture + def mock_sender(self) -> MockEmailSender: + """Create mock email sender""" + return MockEmailSender() + + @pytest.fixture + def email_service( + self, + email_config: EmailI18nConfig, + mock_renderer: MockEmailRenderer, + mock_branding_service: MockBrandingService, + mock_sender: MockEmailSender, + ) -> EmailI18nService: + """Create EmailI18nService with mocked dependencies""" + return EmailI18nService( + config=email_config, + renderer=mock_renderer, + branding_service=mock_branding_service, + sender=mock_sender, + ) + + def test_send_email_with_english_language( + self, + email_service: EmailI18nService, + mock_renderer: MockEmailRenderer, + mock_sender: MockEmailSender, + ) -> None: + """Test sending email with English language""" + email_service.send_email( + email_type=EmailType.RESET_PASSWORD, + language_code="en-US", + to="test@example.com", + template_context={"reset_link": "https://example.com/reset"}, + ) + + # Verify renderer was called with correct template + assert len(mock_renderer.rendered_templates) == 1 + template_path, context = mock_renderer.rendered_templates[0] + assert template_path == "reset_password_en.html" + assert context["reset_link"] == "https://example.com/reset" + assert context["branding_enabled"] is False + assert context["application_title"] == "Dify" + + # Verify email was sent + assert len(mock_sender.sent_emails) == 1 + sent_email = mock_sender.sent_emails[0] + assert sent_email["to"] == "test@example.com" + assert sent_email["subject"] == "Reset Your Dify Password" + assert "reset_password_en.html" in sent_email["html_content"] + + def test_send_email_with_chinese_language( + self, + email_service: EmailI18nService, + mock_sender: MockEmailSender, + ) -> None: + """Test sending email with Chinese language""" + email_service.send_email( + email_type=EmailType.RESET_PASSWORD, + language_code="zh-Hans", + to="test@example.com", + template_context={"reset_link": "https://example.com/reset"}, + ) + + # Verify email was sent with Chinese subject + assert len(mock_sender.sent_emails) == 1 + sent_email = mock_sender.sent_emails[0] + assert sent_email["subject"] == "重置您的 Dify 密码" + + def test_send_email_with_branding_enabled( + self, + email_config: EmailI18nConfig, + mock_renderer: MockEmailRenderer, + mock_sender: MockEmailSender, + ) -> None: + """Test sending email with branding enabled""" + # Create branding service with branding enabled + branding_service = MockBrandingService(enabled=True, application_title="MyApp") + + email_service = EmailI18nService( + config=email_config, + renderer=mock_renderer, + branding_service=branding_service, + sender=mock_sender, + ) + + email_service.send_email( + email_type=EmailType.RESET_PASSWORD, + language_code="en-US", + to="test@example.com", + ) + + # Verify branded template was used + assert len(mock_renderer.rendered_templates) == 1 + template_path, context = mock_renderer.rendered_templates[0] + assert template_path == "branded/reset_password_en.html" + assert context["branding_enabled"] is True + assert context["application_title"] == "MyApp" + + # Verify subject includes custom application title + assert len(mock_sender.sent_emails) == 1 + sent_email = mock_sender.sent_emails[0] + assert sent_email["subject"] == "Reset Your MyApp Password" + + def test_send_email_with_language_fallback( + self, + email_service: EmailI18nService, + mock_sender: MockEmailSender, + ) -> None: + """Test language fallback to English when requested language not available""" + # Request invite member in Chinese (not configured) + email_service.send_email( + email_type=EmailType.INVITE_MEMBER, + language_code="zh-Hans", + to="test@example.com", + ) + + # Should fall back to English + assert len(mock_sender.sent_emails) == 1 + sent_email = mock_sender.sent_emails[0] + assert sent_email["subject"] == "Join Dify Workspace" + + def test_send_email_with_unknown_language_code( + self, + email_service: EmailI18nService, + mock_sender: MockEmailSender, + ) -> None: + """Test unknown language code falls back to English""" + email_service.send_email( + email_type=EmailType.RESET_PASSWORD, + language_code="fr-FR", # French not configured + to="test@example.com", + ) + + # Should use English + assert len(mock_sender.sent_emails) == 1 + sent_email = mock_sender.sent_emails[0] + assert sent_email["subject"] == "Reset Your Dify Password" + + def test_send_change_email_old_phase( + self, + email_config: EmailI18nConfig, + mock_renderer: MockEmailRenderer, + mock_sender: MockEmailSender, + mock_branding_service: MockBrandingService, + ) -> None: + """Test sending change email for old email verification""" + # Add change email templates to config + email_config.templates[EmailType.CHANGE_EMAIL_OLD] = { + EmailLanguage.EN_US: EmailTemplate( + subject="Verify your current email", + template_path="change_email_old_en.html", + branded_template_path="branded/change_email_old_en.html", + ), + } + + email_service = EmailI18nService( + config=email_config, + renderer=mock_renderer, + branding_service=mock_branding_service, + sender=mock_sender, + ) + + email_service.send_change_email( + language_code="en-US", + to="old@example.com", + code="123456", + phase="old_email", + ) + + # Verify correct template and context + assert len(mock_renderer.rendered_templates) == 1 + template_path, context = mock_renderer.rendered_templates[0] + assert template_path == "change_email_old_en.html" + assert context["to"] == "old@example.com" + assert context["code"] == "123456" + + def test_send_change_email_new_phase( + self, + email_config: EmailI18nConfig, + mock_renderer: MockEmailRenderer, + mock_sender: MockEmailSender, + mock_branding_service: MockBrandingService, + ) -> None: + """Test sending change email for new email verification""" + # Add change email templates to config + email_config.templates[EmailType.CHANGE_EMAIL_NEW] = { + EmailLanguage.EN_US: EmailTemplate( + subject="Verify your new email", + template_path="change_email_new_en.html", + branded_template_path="branded/change_email_new_en.html", + ), + } + + email_service = EmailI18nService( + config=email_config, + renderer=mock_renderer, + branding_service=mock_branding_service, + sender=mock_sender, + ) + + email_service.send_change_email( + language_code="en-US", + to="new@example.com", + code="654321", + phase="new_email", + ) + + # Verify correct template and context + assert len(mock_renderer.rendered_templates) == 1 + template_path, context = mock_renderer.rendered_templates[0] + assert template_path == "change_email_new_en.html" + assert context["to"] == "new@example.com" + assert context["code"] == "654321" + + def test_send_change_email_invalid_phase( + self, + email_service: EmailI18nService, + ) -> None: + """Test sending change email with invalid phase raises error""" + with pytest.raises(ValueError, match="Invalid phase: invalid_phase"): + email_service.send_change_email( + language_code="en-US", + to="test@example.com", + code="123456", + phase="invalid_phase", + ) + + def test_send_raw_email_single_recipient( + self, + email_service: EmailI18nService, + mock_sender: MockEmailSender, + ) -> None: + """Test sending raw email to single recipient""" + email_service.send_raw_email( + to="test@example.com", + subject="Test Subject", + html_content="Test Content", + ) + + assert len(mock_sender.sent_emails) == 1 + sent_email = mock_sender.sent_emails[0] + assert sent_email["to"] == "test@example.com" + assert sent_email["subject"] == "Test Subject" + assert sent_email["html_content"] == "Test Content" + + def test_send_raw_email_multiple_recipients( + self, + email_service: EmailI18nService, + mock_sender: MockEmailSender, + ) -> None: + """Test sending raw email to multiple recipients""" + recipients = ["user1@example.com", "user2@example.com", "user3@example.com"] + + email_service.send_raw_email( + to=recipients, + subject="Test Subject", + html_content="Test Content", + ) + + # Should send individual emails to each recipient + assert len(mock_sender.sent_emails) == 3 + for i, recipient in enumerate(recipients): + sent_email = mock_sender.sent_emails[i] + assert sent_email["to"] == recipient + assert sent_email["subject"] == "Test Subject" + assert sent_email["html_content"] == "Test Content" + + def test_get_template_missing_email_type( + self, + email_config: EmailI18nConfig, + ) -> None: + """Test getting template for missing email type raises error""" + with pytest.raises(ValueError, match="No templates configured for email type"): + email_config.get_template(EmailType.EMAIL_CODE_LOGIN, EmailLanguage.EN_US) + + def test_get_template_missing_language_and_english( + self, + email_config: EmailI18nConfig, + ) -> None: + """Test error when neither requested language nor English fallback exists""" + # Add template without English fallback + email_config.templates[EmailType.EMAIL_CODE_LOGIN] = { + EmailLanguage.ZH_HANS: EmailTemplate( + subject="Test", + template_path="test.html", + branded_template_path="branded/test.html", + ), + } + + with pytest.raises(ValueError, match="No template found for"): + # Request a language that doesn't exist and no English fallback + email_config.get_template(EmailType.EMAIL_CODE_LOGIN, EmailLanguage.EN_US) + + def test_subject_templating_with_variables( + self, + email_config: EmailI18nConfig, + mock_renderer: MockEmailRenderer, + mock_sender: MockEmailSender, + mock_branding_service: MockBrandingService, + ) -> None: + """Test subject templating with custom variables""" + # Add template with variable in subject + email_config.templates[EmailType.OWNER_TRANSFER_NEW_NOTIFY] = { + EmailLanguage.EN_US: EmailTemplate( + subject="You are now the owner of {WorkspaceName}", + template_path="owner_transfer_en.html", + branded_template_path="branded/owner_transfer_en.html", + ), + } + + email_service = EmailI18nService( + config=email_config, + renderer=mock_renderer, + branding_service=mock_branding_service, + sender=mock_sender, + ) + + email_service.send_email( + email_type=EmailType.OWNER_TRANSFER_NEW_NOTIFY, + language_code="en-US", + to="test@example.com", + template_context={"WorkspaceName": "My Workspace"}, + ) + + # Verify subject was templated correctly + assert len(mock_sender.sent_emails) == 1 + sent_email = mock_sender.sent_emails[0] + assert sent_email["subject"] == "You are now the owner of My Workspace" + + def test_email_language_from_language_code(self) -> None: + """Test EmailLanguage.from_language_code method""" + assert EmailLanguage.from_language_code("zh-Hans") == EmailLanguage.ZH_HANS + assert EmailLanguage.from_language_code("en-US") == EmailLanguage.EN_US + assert EmailLanguage.from_language_code("fr-FR") == EmailLanguage.EN_US # Fallback + assert EmailLanguage.from_language_code("unknown") == EmailLanguage.EN_US # Fallback + + +class TestEmailI18nIntegration: + """Integration tests for email i18n components""" + + def test_create_default_email_config(self) -> None: + """Test creating default email configuration""" + config = create_default_email_config() + + # Verify key email types have at least English template + expected_types = [ + EmailType.RESET_PASSWORD, + EmailType.INVITE_MEMBER, + EmailType.EMAIL_CODE_LOGIN, + EmailType.CHANGE_EMAIL_OLD, + EmailType.CHANGE_EMAIL_NEW, + EmailType.OWNER_TRANSFER_CONFIRM, + EmailType.OWNER_TRANSFER_OLD_NOTIFY, + EmailType.OWNER_TRANSFER_NEW_NOTIFY, + EmailType.ACCOUNT_DELETION_SUCCESS, + EmailType.ACCOUNT_DELETION_VERIFICATION, + EmailType.QUEUE_MONITOR_ALERT, + EmailType.DOCUMENT_CLEAN_NOTIFY, + ] + + for email_type in expected_types: + assert email_type in config.templates + assert EmailLanguage.EN_US in config.templates[email_type] + + # Verify some have Chinese translations + assert EmailLanguage.ZH_HANS in config.templates[EmailType.RESET_PASSWORD] + assert EmailLanguage.ZH_HANS in config.templates[EmailType.INVITE_MEMBER] + + def test_get_email_i18n_service(self) -> None: + """Test getting global email i18n service instance""" + service1 = get_email_i18n_service() + service2 = get_email_i18n_service() + + # Should return the same instance + assert service1 is service2 + + def test_flask_email_renderer(self) -> None: + """Test FlaskEmailRenderer implementation""" + renderer = FlaskEmailRenderer() + + # Should raise TemplateNotFound when template doesn't exist + from jinja2.exceptions import TemplateNotFound + + with pytest.raises(TemplateNotFound): + renderer.render_template("test.html", foo="bar") + + def test_flask_mail_sender_not_initialized(self) -> None: + """Test FlaskMailSender when mail is not initialized""" + sender = FlaskMailSender() + + # Mock mail.is_inited() to return False + import libs.email_i18n + + original_mail = libs.email_i18n.mail + mock_mail = MagicMock() + mock_mail.is_inited.return_value = False + libs.email_i18n.mail = mock_mail + + try: + # Should not send email when mail is not initialized + sender.send_email("test@example.com", "Subject", "Content") + mock_mail.send.assert_not_called() + finally: + # Restore original mail + libs.email_i18n.mail = original_mail + + def test_flask_mail_sender_initialized(self) -> None: + """Test FlaskMailSender when mail is initialized""" + sender = FlaskMailSender() + + # Mock mail.is_inited() to return True + import libs.email_i18n + + original_mail = libs.email_i18n.mail + mock_mail = MagicMock() + mock_mail.is_inited.return_value = True + libs.email_i18n.mail = mock_mail + + try: + # Should send email when mail is initialized + sender.send_email("test@example.com", "Subject", "Content") + mock_mail.send.assert_called_once_with( + to="test@example.com", + subject="Subject", + html="Content", + ) + finally: + # Restore original mail + libs.email_i18n.mail = original_mail diff --git a/api/tests/unit_tests/models/test_types_enum_text.py b/api/tests/unit_tests/models/test_types_enum_text.py index 3afa0f17a0..908b5a5360 100644 --- a/api/tests/unit_tests/models/test_types_enum_text.py +++ b/api/tests/unit_tests/models/test_types_enum_text.py @@ -6,7 +6,7 @@ import pytest import sqlalchemy as sa from sqlalchemy import exc as sa_exc from sqlalchemy import insert -from sqlalchemy.orm import DeclarativeBase, Mapped, Session +from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column from sqlalchemy.sql.sqltypes import VARCHAR from models.types import EnumText @@ -32,22 +32,26 @@ class _EnumWithLongValue(StrEnum): class _User(_Base): __tablename__ = "users" - id: Mapped[int] = sa.Column(sa.Integer, primary_key=True) - name: Mapped[str] = sa.Column(sa.String(length=255), nullable=False) - user_type: Mapped[_UserType] = sa.Column(EnumText(enum_class=_UserType), nullable=False, default=_UserType.normal) - user_type_nullable: Mapped[_UserType | None] = sa.Column(EnumText(enum_class=_UserType), nullable=True) + id: Mapped[int] = mapped_column(sa.Integer, primary_key=True) + name: Mapped[str] = mapped_column(sa.String(length=255), nullable=False) + user_type: Mapped[_UserType] = mapped_column( + EnumText(enum_class=_UserType), nullable=False, default=_UserType.normal + ) + user_type_nullable: Mapped[_UserType | None] = mapped_column(EnumText(enum_class=_UserType), nullable=True) class _ColumnTest(_Base): __tablename__ = "column_test" - id: Mapped[int] = sa.Column(sa.Integer, primary_key=True) + id: Mapped[int] = mapped_column(sa.Integer, primary_key=True) - user_type: Mapped[_UserType] = sa.Column(EnumText(enum_class=_UserType), nullable=False, default=_UserType.normal) - explicit_length: Mapped[_UserType | None] = sa.Column( + user_type: Mapped[_UserType] = mapped_column( + EnumText(enum_class=_UserType), nullable=False, default=_UserType.normal + ) + explicit_length: Mapped[_UserType | None] = mapped_column( EnumText(_UserType, length=50), nullable=True, default=_UserType.normal ) - long_value: Mapped[_EnumWithLongValue] = sa.Column(EnumText(enum_class=_EnumWithLongValue), nullable=False) + long_value: Mapped[_EnumWithLongValue] = mapped_column(EnumText(enum_class=_EnumWithLongValue), nullable=False) _T = TypeVar("_T") diff --git a/api/tests/unit_tests/services/auth/test_api_key_auth_base.py b/api/tests/unit_tests/services/auth/test_api_key_auth_base.py new file mode 100644 index 0000000000..b5d91ef3fb --- /dev/null +++ b/api/tests/unit_tests/services/auth/test_api_key_auth_base.py @@ -0,0 +1,49 @@ +import pytest + +from services.auth.api_key_auth_base import ApiKeyAuthBase + + +class ConcreteApiKeyAuth(ApiKeyAuthBase): + """Concrete implementation for testing abstract base class""" + + def validate_credentials(self): + return True + + +class TestApiKeyAuthBase: + def test_should_store_credentials_on_init(self): + """Test that credentials are properly stored during initialization""" + credentials = {"api_key": "test_key", "auth_type": "bearer"} + auth = ConcreteApiKeyAuth(credentials) + assert auth.credentials == credentials + + def test_should_not_instantiate_abstract_class(self): + """Test that ApiKeyAuthBase cannot be instantiated directly""" + credentials = {"api_key": "test_key"} + + with pytest.raises(TypeError) as exc_info: + ApiKeyAuthBase(credentials) + + assert "Can't instantiate abstract class" in str(exc_info.value) + assert "validate_credentials" in str(exc_info.value) + + def test_should_allow_subclass_implementation(self): + """Test that subclasses can properly implement the abstract method""" + credentials = {"api_key": "test_key", "auth_type": "bearer"} + auth = ConcreteApiKeyAuth(credentials) + + # Should not raise any exception + result = auth.validate_credentials() + assert result is True + + def test_should_handle_empty_credentials(self): + """Test initialization with empty credentials""" + credentials = {} + auth = ConcreteApiKeyAuth(credentials) + assert auth.credentials == {} + + def test_should_handle_none_credentials(self): + """Test initialization with None credentials""" + credentials = None + auth = ConcreteApiKeyAuth(credentials) + assert auth.credentials is None diff --git a/api/tests/unit_tests/services/auth/test_api_key_auth_factory.py b/api/tests/unit_tests/services/auth/test_api_key_auth_factory.py new file mode 100644 index 0000000000..9d9cb7c6d5 --- /dev/null +++ b/api/tests/unit_tests/services/auth/test_api_key_auth_factory.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from services.auth.api_key_auth_factory import ApiKeyAuthFactory +from services.auth.auth_type import AuthType + + +class TestApiKeyAuthFactory: + """Test cases for ApiKeyAuthFactory""" + + @pytest.mark.parametrize( + ("provider", "auth_class_path"), + [ + (AuthType.FIRECRAWL, "services.auth.firecrawl.firecrawl.FirecrawlAuth"), + (AuthType.WATERCRAWL, "services.auth.watercrawl.watercrawl.WatercrawlAuth"), + (AuthType.JINA, "services.auth.jina.jina.JinaAuth"), + ], + ) + def test_get_apikey_auth_factory_valid_providers(self, provider, auth_class_path): + """Test getting auth factory for all valid providers""" + with patch(auth_class_path) as mock_auth: + auth_class = ApiKeyAuthFactory.get_apikey_auth_factory(provider) + assert auth_class == mock_auth + + @pytest.mark.parametrize( + "invalid_provider", + [ + "invalid_provider", + "", + None, + 123, + "UNSUPPORTED", + ], + ) + def test_get_apikey_auth_factory_invalid_providers(self, invalid_provider): + """Test getting auth factory with various invalid providers""" + with pytest.raises(ValueError) as exc_info: + ApiKeyAuthFactory.get_apikey_auth_factory(invalid_provider) + assert str(exc_info.value) == "Invalid provider" + + @pytest.mark.parametrize( + ("credentials_return_value", "expected_result"), + [ + (True, True), + (False, False), + ], + ) + @patch("services.auth.api_key_auth_factory.ApiKeyAuthFactory.get_apikey_auth_factory") + def test_validate_credentials_delegates_to_auth_instance( + self, mock_get_factory, credentials_return_value, expected_result + ): + """Test that validate_credentials delegates to auth instance correctly""" + # Arrange + mock_auth_instance = MagicMock() + mock_auth_instance.validate_credentials.return_value = credentials_return_value + mock_auth_class = MagicMock(return_value=mock_auth_instance) + mock_get_factory.return_value = mock_auth_class + + # Act + factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, {"api_key": "test_key"}) + result = factory.validate_credentials() + + # Assert + assert result is expected_result + mock_auth_instance.validate_credentials.assert_called_once() + + @patch("services.auth.api_key_auth_factory.ApiKeyAuthFactory.get_apikey_auth_factory") + def test_validate_credentials_propagates_exceptions(self, mock_get_factory): + """Test that exceptions from auth instance are propagated""" + # Arrange + mock_auth_instance = MagicMock() + mock_auth_instance.validate_credentials.side_effect = Exception("Authentication error") + mock_auth_class = MagicMock(return_value=mock_auth_instance) + mock_get_factory.return_value = mock_auth_class + + # Act & Assert + factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, {"api_key": "test_key"}) + with pytest.raises(Exception) as exc_info: + factory.validate_credentials() + assert str(exc_info.value) == "Authentication error" diff --git a/api/tests/unit_tests/services/auth/test_auth_type.py b/api/tests/unit_tests/services/auth/test_auth_type.py new file mode 100644 index 0000000000..94073f451e --- /dev/null +++ b/api/tests/unit_tests/services/auth/test_auth_type.py @@ -0,0 +1,150 @@ +import pytest + +from services.auth.auth_type import AuthType + + +class TestAuthType: + """Test cases for AuthType enum""" + + def test_auth_type_is_str_enum(self): + """Test that AuthType is properly a StrEnum""" + assert issubclass(AuthType, str) + assert hasattr(AuthType, "__members__") + + def test_auth_type_has_expected_values(self): + """Test that all expected auth types exist with correct values""" + expected_values = { + "FIRECRAWL": "firecrawl", + "WATERCRAWL": "watercrawl", + "JINA": "jinareader", + } + + # Verify all expected members exist + for member_name, expected_value in expected_values.items(): + assert hasattr(AuthType, member_name) + assert getattr(AuthType, member_name).value == expected_value + + # Verify no extra members exist + assert len(AuthType) == len(expected_values) + + @pytest.mark.parametrize( + ("auth_type", "expected_string"), + [ + (AuthType.FIRECRAWL, "firecrawl"), + (AuthType.WATERCRAWL, "watercrawl"), + (AuthType.JINA, "jinareader"), + ], + ) + def test_auth_type_string_representation(self, auth_type, expected_string): + """Test string representation of auth types""" + assert str(auth_type) == expected_string + assert auth_type.value == expected_string + + @pytest.mark.parametrize( + ("auth_type", "compare_value", "expected_result"), + [ + (AuthType.FIRECRAWL, "firecrawl", True), + (AuthType.WATERCRAWL, "watercrawl", True), + (AuthType.JINA, "jinareader", True), + (AuthType.FIRECRAWL, "FIRECRAWL", False), # Case sensitive + (AuthType.FIRECRAWL, "watercrawl", False), + (AuthType.JINA, "jina", False), # Full value mismatch + ], + ) + def test_auth_type_comparison(self, auth_type, compare_value, expected_result): + """Test auth type comparison with strings""" + assert (auth_type == compare_value) is expected_result + + def test_auth_type_iteration(self): + """Test that AuthType can be iterated over""" + auth_types = list(AuthType) + assert len(auth_types) == 3 + assert AuthType.FIRECRAWL in auth_types + assert AuthType.WATERCRAWL in auth_types + assert AuthType.JINA in auth_types + + def test_auth_type_membership(self): + """Test membership checking for AuthType""" + assert "firecrawl" in [auth.value for auth in AuthType] + assert "watercrawl" in [auth.value for auth in AuthType] + assert "jinareader" in [auth.value for auth in AuthType] + assert "invalid" not in [auth.value for auth in AuthType] + + def test_auth_type_invalid_attribute_access(self): + """Test accessing non-existent auth type raises AttributeError""" + with pytest.raises(AttributeError): + _ = AuthType.INVALID_TYPE + + def test_auth_type_immutability(self): + """Test that enum values cannot be modified""" + # In Python 3.11+, enum members are read-only + with pytest.raises(AttributeError): + AuthType.FIRECRAWL = "modified" + + def test_auth_type_from_value(self): + """Test creating AuthType from string value""" + assert AuthType("firecrawl") == AuthType.FIRECRAWL + assert AuthType("watercrawl") == AuthType.WATERCRAWL + assert AuthType("jinareader") == AuthType.JINA + + # Test invalid value + with pytest.raises(ValueError) as exc_info: + AuthType("invalid_auth_type") + assert "invalid_auth_type" in str(exc_info.value) + + def test_auth_type_name_property(self): + """Test the name property of enum members""" + assert AuthType.FIRECRAWL.name == "FIRECRAWL" + assert AuthType.WATERCRAWL.name == "WATERCRAWL" + assert AuthType.JINA.name == "JINA" + + @pytest.mark.parametrize( + "auth_type", + [AuthType.FIRECRAWL, AuthType.WATERCRAWL, AuthType.JINA], + ) + def test_auth_type_isinstance_checks(self, auth_type): + """Test isinstance checks for auth types""" + assert isinstance(auth_type, AuthType) + assert isinstance(auth_type, str) + assert isinstance(auth_type.value, str) + + def test_auth_type_hash(self): + """Test that auth types are hashable and can be used in sets/dicts""" + auth_set = {AuthType.FIRECRAWL, AuthType.WATERCRAWL, AuthType.JINA} + assert len(auth_set) == 3 + + auth_dict = { + AuthType.FIRECRAWL: "firecrawl_handler", + AuthType.WATERCRAWL: "watercrawl_handler", + AuthType.JINA: "jina_handler", + } + assert auth_dict[AuthType.FIRECRAWL] == "firecrawl_handler" + + def test_auth_type_json_serializable(self): + """Test that auth types can be JSON serialized""" + import json + + auth_data = { + "provider": AuthType.FIRECRAWL, + "enabled": True, + } + + # Should serialize to string value + json_str = json.dumps(auth_data, default=str) + assert '"provider": "firecrawl"' in json_str + + def test_auth_type_matches_factory_usage(self): + """Test that all AuthType values are handled by ApiKeyAuthFactory""" + # This test verifies that the enum values match what's expected + # by the factory implementation + from services.auth.api_key_auth_factory import ApiKeyAuthFactory + + for auth_type in AuthType: + # Should not raise ValueError for valid auth types + try: + auth_class = ApiKeyAuthFactory.get_apikey_auth_factory(auth_type) + assert auth_class is not None + except ImportError: + # It's OK if the actual auth implementation doesn't exist + # We're just testing that the enum value is recognized + pass diff --git a/api/tests/unit_tests/services/auth/test_firecrawl_auth.py b/api/tests/unit_tests/services/auth/test_firecrawl_auth.py new file mode 100644 index 0000000000..ffdf5897ed --- /dev/null +++ b/api/tests/unit_tests/services/auth/test_firecrawl_auth.py @@ -0,0 +1,191 @@ +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from services.auth.firecrawl.firecrawl import FirecrawlAuth + + +class TestFirecrawlAuth: + @pytest.fixture + def valid_credentials(self): + """Fixture for valid bearer credentials""" + return {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + + @pytest.fixture + def auth_instance(self, valid_credentials): + """Fixture for FirecrawlAuth instance with valid credentials""" + return FirecrawlAuth(valid_credentials) + + def test_should_initialize_with_valid_bearer_credentials(self, valid_credentials): + """Test successful initialization with valid bearer credentials""" + auth = FirecrawlAuth(valid_credentials) + assert auth.api_key == "test_api_key_123" + assert auth.base_url == "https://api.firecrawl.dev" + assert auth.credentials == valid_credentials + + def test_should_initialize_with_custom_base_url(self): + """Test initialization with custom base URL""" + credentials = { + "auth_type": "bearer", + "config": {"api_key": "test_api_key_123", "base_url": "https://custom.firecrawl.dev"}, + } + auth = FirecrawlAuth(credentials) + assert auth.api_key == "test_api_key_123" + assert auth.base_url == "https://custom.firecrawl.dev" + + @pytest.mark.parametrize( + ("auth_type", "expected_error"), + [ + ("basic", "Invalid auth type, Firecrawl auth type must be Bearer"), + ("x-api-key", "Invalid auth type, Firecrawl auth type must be Bearer"), + ("", "Invalid auth type, Firecrawl auth type must be Bearer"), + ], + ) + def test_should_raise_error_for_invalid_auth_type(self, auth_type, expected_error): + """Test that non-bearer auth types raise ValueError""" + credentials = {"auth_type": auth_type, "config": {"api_key": "test_api_key_123"}} + with pytest.raises(ValueError) as exc_info: + FirecrawlAuth(credentials) + assert str(exc_info.value) == expected_error + + @pytest.mark.parametrize( + ("credentials", "expected_error"), + [ + ({"auth_type": "bearer", "config": {}}, "No API key provided"), + ({"auth_type": "bearer"}, "No API key provided"), + ({"auth_type": "bearer", "config": {"api_key": ""}}, "No API key provided"), + ({"auth_type": "bearer", "config": {"api_key": None}}, "No API key provided"), + ], + ) + def test_should_raise_error_for_missing_api_key(self, credentials, expected_error): + """Test that missing or empty API key raises ValueError""" + with pytest.raises(ValueError) as exc_info: + FirecrawlAuth(credentials) + assert str(exc_info.value) == expected_error + + @patch("services.auth.firecrawl.firecrawl.requests.post") + def test_should_validate_valid_credentials_successfully(self, mock_post, auth_instance): + """Test successful credential validation""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + result = auth_instance.validate_credentials() + + assert result is True + expected_data = { + "url": "https://example.com", + "includePaths": [], + "excludePaths": [], + "limit": 1, + "scrapeOptions": {"onlyMainContent": True}, + } + mock_post.assert_called_once_with( + "https://api.firecrawl.dev/v1/crawl", + headers={"Content-Type": "application/json", "Authorization": "Bearer test_api_key_123"}, + json=expected_data, + ) + + @pytest.mark.parametrize( + ("status_code", "error_message"), + [ + (402, "Payment required"), + (409, "Conflict error"), + (500, "Internal server error"), + ], + ) + @patch("services.auth.firecrawl.firecrawl.requests.post") + def test_should_handle_http_errors(self, mock_post, status_code, error_message, auth_instance): + """Test handling of various HTTP error codes""" + mock_response = MagicMock() + mock_response.status_code = status_code + mock_response.json.return_value = {"error": error_message} + mock_post.return_value = mock_response + + with pytest.raises(Exception) as exc_info: + auth_instance.validate_credentials() + assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}" + + @pytest.mark.parametrize( + ("status_code", "response_text", "has_json_error", "expected_error_contains"), + [ + (403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"), + (404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"), + (401, "Not JSON", True, "Expecting value"), # JSON decode error + ], + ) + @patch("services.auth.firecrawl.firecrawl.requests.post") + def test_should_handle_unexpected_errors( + self, mock_post, status_code, response_text, has_json_error, expected_error_contains, auth_instance + ): + """Test handling of unexpected errors with various response formats""" + mock_response = MagicMock() + mock_response.status_code = status_code + mock_response.text = response_text + if has_json_error: + mock_response.json.side_effect = Exception("Not JSON") + mock_post.return_value = mock_response + + with pytest.raises(Exception) as exc_info: + auth_instance.validate_credentials() + assert expected_error_contains in str(exc_info.value) + + @pytest.mark.parametrize( + ("exception_type", "exception_message"), + [ + (requests.ConnectionError, "Network error"), + (requests.Timeout, "Request timeout"), + (requests.ReadTimeout, "Read timeout"), + (requests.ConnectTimeout, "Connection timeout"), + ], + ) + @patch("services.auth.firecrawl.firecrawl.requests.post") + def test_should_handle_network_errors(self, mock_post, exception_type, exception_message, auth_instance): + """Test handling of various network-related errors including timeouts""" + mock_post.side_effect = exception_type(exception_message) + + with pytest.raises(exception_type) as exc_info: + auth_instance.validate_credentials() + assert exception_message in str(exc_info.value) + + def test_should_not_expose_api_key_in_error_messages(self): + """Test that API key is not exposed in error messages""" + credentials = {"auth_type": "bearer", "config": {"api_key": "super_secret_key_12345"}} + auth = FirecrawlAuth(credentials) + + # Verify API key is stored but not in any error message + assert auth.api_key == "super_secret_key_12345" + + # Test various error scenarios don't expose the key + with pytest.raises(ValueError) as exc_info: + FirecrawlAuth({"auth_type": "basic", "config": {"api_key": "super_secret_key_12345"}}) + assert "super_secret_key_12345" not in str(exc_info.value) + + @patch("services.auth.firecrawl.firecrawl.requests.post") + def test_should_use_custom_base_url_in_validation(self, mock_post): + """Test that custom base URL is used in validation""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + credentials = { + "auth_type": "bearer", + "config": {"api_key": "test_api_key_123", "base_url": "https://custom.firecrawl.dev"}, + } + auth = FirecrawlAuth(credentials) + result = auth.validate_credentials() + + assert result is True + assert mock_post.call_args[0][0] == "https://custom.firecrawl.dev/v1/crawl" + + @patch("services.auth.firecrawl.firecrawl.requests.post") + def test_should_handle_timeout_with_retry_suggestion(self, mock_post, auth_instance): + """Test that timeout errors are handled gracefully with appropriate error message""" + mock_post.side_effect = requests.Timeout("The request timed out after 30 seconds") + + with pytest.raises(requests.Timeout) as exc_info: + auth_instance.validate_credentials() + + # Verify the timeout exception is raised with original message + assert "timed out" in str(exc_info.value) diff --git a/api/tests/unit_tests/services/auth/test_jina_auth.py b/api/tests/unit_tests/services/auth/test_jina_auth.py new file mode 100644 index 0000000000..ccbca5a36f --- /dev/null +++ b/api/tests/unit_tests/services/auth/test_jina_auth.py @@ -0,0 +1,155 @@ +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from services.auth.jina.jina import JinaAuth + + +class TestJinaAuth: + def test_should_initialize_with_valid_bearer_credentials(self): + """Test successful initialization with valid bearer credentials""" + credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + auth = JinaAuth(credentials) + assert auth.api_key == "test_api_key_123" + assert auth.credentials == credentials + + def test_should_raise_error_for_invalid_auth_type(self): + """Test that non-bearer auth type raises ValueError""" + credentials = {"auth_type": "basic", "config": {"api_key": "test_api_key_123"}} + with pytest.raises(ValueError) as exc_info: + JinaAuth(credentials) + assert str(exc_info.value) == "Invalid auth type, Jina Reader auth type must be Bearer" + + def test_should_raise_error_for_missing_api_key(self): + """Test that missing API key raises ValueError""" + credentials = {"auth_type": "bearer", "config": {}} + with pytest.raises(ValueError) as exc_info: + JinaAuth(credentials) + assert str(exc_info.value) == "No API key provided" + + def test_should_raise_error_for_missing_config(self): + """Test that missing config section raises ValueError""" + credentials = {"auth_type": "bearer"} + with pytest.raises(ValueError) as exc_info: + JinaAuth(credentials) + assert str(exc_info.value) == "No API key provided" + + @patch("services.auth.jina.jina.requests.post") + def test_should_validate_valid_credentials_successfully(self, mock_post): + """Test successful credential validation""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + auth = JinaAuth(credentials) + result = auth.validate_credentials() + + assert result is True + mock_post.assert_called_once_with( + "https://r.jina.ai", + headers={"Content-Type": "application/json", "Authorization": "Bearer test_api_key_123"}, + json={"url": "https://example.com"}, + ) + + @patch("services.auth.jina.jina.requests.post") + def test_should_handle_http_402_error(self, mock_post): + """Test handling of 402 Payment Required error""" + mock_response = MagicMock() + mock_response.status_code = 402 + mock_response.json.return_value = {"error": "Payment required"} + mock_post.return_value = mock_response + + credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + auth = JinaAuth(credentials) + + with pytest.raises(Exception) as exc_info: + auth.validate_credentials() + assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required" + + @patch("services.auth.jina.jina.requests.post") + def test_should_handle_http_409_error(self, mock_post): + """Test handling of 409 Conflict error""" + mock_response = MagicMock() + mock_response.status_code = 409 + mock_response.json.return_value = {"error": "Conflict error"} + mock_post.return_value = mock_response + + credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + auth = JinaAuth(credentials) + + with pytest.raises(Exception) as exc_info: + auth.validate_credentials() + assert str(exc_info.value) == "Failed to authorize. Status code: 409. Error: Conflict error" + + @patch("services.auth.jina.jina.requests.post") + def test_should_handle_http_500_error(self, mock_post): + """Test handling of 500 Internal Server Error""" + mock_response = MagicMock() + mock_response.status_code = 500 + mock_response.json.return_value = {"error": "Internal server error"} + mock_post.return_value = mock_response + + credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + auth = JinaAuth(credentials) + + with pytest.raises(Exception) as exc_info: + auth.validate_credentials() + assert str(exc_info.value) == "Failed to authorize. Status code: 500. Error: Internal server error" + + @patch("services.auth.jina.jina.requests.post") + def test_should_handle_unexpected_error_with_text_response(self, mock_post): + """Test handling of unexpected errors with text response""" + mock_response = MagicMock() + mock_response.status_code = 403 + mock_response.text = '{"error": "Forbidden"}' + mock_response.json.side_effect = Exception("Not JSON") + mock_post.return_value = mock_response + + credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + auth = JinaAuth(credentials) + + with pytest.raises(Exception) as exc_info: + auth.validate_credentials() + assert str(exc_info.value) == "Failed to authorize. Status code: 403. Error: Forbidden" + + @patch("services.auth.jina.jina.requests.post") + def test_should_handle_unexpected_error_without_text(self, mock_post): + """Test handling of unexpected errors without text response""" + mock_response = MagicMock() + mock_response.status_code = 404 + mock_response.text = "" + mock_response.json.side_effect = Exception("Not JSON") + mock_post.return_value = mock_response + + credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + auth = JinaAuth(credentials) + + with pytest.raises(Exception) as exc_info: + auth.validate_credentials() + assert str(exc_info.value) == "Unexpected error occurred while trying to authorize. Status code: 404" + + @patch("services.auth.jina.jina.requests.post") + def test_should_handle_network_errors(self, mock_post): + """Test handling of network connection errors""" + mock_post.side_effect = requests.ConnectionError("Network error") + + credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}} + auth = JinaAuth(credentials) + + with pytest.raises(requests.ConnectionError): + auth.validate_credentials() + + def test_should_not_expose_api_key_in_error_messages(self): + """Test that API key is not exposed in error messages""" + credentials = {"auth_type": "bearer", "config": {"api_key": "super_secret_key_12345"}} + auth = JinaAuth(credentials) + + # Verify API key is stored but not in any error message + assert auth.api_key == "super_secret_key_12345" + + # Test various error scenarios don't expose the key + with pytest.raises(ValueError) as exc_info: + JinaAuth({"auth_type": "basic", "config": {"api_key": "super_secret_key_12345"}}) + assert "super_secret_key_12345" not in str(exc_info.value) diff --git a/api/tests/unit_tests/services/auth/test_watercrawl_auth.py b/api/tests/unit_tests/services/auth/test_watercrawl_auth.py new file mode 100644 index 0000000000..bacf0b24ea --- /dev/null +++ b/api/tests/unit_tests/services/auth/test_watercrawl_auth.py @@ -0,0 +1,205 @@ +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from services.auth.watercrawl.watercrawl import WatercrawlAuth + + +class TestWatercrawlAuth: + @pytest.fixture + def valid_credentials(self): + """Fixture for valid x-api-key credentials""" + return {"auth_type": "x-api-key", "config": {"api_key": "test_api_key_123"}} + + @pytest.fixture + def auth_instance(self, valid_credentials): + """Fixture for WatercrawlAuth instance with valid credentials""" + return WatercrawlAuth(valid_credentials) + + def test_should_initialize_with_valid_x_api_key_credentials(self, valid_credentials): + """Test successful initialization with valid x-api-key credentials""" + auth = WatercrawlAuth(valid_credentials) + assert auth.api_key == "test_api_key_123" + assert auth.base_url == "https://app.watercrawl.dev" + assert auth.credentials == valid_credentials + + def test_should_initialize_with_custom_base_url(self): + """Test initialization with custom base URL""" + credentials = { + "auth_type": "x-api-key", + "config": {"api_key": "test_api_key_123", "base_url": "https://custom.watercrawl.dev"}, + } + auth = WatercrawlAuth(credentials) + assert auth.api_key == "test_api_key_123" + assert auth.base_url == "https://custom.watercrawl.dev" + + @pytest.mark.parametrize( + ("auth_type", "expected_error"), + [ + ("bearer", "Invalid auth type, WaterCrawl auth type must be x-api-key"), + ("basic", "Invalid auth type, WaterCrawl auth type must be x-api-key"), + ("", "Invalid auth type, WaterCrawl auth type must be x-api-key"), + ], + ) + def test_should_raise_error_for_invalid_auth_type(self, auth_type, expected_error): + """Test that non-x-api-key auth types raise ValueError""" + credentials = {"auth_type": auth_type, "config": {"api_key": "test_api_key_123"}} + with pytest.raises(ValueError) as exc_info: + WatercrawlAuth(credentials) + assert str(exc_info.value) == expected_error + + @pytest.mark.parametrize( + ("credentials", "expected_error"), + [ + ({"auth_type": "x-api-key", "config": {}}, "No API key provided"), + ({"auth_type": "x-api-key"}, "No API key provided"), + ({"auth_type": "x-api-key", "config": {"api_key": ""}}, "No API key provided"), + ({"auth_type": "x-api-key", "config": {"api_key": None}}, "No API key provided"), + ], + ) + def test_should_raise_error_for_missing_api_key(self, credentials, expected_error): + """Test that missing or empty API key raises ValueError""" + with pytest.raises(ValueError) as exc_info: + WatercrawlAuth(credentials) + assert str(exc_info.value) == expected_error + + @patch("services.auth.watercrawl.watercrawl.requests.get") + def test_should_validate_valid_credentials_successfully(self, mock_get, auth_instance): + """Test successful credential validation""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + result = auth_instance.validate_credentials() + + assert result is True + mock_get.assert_called_once_with( + "https://app.watercrawl.dev/api/v1/core/crawl-requests/", + headers={"Content-Type": "application/json", "X-API-KEY": "test_api_key_123"}, + ) + + @pytest.mark.parametrize( + ("status_code", "error_message"), + [ + (402, "Payment required"), + (409, "Conflict error"), + (500, "Internal server error"), + ], + ) + @patch("services.auth.watercrawl.watercrawl.requests.get") + def test_should_handle_http_errors(self, mock_get, status_code, error_message, auth_instance): + """Test handling of various HTTP error codes""" + mock_response = MagicMock() + mock_response.status_code = status_code + mock_response.json.return_value = {"error": error_message} + mock_get.return_value = mock_response + + with pytest.raises(Exception) as exc_info: + auth_instance.validate_credentials() + assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}" + + @pytest.mark.parametrize( + ("status_code", "response_text", "has_json_error", "expected_error_contains"), + [ + (403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"), + (404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"), + (401, "Not JSON", True, "Expecting value"), # JSON decode error + ], + ) + @patch("services.auth.watercrawl.watercrawl.requests.get") + def test_should_handle_unexpected_errors( + self, mock_get, status_code, response_text, has_json_error, expected_error_contains, auth_instance + ): + """Test handling of unexpected errors with various response formats""" + mock_response = MagicMock() + mock_response.status_code = status_code + mock_response.text = response_text + if has_json_error: + mock_response.json.side_effect = Exception("Not JSON") + mock_get.return_value = mock_response + + with pytest.raises(Exception) as exc_info: + auth_instance.validate_credentials() + assert expected_error_contains in str(exc_info.value) + + @pytest.mark.parametrize( + ("exception_type", "exception_message"), + [ + (requests.ConnectionError, "Network error"), + (requests.Timeout, "Request timeout"), + (requests.ReadTimeout, "Read timeout"), + (requests.ConnectTimeout, "Connection timeout"), + ], + ) + @patch("services.auth.watercrawl.watercrawl.requests.get") + def test_should_handle_network_errors(self, mock_get, exception_type, exception_message, auth_instance): + """Test handling of various network-related errors including timeouts""" + mock_get.side_effect = exception_type(exception_message) + + with pytest.raises(exception_type) as exc_info: + auth_instance.validate_credentials() + assert exception_message in str(exc_info.value) + + def test_should_not_expose_api_key_in_error_messages(self): + """Test that API key is not exposed in error messages""" + credentials = {"auth_type": "x-api-key", "config": {"api_key": "super_secret_key_12345"}} + auth = WatercrawlAuth(credentials) + + # Verify API key is stored but not in any error message + assert auth.api_key == "super_secret_key_12345" + + # Test various error scenarios don't expose the key + with pytest.raises(ValueError) as exc_info: + WatercrawlAuth({"auth_type": "bearer", "config": {"api_key": "super_secret_key_12345"}}) + assert "super_secret_key_12345" not in str(exc_info.value) + + @patch("services.auth.watercrawl.watercrawl.requests.get") + def test_should_use_custom_base_url_in_validation(self, mock_get): + """Test that custom base URL is used in validation""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + credentials = { + "auth_type": "x-api-key", + "config": {"api_key": "test_api_key_123", "base_url": "https://custom.watercrawl.dev"}, + } + auth = WatercrawlAuth(credentials) + result = auth.validate_credentials() + + assert result is True + assert mock_get.call_args[0][0] == "https://custom.watercrawl.dev/api/v1/core/crawl-requests/" + + @pytest.mark.parametrize( + ("base_url", "expected_url"), + [ + ("https://app.watercrawl.dev", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"), + ("https://app.watercrawl.dev/", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"), + ("https://app.watercrawl.dev//", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"), + ], + ) + @patch("services.auth.watercrawl.watercrawl.requests.get") + def test_should_use_urljoin_for_url_construction(self, mock_get, base_url, expected_url): + """Test that urljoin is used correctly for URL construction with various base URLs""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + credentials = {"auth_type": "x-api-key", "config": {"api_key": "test_api_key_123", "base_url": base_url}} + auth = WatercrawlAuth(credentials) + auth.validate_credentials() + + # Verify the correct URL was called + assert mock_get.call_args[0][0] == expected_url + + @patch("services.auth.watercrawl.watercrawl.requests.get") + def test_should_handle_timeout_with_retry_suggestion(self, mock_get, auth_instance): + """Test that timeout errors are handled gracefully with appropriate error message""" + mock_get.side_effect = requests.Timeout("The request timed out after 30 seconds") + + with pytest.raises(requests.Timeout) as exc_info: + auth_instance.validate_credentials() + + # Verify the timeout exception is raised with original message + assert "timed out" in str(exc_info.value) diff --git a/api/tests/unit_tests/services/test_dataset_service_update_dataset.py b/api/tests/unit_tests/services/test_dataset_service_update_dataset.py index 87b46f213b..7c40b1e556 100644 --- a/api/tests/unit_tests/services/test_dataset_service_update_dataset.py +++ b/api/tests/unit_tests/services/test_dataset_service_update_dataset.py @@ -102,17 +102,16 @@ class TestDatasetServiceUpdateDataset: patch("services.dataset_service.DatasetService.get_dataset") as mock_get_dataset, patch("services.dataset_service.DatasetService.check_dataset_permission") as mock_check_perm, patch("extensions.ext_database.db.session") as mock_db, - patch("services.dataset_service.datetime") as mock_datetime, + patch("services.dataset_service.naive_utc_now") as mock_naive_utc_now, ): current_time = datetime.datetime(2023, 1, 1, 12, 0, 0) - mock_datetime.datetime.now.return_value = current_time - mock_datetime.UTC = datetime.UTC + mock_naive_utc_now.return_value = current_time yield { "get_dataset": mock_get_dataset, "check_permission": mock_check_perm, "db_session": mock_db, - "datetime": mock_datetime, + "naive_utc_now": mock_naive_utc_now, "current_time": current_time, } @@ -292,7 +291,7 @@ class TestDatasetServiceUpdateDataset: "embedding_model_provider": "openai", "embedding_model": "text-embedding-ada-002", "updated_by": user.id, - "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + "updated_at": mock_dataset_service_dependencies["current_time"], } self._assert_database_update_called( @@ -327,7 +326,7 @@ class TestDatasetServiceUpdateDataset: "indexing_technique": "high_quality", "retrieval_model": "new_model", "updated_by": user.id, - "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + "updated_at": mock_dataset_service_dependencies["current_time"], } actual_call_args = mock_dataset_service_dependencies[ @@ -365,7 +364,7 @@ class TestDatasetServiceUpdateDataset: "collection_binding_id": None, "retrieval_model": "new_model", "updated_by": user.id, - "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + "updated_at": mock_dataset_service_dependencies["current_time"], } self._assert_database_update_called( @@ -422,7 +421,7 @@ class TestDatasetServiceUpdateDataset: "collection_binding_id": "binding-456", "retrieval_model": "new_model", "updated_by": user.id, - "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + "updated_at": mock_dataset_service_dependencies["current_time"], } self._assert_database_update_called( @@ -463,7 +462,7 @@ class TestDatasetServiceUpdateDataset: "collection_binding_id": "binding-123", "retrieval_model": "new_model", "updated_by": user.id, - "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + "updated_at": mock_dataset_service_dependencies["current_time"], } self._assert_database_update_called( @@ -525,7 +524,7 @@ class TestDatasetServiceUpdateDataset: "collection_binding_id": "binding-789", "retrieval_model": "new_model", "updated_by": user.id, - "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + "updated_at": mock_dataset_service_dependencies["current_time"], } self._assert_database_update_called( @@ -568,7 +567,7 @@ class TestDatasetServiceUpdateDataset: "collection_binding_id": "binding-123", "retrieval_model": "new_model", "updated_by": user.id, - "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + "updated_at": mock_dataset_service_dependencies["current_time"], } self._assert_database_update_called( diff --git a/api/tests/unit_tests/utils/position_helper/test_position_helper.py b/api/tests/unit_tests/utils/position_helper/test_position_helper.py index 29558a93c2..dbd8f05098 100644 --- a/api/tests/unit_tests/utils/position_helper/test_position_helper.py +++ b/api/tests/unit_tests/utils/position_helper/test_position_helper.py @@ -95,7 +95,7 @@ def test_included_position_data(prepare_example_positions_yaml): position_map = get_position_map(folder_path=prepare_example_positions_yaml, file_name="example_positions.yaml") pin_list = ["forth", "first"] include_set = {"forth", "first"} - exclude_set = {} + exclude_set = set() position_map = pin_position_map(original_position_map=position_map, pin_list=pin_list) diff --git a/docker/.env.example b/docker/.env.example index a05141569b..88cc544730 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -283,11 +283,12 @@ REDIS_CLUSTERS_PASSWORD= # Celery Configuration # ------------------------------ -# Use redis as the broker, and redis db 1 for celery broker. -# Format as follows: `redis://:@:/` +# Use standalone redis as the broker, and redis db 1 for celery broker. (redis_username is usually set by defualt as empty) +# Format as follows: `redis://:@:/`. # Example: redis://:difyai123456@redis:6379/1 -# If use Redis Sentinel, format as follows: `sentinel://:@:/` -# Example: sentinel://localhost:26379/1;sentinel://localhost:26380/1;sentinel://localhost:26381/1 +# If use Redis Sentinel, format as follows: `sentinel://:@:/` +# For high availability, you can configure multiple Sentinel nodes (if provided) separated by semicolons like below example: +# Example: sentinel://:difyai123456@localhost:26379/1;sentinel://:difyai12345@localhost:26379/1;sentinel://:difyai12345@localhost:26379/1 CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1 CELERY_BACKEND=redis BROKER_USE_SSL=false @@ -412,6 +413,8 @@ SUPABASE_URL=your-server-url # The type of vector store to use. # Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`. VECTOR_STORE=weaviate +# Prefix used to create collection name in vector database +VECTOR_INDEX_NAME_PREFIX=Vector_index # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. WEAVIATE_ENDPOINT=http://weaviate:8080 @@ -1165,3 +1168,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..878268e76b 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -55,6 +55,25 @@ 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 + 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 5962adb079..f914d92254 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -136,6 +136,7 @@ x-shared-env: &shared-api-worker-env SUPABASE_API_KEY: ${SUPABASE_API_KEY:-your-access-key} SUPABASE_URL: ${SUPABASE_URL:-your-server-url} VECTOR_STORE: ${VECTOR_STORE:-weaviate} + VECTOR_INDEX_NAME_PREFIX: ${VECTOR_INDEX_NAME_PREFIX:-Vector_index} WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080} WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih} QDRANT_URL: ${QDRANT_URL:-http://qdrant:6333} @@ -526,6 +527,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 @@ -583,6 +592,25 @@ 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 + networks: + - ssrf_proxy_network + - default + # Frontend web application. web: image: langgenius/dify-web:1.6.0 diff --git a/sdks/python-client/dify_client/__init__.py b/sdks/python-client/dify_client/__init__.py index b557a9ce95..d00c207afa 100644 --- a/sdks/python-client/dify_client/__init__.py +++ b/sdks/python-client/dify_client/__init__.py @@ -1 +1,7 @@ -from dify_client.client import ChatClient, CompletionClient, WorkflowClient, KnowledgeBaseClient, DifyClient +from dify_client.client import ( + ChatClient, + CompletionClient, + WorkflowClient, + KnowledgeBaseClient, + DifyClient, +) diff --git a/tests/unit_tests/events/test_provider_update_deadlock_prevention.py b/tests/unit_tests/events/test_provider_update_deadlock_prevention.py deleted file mode 100644 index 47c175acd7..0000000000 --- a/tests/unit_tests/events/test_provider_update_deadlock_prevention.py +++ /dev/null @@ -1,248 +0,0 @@ -import threading -from unittest.mock import Mock, patch - -from core.app.entities.app_invoke_entities import ChatAppGenerateEntity -from core.entities.provider_entities import QuotaUnit -from events.event_handlers.update_provider_when_message_created import ( - handle, - get_update_stats, -) -from models.provider import ProviderType -from sqlalchemy.exc import OperationalError - - -class TestProviderUpdateDeadlockPrevention: - """Test suite for deadlock prevention in Provider updates.""" - - def setup_method(self): - """Setup test fixtures.""" - self.mock_message = Mock() - self.mock_message.answer_tokens = 100 - - self.mock_app_config = Mock() - self.mock_app_config.tenant_id = "test-tenant-123" - - self.mock_model_conf = Mock() - self.mock_model_conf.provider = "openai" - - self.mock_system_config = Mock() - self.mock_system_config.current_quota_type = QuotaUnit.TOKENS - - self.mock_provider_config = Mock() - self.mock_provider_config.using_provider_type = ProviderType.SYSTEM - self.mock_provider_config.system_configuration = self.mock_system_config - - self.mock_provider_bundle = Mock() - self.mock_provider_bundle.configuration = self.mock_provider_config - - self.mock_model_conf.provider_model_bundle = self.mock_provider_bundle - - self.mock_generate_entity = Mock(spec=ChatAppGenerateEntity) - self.mock_generate_entity.app_config = self.mock_app_config - self.mock_generate_entity.model_conf = self.mock_model_conf - - @patch("events.event_handlers.update_provider_when_message_created.db") - def test_consolidated_handler_basic_functionality(self, mock_db): - """Test that the consolidated handler performs both updates correctly.""" - # Setup mock query chain - mock_query = Mock() - mock_db.session.query.return_value = mock_query - mock_query.filter.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.update.return_value = 1 # 1 row affected - - # Call the handler - handle(self.mock_message, application_generate_entity=self.mock_generate_entity) - - # Verify db.session.query was called - assert mock_db.session.query.called - - # Verify commit was called - mock_db.session.commit.assert_called_once() - - # Verify no rollback was called - assert not mock_db.session.rollback.called - - @patch("events.event_handlers.update_provider_when_message_created.db") - def test_deadlock_retry_mechanism(self, mock_db): - """Test that deadlock errors trigger retry logic.""" - # Setup mock to raise deadlock error on first attempt, succeed on second - mock_query = Mock() - mock_db.session.query.return_value = mock_query - mock_query.filter.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.update.return_value = 1 - - # First call raises deadlock, second succeeds - mock_db.session.commit.side_effect = [ - OperationalError("deadlock detected", None, None), - None, # Success on retry - ] - - # Call the handler - handle(self.mock_message, application_generate_entity=self.mock_generate_entity) - - # Verify commit was called twice (original + retry) - assert mock_db.session.commit.call_count == 2 - - # Verify rollback was called once (after first failure) - mock_db.session.rollback.assert_called_once() - - @patch("events.event_handlers.update_provider_when_message_created.db") - @patch("events.event_handlers.update_provider_when_message_created.time.sleep") - def test_exponential_backoff_timing(self, mock_sleep, mock_db): - """Test that retry delays follow exponential backoff pattern.""" - # Setup mock to fail twice, succeed on third attempt - mock_query = Mock() - mock_db.session.query.return_value = mock_query - mock_query.filter.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.update.return_value = 1 - - mock_db.session.commit.side_effect = [ - OperationalError("deadlock detected", None, None), - OperationalError("deadlock detected", None, None), - None, # Success on third attempt - ] - - # Call the handler - handle(self.mock_message, application_generate_entity=self.mock_generate_entity) - - # Verify sleep was called twice with increasing delays - assert mock_sleep.call_count == 2 - - # First delay should be around 0.1s + jitter - first_delay = mock_sleep.call_args_list[0][0][0] - assert 0.1 <= first_delay <= 0.3 - - # Second delay should be around 0.2s + jitter - second_delay = mock_sleep.call_args_list[1][0][0] - assert 0.2 <= second_delay <= 0.4 - - def test_concurrent_handler_execution(self): - """Test that multiple handlers can run concurrently without deadlock.""" - results = [] - errors = [] - - def run_handler(): - try: - with patch( - "events.event_handlers.update_provider_when_message_created.db" - ) as mock_db: - mock_query = Mock() - mock_db.session.query.return_value = mock_query - mock_query.filter.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.update.return_value = 1 - - handle( - self.mock_message, - application_generate_entity=self.mock_generate_entity, - ) - results.append("success") - except Exception as e: - errors.append(str(e)) - - # Run multiple handlers concurrently - threads = [] - for _ in range(5): - thread = threading.Thread(target=run_handler) - threads.append(thread) - thread.start() - - # Wait for all threads to complete - for thread in threads: - thread.join(timeout=5) - - # Verify all handlers completed successfully - assert len(results) == 5 - assert len(errors) == 0 - - def test_performance_stats_tracking(self): - """Test that performance statistics are tracked correctly.""" - # Reset stats - stats = get_update_stats() - initial_total = stats["total_updates"] - - with patch( - "events.event_handlers.update_provider_when_message_created.db" - ) as mock_db: - mock_query = Mock() - mock_db.session.query.return_value = mock_query - mock_query.filter.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.update.return_value = 1 - - # Call handler - handle( - self.mock_message, application_generate_entity=self.mock_generate_entity - ) - - # Check that stats were updated - updated_stats = get_update_stats() - assert updated_stats["total_updates"] == initial_total + 1 - assert updated_stats["successful_updates"] >= initial_total + 1 - - def test_non_chat_entity_ignored(self): - """Test that non-chat entities are ignored by the handler.""" - # Create a non-chat entity - mock_non_chat_entity = Mock() - mock_non_chat_entity.__class__.__name__ = "NonChatEntity" - - with patch( - "events.event_handlers.update_provider_when_message_created.db" - ) as mock_db: - # Call handler with non-chat entity - handle(self.mock_message, application_generate_entity=mock_non_chat_entity) - - # Verify no database operations were performed - assert not mock_db.session.query.called - assert not mock_db.session.commit.called - - @patch("events.event_handlers.update_provider_when_message_created.db") - def test_quota_calculation_tokens(self, mock_db): - """Test quota calculation for token-based quotas.""" - # Setup token-based quota - self.mock_system_config.current_quota_type = QuotaUnit.TOKENS - self.mock_message.answer_tokens = 150 - - mock_query = Mock() - mock_db.session.query.return_value = mock_query - mock_query.filter.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.update.return_value = 1 - - # Call handler - handle(self.mock_message, application_generate_entity=self.mock_generate_entity) - - # Verify update was called with token count - update_calls = mock_query.update.call_args_list - - # Should have at least one call with quota_used update - quota_update_found = False - for call in update_calls: - values = call[0][0] # First argument to update() - if "quota_used" in values: - quota_update_found = True - break - - assert quota_update_found - - @patch("events.event_handlers.update_provider_when_message_created.db") - def test_quota_calculation_times(self, mock_db): - """Test quota calculation for times-based quotas.""" - # Setup times-based quota - self.mock_system_config.current_quota_type = QuotaUnit.TIMES - - mock_query = Mock() - mock_db.session.query.return_value = mock_query - mock_query.filter.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.update.return_value = 1 - - # Call handler - handle(self.mock_message, application_generate_entity=self.mock_generate_entity) - - # Verify update was called - assert mock_query.update.called - assert mock_db.session.commit.called diff --git a/web/Dockerfile b/web/Dockerfile index 93eef59815..d59039528c 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -6,7 +6,7 @@ LABEL maintainer="takatost@gmail.com" # RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories RUN apk add --no-cache tzdata -RUN npm install -g pnpm@10.11.1 +RUN npm install -g pnpm@10.13.1 ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 8bf18904be..d082523222 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -83,27 +83,50 @@ const Panel: FC = () => { const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig || opikConfig || weaveConfig || arizeConfig || phoenixConfig || aliyunConfig) const fetchTracingConfig = async () => { - const { tracing_config: arizeConfig, has_not_configured: arizeHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.arize }) - if (!arizeHasNotConfig) - setArizeConfig(arizeConfig as ArizeConfig) - const { tracing_config: phoenixConfig, has_not_configured: phoenixHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.phoenix }) - if (!phoenixHasNotConfig) - setPhoenixConfig(phoenixConfig as PhoenixConfig) - const { tracing_config: langSmithConfig, has_not_configured: langSmithHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langSmith }) - if (!langSmithHasNotConfig) - setLangSmithConfig(langSmithConfig as LangSmithConfig) - const { tracing_config: langFuseConfig, has_not_configured: langFuseHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langfuse }) - if (!langFuseHasNotConfig) - setLangFuseConfig(langFuseConfig as LangFuseConfig) - const { tracing_config: opikConfig, has_not_configured: OpikHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.opik }) - if (!OpikHasNotConfig) - setOpikConfig(opikConfig as OpikConfig) - const { tracing_config: weaveConfig, has_not_configured: weaveHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.weave }) - if (!weaveHasNotConfig) - setWeaveConfig(weaveConfig as WeaveConfig) - const { tracing_config: aliyunConfig, has_not_configured: aliyunHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.aliyun }) - if (!aliyunHasNotConfig) - setAliyunConfig(aliyunConfig as AliyunConfig) + const getArizeConfig = async () => { + const { tracing_config: arizeConfig, has_not_configured: arizeHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.arize }) + if (!arizeHasNotConfig) + setArizeConfig(arizeConfig as ArizeConfig) + } + const getPhoenixConfig = async () => { + const { tracing_config: phoenixConfig, has_not_configured: phoenixHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.phoenix }) + if (!phoenixHasNotConfig) + setPhoenixConfig(phoenixConfig as PhoenixConfig) + } + const getLangSmithConfig = async () => { + const { tracing_config: langSmithConfig, has_not_configured: langSmithHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langSmith }) + if (!langSmithHasNotConfig) + setLangSmithConfig(langSmithConfig as LangSmithConfig) + } + const getLangFuseConfig = async () => { + const { tracing_config: langFuseConfig, has_not_configured: langFuseHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langfuse }) + if (!langFuseHasNotConfig) + setLangFuseConfig(langFuseConfig as LangFuseConfig) + } + const getOpikConfig = async () => { + const { tracing_config: opikConfig, has_not_configured: OpikHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.opik }) + if (!OpikHasNotConfig) + setOpikConfig(opikConfig as OpikConfig) + } + const getWeaveConfig = async () => { + const { tracing_config: weaveConfig, has_not_configured: weaveHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.weave }) + if (!weaveHasNotConfig) + setWeaveConfig(weaveConfig as WeaveConfig) + } + const getAliyunConfig = async () => { + const { tracing_config: aliyunConfig, has_not_configured: aliyunHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.aliyun }) + if (!aliyunHasNotConfig) + setAliyunConfig(aliyunConfig as AliyunConfig) + } + Promise.all([ + getArizeConfig(), + getPhoenixConfig(), + getLangSmithConfig(), + getLangFuseConfig(), + getOpikConfig(), + getWeaveConfig(), + getAliyunConfig(), + ]) } const handleTracingConfigUpdated = async (provider: TracingProvider) => { @@ -155,7 +178,6 @@ const Panel: FC = () => { await fetchTracingConfig() setLoaded() })() - // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const [controlShowPopup, setControlShowPopup] = useState(0) diff --git a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx index f3532f398d..62f6a34be0 100644 --- a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx +++ b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx @@ -1,6 +1,5 @@ 'use client' import { useTranslation } from 'react-i18next' -import { basePath } from '@/utils/var' import { RiAddLine, RiArrowRightLine, @@ -18,7 +17,7 @@ const CreateAppCard = ({ ref }: CreateAppCardProps) => {
- +
{
{t('dataset.createDatasetIntro')}
- +
{t('dataset.connectDataset')}
diff --git a/web/app/(commonLayout)/datasets/template/template.ja.mdx b/web/app/(commonLayout)/datasets/template/template.ja.mdx index 23f78b5d7d..6c0e20e1bb 100644 --- a/web/app/(commonLayout)/datasets/template/template.ja.mdx +++ b/web/app/(commonLayout)/datasets/template/template.ja.mdx @@ -83,7 +83,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - subchunk_segmentation (object) 子チャンクルール - separator セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは *** - max_tokens 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - - chunk_overlap 隣接するチャンク間の重複を定義 (オプション) + - chunk_overlap 隣接するチャンク間の重なりを定義 (オプション) ナレッジベースにパラメータが設定されていない場合、最初のアップロードには以下のパラメータを提供する必要があります。提供されない場合、デフォルトパラメータが使用されます。 @@ -218,7 +218,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - subchunk_segmentation (object) 子チャンクルール - separator セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは *** - max_tokens 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - - chunk_overlap 隣接するチャンク間の重複を定義 (オプション) + - chunk_overlap 隣接するチャンク間の重なりを定義 (オプション) アップロードする必要があるファイル。 @@ -555,7 +555,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - subchunk_segmentation (object) 子チャンクルール - separator セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは *** - max_tokens 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - - chunk_overlap 隣接するチャンク間の重複を定義 (オプション) + - chunk_overlap 隣接するチャンク間の重なりを定義 (オプション) @@ -657,7 +657,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - subchunk_segmentation (object) 子チャンクルール - separator セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは *** - max_tokens 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - - chunk_overlap 隣接するチャンク間の重複を定義 (オプション) + - chunk_overlap 隣接するチャンク間の重なりを定義 (オプション) diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index d07e2a99d9..64186a1b10 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -1,6 +1,6 @@ import React from 'react' import type { ReactNode } from 'react' -import SwrInitor from '@/app/components/swr-initor' +import SwrInitializer from '@/app/components/swr-initializer' import { AppContextProvider } from '@/context/app-context' import GA, { GaType } from '@/app/components/base/ga' import HeaderWrapper from '@/app/components/header/header-wrapper' @@ -13,7 +13,7 @@ const Layout = ({ children }: { children: ReactNode }) => { return ( <> - + @@ -26,7 +26,7 @@ const Layout = ({ children }: { children: ReactNode }) => { - + ) } diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index 55fa2983dd..47b8f045d2 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -1,5 +1,6 @@ 'use client' import { useState } from 'react' +import useSWR from 'swr' import { useTranslation } from 'react-i18next' import { RiGraduationCapFill, @@ -22,6 +23,8 @@ import PremiumBadge from '@/app/components/base/premium-badge' import { useGlobalPublicStore } from '@/context/global-public-context' import EmailChangeModal from './email-change-modal' import { validPassword } from '@/config' +import { fetchAppList } from '@/service/apps' +import type { App } from '@/types/app' const titleClassName = ` system-sm-semibold text-text-secondary @@ -33,7 +36,9 @@ const descriptionClassName = ` export default function AccountPage() { const { t } = useTranslation() const { systemFeatures } = useGlobalPublicStore() - const { mutateUserProfile, userProfile, apps } = useAppContext() + const { data: appList } = useSWR({ url: '/apps', params: { page: 1, limit: 100, name: '' } }, fetchAppList) + const apps = appList?.data || [] + const { mutateUserProfile, userProfile } = useAppContext() const { isEducationAccount } = useProviderContext() const { notify } = useContext(ToastContext) const [editNameModalVisible, setEditNameModalVisible] = useState(false) @@ -202,7 +207,7 @@ export default function AccountPage() { {!!apps.length && ( ({ ...app, key: app.id, name: app.name }))} + items={apps.map((app: App) => ({ ...app, key: app.id, name: app.name }))} renderItem={renderAppItem} wrapperClassName='mt-2' /> diff --git a/web/app/account/layout.tsx b/web/app/account/layout.tsx index e74716fb3b..b3225b5341 100644 --- a/web/app/account/layout.tsx +++ b/web/app/account/layout.tsx @@ -1,7 +1,7 @@ import React from 'react' import type { ReactNode } from 'react' import Header from './header' -import SwrInitor from '@/app/components/swr-initor' +import SwrInitor from '@/app/components/swr-initializer' import { AppContextProvider } from '@/context/app-context' import GA, { GaType } from '@/app/components/base/ga' import HeaderWrapper from '@/app/components/header/header-wrapper' diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index e85eaa2f53..c35047bbc5 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' import { useRouter } from 'next/navigation' -import { useContext, useContextSelector } from 'use-context-selector' +import { useContext } from 'use-context-selector' import React, { useCallback, useState } from 'react' import { RiDeleteBinLine, @@ -15,7 +15,7 @@ import AppIcon from '../base/app-icon' import cn from '@/utils/classnames' import { useStore as useAppStore } from '@/app/components/app/store' import { ToastContext } from '@/app/components/base/toast' -import AppsContext, { useAppContext } from '@/context/app-context' +import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' @@ -73,11 +73,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx const [showImportDSLModal, setShowImportDSLModal] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) - const mutateApps = useContextSelector( - AppsContext, - state => state.mutateApps, - ) - const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ name, icon_type, @@ -106,12 +101,11 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx message: t('app.editDone'), }) setAppDetail(app) - mutateApps() } catch { notify({ type: 'error', message: t('app.editFailed') }) } - }, [appDetail, mutateApps, notify, setAppDetail, t]) + }, [appDetail, notify, setAppDetail, t]) const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { if (!appDetail) @@ -131,7 +125,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx message: t('app.newApp.appCreated'), }) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') - mutateApps() onPlanInfoChanged() getRedirection(true, newApp, replace) } @@ -186,7 +179,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx try { await deleteApp(appDetail.id) notify({ type: 'success', message: t('app.appDeleted') }) - mutateApps() onPlanInfoChanged() setAppDetail() replace('/apps') @@ -198,7 +190,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx }) } setShowConfirmDelete(false) - }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, setAppDetail, t]) + }, [appDetail, notify, onPlanInfoChanged, replace, setAppDetail, t]) const { isCurrentWorkspaceEditor } = useAppContext() diff --git a/web/app/components/app/app-access-control/access-control-dialog.tsx b/web/app/components/app/app-access-control/access-control-dialog.tsx index e3e013fbd4..72dd33c72e 100644 --- a/web/app/components/app/app-access-control/access-control-dialog.tsx +++ b/web/app/components/app/app-access-control/access-control-dialog.tsx @@ -47,7 +47,7 @@ const AccessControlDialog = ({ >
close()} className="absolute right-5 top-5 z-10 flex h-8 w-8 cursor-pointer items-center justify-center"> - +
{children}
diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx index 13faaea957..e8e2358e0e 100644 --- a/web/app/components/app/app-access-control/index.tsx +++ b/web/app/components/app/app-access-control/index.tsx @@ -72,7 +72,7 @@ export default function AccessControl(props: AccessControlProps) {
-

{t('app.accessControlDialog.accessLabel')}

+

{t('app.accessControlDialog.accessLabel')}

diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx index ffdb714f08..4c36ad9956 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx @@ -14,7 +14,6 @@ import Loading from '@/app/components/base/loading' import Badge from '@/app/components/base/badge' import { useKnowledge } from '@/hooks/use-knowledge' import cn from '@/utils/classnames' -import { basePath } from '@/utils/var' export type ISelectDataSetProps = { isShow: boolean @@ -112,7 +111,7 @@ const SelectDataSet: FC = ({ }} > {t('appDebug.feature.dataSet.noDataSet')} - {t('appDebug.feature.dataSet.toCreate')} + {t('appDebug.feature.dataSet.toCreate')}
)} diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index f0a0da41a5..c37f7b051a 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRouter } from 'next/navigation' -import { useContext, useContextSelector } from 'use-context-selector' +import { useContext } from 'use-context-selector' import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react' import Link from 'next/link' import { useDebounceFn, useKeyPress } from 'ahooks' @@ -15,7 +15,7 @@ import Button from '@/app/components/base/button' import Divider from '@/app/components/base/divider' import cn from '@/utils/classnames' import { basePath } from '@/utils/var' -import AppsContext, { useAppContext } from '@/context/app-context' +import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { ToastContext } from '@/app/components/base/toast' import type { AppMode } from '@/types/app' @@ -41,7 +41,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) const { t } = useTranslation() const { push } = useRouter() const { notify } = useContext(ToastContext) - const mutateApps = useContextSelector(AppsContext, state => state.mutateApps) const [appMode, setAppMode] = useState('advanced-chat') const [appIcon, setAppIcon] = useState({ type: 'emoji', icon: '🤖', background: '#FFEAD5' }) @@ -80,7 +79,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) notify({ type: 'success', message: t('app.newApp.appCreated') }) onSuccess() onClose() - mutateApps() localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') getRedirection(isCurrentWorkspaceEditor, app, push) } @@ -88,7 +86,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) } isCreatingRef.current = false - }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) + }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor]) const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 }) useKeyPress(['meta.enter', 'ctrl.enter'], () => { @@ -298,7 +296,7 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP > {icon}
{title}
-
{description}
+
{description}
} diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index b48eac5458..9d97eae38d 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -90,10 +90,10 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam const [option, setOption] = useState
) diff --git a/web/app/components/base/chat/chat/question.tsx b/web/app/components/base/chat/chat/question.tsx index 666a869a32..6630d9bb9d 100644 --- a/web/app/components/base/chat/chat/question.tsx +++ b/web/app/components/base/chat/chat/question.tsx @@ -117,7 +117,7 @@ const Question: FC = ({
{ 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..cdc3924194 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/features/types.ts b/web/app/components/base/features/types.ts index 83f876383d..56bd7829ad 100644 --- a/web/app/components/base/features/types.ts +++ b/web/app/components/base/features/types.ts @@ -35,6 +35,22 @@ export type FileUpload = { number_limits?: number transfer_methods?: TransferMethod[] } + document?: EnabledOrDisabled & { + number_limits?: number + transfer_methods?: TransferMethod[] + } + audio?: EnabledOrDisabled & { + number_limits?: number + transfer_methods?: TransferMethod[] + } + video?: EnabledOrDisabled & { + number_limits?: number + transfer_methods?: TransferMethod[] + } + custom?: EnabledOrDisabled & { + number_limits?: number + transfer_methods?: TransferMethod[] + } allowed_file_types?: string[] allowed_file_extensions?: string[] allowed_file_upload_methods?: TransferMethod[] 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/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/markdown/index.tsx b/web/app/components/base/markdown/index.tsx index 1e50e6745b..bab5ac8eba 100644 --- a/web/app/components/base/markdown/index.tsx +++ b/web/app/components/base/markdown/index.tsx @@ -28,8 +28,15 @@ import { * Further refactoring candidates (custom block components not fitting general categories) * are noted in their respective files if applicable. */ +export type MarkdownProps = { + content: string + className?: string + customDisallowedElements?: string[] + customComponents?: Record> +} -export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) { +export const Markdown = (props: MarkdownProps) => { + const { customComponents = {} } = props const latexContent = flow([ preprocessThinkTag, preprocessLaTeX, @@ -78,6 +85,7 @@ export function Markdown(props: { content: string; className?: string; customDis form: MarkdownForm, script: ScriptBlock as any, details: ThinkBlock, + ...customComponents, }} > {/* Markdown detect has problem. */} diff --git a/web/app/components/billing/apps-full-in-dialog/index.tsx b/web/app/components/billing/apps-full-in-dialog/index.tsx index b721b94b01..fda3213713 100644 --- a/web/app/components/billing/apps-full-in-dialog/index.tsx +++ b/web/app/components/billing/apps-full-in-dialog/index.tsx @@ -21,7 +21,7 @@ const AppsFull: FC<{ loc: string; className?: string; }> = ({ }) => { const { t } = useTranslation() const { plan } = useProviderContext() - const { userProfile, langeniusVersionInfo } = useAppContext() + const { userProfile, langGeniusVersionInfo } = useAppContext() const isTeam = plan.type === Plan.team const usage = plan.usage.buildApps const total = plan.total.buildApps @@ -62,7 +62,7 @@ const AppsFull: FC<{ loc: string; className?: string; }> = ({ )} {plan.type !== Plan.sandbox && plan.type !== Plan.professional && ( diff --git a/web/app/components/browser-initor.tsx b/web/app/components/browser-initializer.tsx similarity index 88% rename from web/app/components/browser-initor.tsx rename to web/app/components/browser-initializer.tsx index f2f4b02dc0..fcae22c448 100644 --- a/web/app/components/browser-initor.tsx +++ b/web/app/components/browser-initializer.tsx @@ -43,10 +43,10 @@ Object.defineProperty(globalThis, 'sessionStorage', { value: sessionStorage, }) -const BrowserInitor = ({ +const BrowserInitializer = ({ children, -}: { children: React.ReactNode }) => { +}: { children: React.ReactElement }) => { return children } -export default BrowserInitor +export default BrowserInitializer diff --git a/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx b/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx index 78dda7bbb6..68d610f886 100644 --- a/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx @@ -43,8 +43,8 @@ const BatchAction: FC = ({ hideDeleteConfirm() } return ( -
    -
    +
    +
    {selectedIds.length} diff --git a/web/app/components/develop/template/template.en.mdx b/web/app/components/develop/template/template.en.mdx index f178645a8e..3fdb782628 100755 --- a/web/app/components/develop/template/template.en.mdx +++ b/web/app/components/develop/template/template.en.mdx @@ -580,11 +580,30 @@ The text generation application offers non-session support and is ideal for tran - `default` (string) Default value - `options` (array[string]) Option values - `file_upload` (object) File upload configuration - - `image` (object) Image settings - Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) Whether it is enabled - - `number_limits` (int) Image number limit, default is 3 - - `transfer_methods` (array[string]) List of transfer methods, remote_url, local_file, must choose one + - `document` (object) Document settings + Currently only supports document types: `txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Document number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `image` (object) Image settings + Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Image number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `audio` (object) Audio settings + Currently only supports audio types: `mp3`, `m4a`, `wav`, `webm`, `amr`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Audio number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `video` (object) Video settings + Currently only supports video types: `mp4`, `mov`, `mpeg`, `mpga`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Video number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `custom` (object) Custom settings + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Custom number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. - `system_parameters` (object) System parameters - `file_size_limit` (int) Document upload size limit (MB) - `image_file_size_limit` (int) Image file upload size limit (MB) diff --git a/web/app/components/develop/template/template.ja.mdx b/web/app/components/develop/template/template.ja.mdx index 4dbefca8f8..238a921fb5 100755 --- a/web/app/components/develop/template/template.ja.mdx +++ b/web/app/components/develop/template/template.ja.mdx @@ -578,11 +578,30 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `default` (string) デフォルト値 - `options` (array[string]) オプション値 - `file_upload` (object) ファイルアップロード設定 - - `image` (object) 画像設定 - 現在は画像タイプのみ対応:`png`、`jpg`、`jpeg`、`webp`、`gif` - - `enabled` (bool) 有効かどうか - - `number_limits` (int) 画像数制限、デフォルトは3 - - `transfer_methods` (array[string]) 転送方法リスト、remote_url、local_file、いずれかを選択 + - `document` (object) ドキュメント設定 + 現在サポートされているドキュメントタイプ:`txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) ドキュメント数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `image` (object) 画像設定 + 現在サポートされている画像タイプ:`png`, `jpg`, `jpeg`, `webp`, `gif`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `audio` (object) オーディオ設定 + 現在サポートされているオーディオタイプ:`mp3`, `m4a`, `wav`, `webm`, `amr`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) オーディオ数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `video` (object) ビデオ設定 + 現在サポートされているビデオタイプ:`mp4`, `mov`, `mpeg`, `mpga`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) ビデオ数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `custom` (object) カスタム設定 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) カスタム数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 - `system_parameters` (object) システムパラメータ - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) diff --git a/web/app/components/develop/template/template.zh.mdx b/web/app/components/develop/template/template.zh.mdx index 4af5a28050..a5eea3d193 100755 --- a/web/app/components/develop/template/template.zh.mdx +++ b/web/app/components/develop/template/template.zh.mdx @@ -552,11 +552,30 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `default` (string) 默认值 - `options` (array[string]) 选项值 - `file_upload` (object) 文件上传配置 - - `image` (object) 图片设置 - 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 是否开启 - - `number_limits` (int) 图片数量限制,默认 3 - - `transfer_methods` (array[string]) 传递方式列表,remote_url , local_file,必选一个 + - `document` (object) 文档设置 + 当前仅支持文档类型:`txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 文档数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `image` (object) 图片设置 + 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 图片数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `audio` (object) 音频设置 + 当前仅支持音频类型:`mp3`, `m4a`, `wav`, `webm`, `amr`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 音频数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `video` (object) 视频设置 + 当前仅支持视频类型:`mp4`, `mov`, `mpeg`, `mpga`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 视频数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `custom` (object) 自定义设置 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 自定义数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 - `system_parameters` (object) 系统参数 - `file_size_limit` (int) 文档上传大小限制 (MB) - `image_file_size_limit` (int) 图片文件上传大小限制(MB) diff --git a/web/app/components/develop/template/template_advanced_chat.en.mdx b/web/app/components/develop/template/template_advanced_chat.en.mdx index faecd3d1cd..bafcb1f99a 100644 --- a/web/app/components/develop/template/template_advanced_chat.en.mdx +++ b/web/app/components/develop/template/template_advanced_chat.en.mdx @@ -80,6 +80,12 @@ Chat applications support session persistence, allowing previous chat history to Auto-generate title, default is `true`. If set to `false`, can achieve async title generation by calling the conversation rename API and setting `auto_generate` to `true`. + + (Optional) Trace ID. Used for integration with existing business trace components to achieve end-to-end distributed tracing. If not provided, the system will automatically generate a trace_id. Supports the following three ways to pass, in order of priority:
    + - Header: via HTTP Header X-Trace-Id, highest priority.
    + - Query parameter: via URL query parameter trace_id.
    + - Request Body: via request body field trace_id (i.e., this field).
    +
    ### Response @@ -1197,11 +1203,30 @@ Chat applications support session persistence, allowing previous chat history to - `default` (string) Default value - `options` (array[string]) Option values - `file_upload` (object) File upload configuration - - `image` (object) Image settings - Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) Whether it is enabled - - `number_limits` (int) Image number limit, default is 3 - - `transfer_methods` (array[string]) List of transfer methods, remote_url, local_file, must choose one + - `document` (object) Document settings + Currently only supports document types: `txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Document number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `image` (object) Image settings + Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Image number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `audio` (object) Audio settings + Currently only supports audio types: `mp3`, `m4a`, `wav`, `webm`, `amr`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Audio number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `video` (object) Video settings + Currently only supports video types: `mp4`, `mov`, `mpeg`, `mpga`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Video number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `custom` (object) Custom settings + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Custom number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. - `system_parameters` (object) System parameters - `file_size_limit` (int) Document upload size limit (MB) - `image_file_size_limit` (int) Image file upload size limit (MB) diff --git a/web/app/components/develop/template/template_advanced_chat.ja.mdx b/web/app/components/develop/template/template_advanced_chat.ja.mdx index 5ce54a61d2..d8c5464ed5 100644 --- a/web/app/components/develop/template/template_advanced_chat.ja.mdx +++ b/web/app/components/develop/template/template_advanced_chat.ja.mdx @@ -80,6 +80,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from タイトルを自動生成、デフォルトは`true`。 `false`に設定すると、会話のリネームAPIを呼び出し、`auto_generate`を`true`に設定することで非同期タイトル生成を実現できます。 + + (オプション)トレースID。既存の業務システムのトレースコンポーネントと連携し、エンドツーエンドの分散トレーシングを実現するために使用します。指定がない場合、システムが自動的に trace_id を生成します。以下の3つの方法で渡すことができ、優先順位は次のとおりです:
    + - Header:HTTPヘッダー X-Trace-Id で渡す(最優先)。
    + - クエリパラメータ:URLクエリパラメータ trace_id で渡す。
    + - リクエストボディ:リクエストボディの trace_id フィールドで渡す(本フィールド)。
    +
    ### 応答 @@ -1197,11 +1203,30 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `default` (string) デフォルト値 - `options` (array[string]) オプション値 - `file_upload` (object) ファイルアップロード設定 - - `image` (object) 画像設定 - 現在サポートされている画像タイプ:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 有効かどうか - - `number_limits` (int) 画像数の制限、デフォルトは3 - - `transfer_methods` (array[string]) 転送方法のリスト、remote_url, local_file、いずれかを選択する必要があります + - `document` (object) ドキュメント設定 + 現在サポートされているドキュメントタイプ:`txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) ドキュメント数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `image` (object) 画像設定 + 現在サポートされている画像タイプ:`png`, `jpg`, `jpeg`, `webp`, `gif`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `audio` (object) オーディオ設定 + 現在サポートされているオーディオタイプ:`mp3`, `m4a`, `wav`, `webm`, `amr`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) オーディオ数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `video` (object) ビデオ設定 + 現在サポートされているビデオタイプ:`mp4`, `mov`, `mpeg`, `mpga`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) ビデオ数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `custom` (object) カスタム設定 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) カスタム数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 - `system_parameters` (object) システムパラメータ - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) diff --git a/web/app/components/develop/template/template_advanced_chat.zh.mdx b/web/app/components/develop/template/template_advanced_chat.zh.mdx index 7a69ee60aa..30068d93a2 100755 --- a/web/app/components/develop/template/template_advanced_chat.zh.mdx +++ b/web/app/components/develop/template/template_advanced_chat.zh.mdx @@ -78,6 +78,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' (选填)自动生成标题,默认 `true`。 若设置为 `false`,则可通过调用会话重命名接口并设置 `auto_generate` 为 `true` 实现异步生成标题。 + + (选填)链路追踪ID。适用于与业务系统已有的trace组件打通,实现端到端分布式追踪等场景。如果未指定,系统会自动生成trace_id。支持以下三种方式传递,具体优先级依次为:
    + - Header:通过 HTTP Header X-Trace-Id 传递,优先级最高。
    + - Query 参数:通过 URL 查询参数 trace_id 传递。
    + - Request Body:通过请求体字段 trace_id 传递(即本字段)。
    +
    ### Response @@ -1229,11 +1235,30 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `default` (string) 默认值 - `options` (array[string]) 选项值 - `file_upload` (object) 文件上传配置 - - `image` (object) 图片设置 - 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 是否开启 - - `number_limits` (int) 图片数量限制,默认 3 - - `transfer_methods` (array[string]) 传递方式列表,remote_url , local_file,必选一个 + - `document` (object) 文档设置 + 当前仅支持文档类型:`txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 文档数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `image` (object) 图片设置 + 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 图片数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `audio` (object) 音频设置 + 当前仅支持音频类型:`mp3`, `m4a`, `wav`, `webm`, `amr`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 音频数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `video` (object) 视频设置 + 当前仅支持视频类型:`mp4`, `mov`, `mpeg`, `mpga`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 视频数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `custom` (object) 自定义设置 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 自定义数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 - `system_parameters` (object) 系统参数 - `file_size_limit` (int) Document upload size limit (MB) - `image_file_size_limit` (int) Image file upload size limit (MB) diff --git a/web/app/components/develop/template/template_chat.en.mdx b/web/app/components/develop/template/template_chat.en.mdx index c95471160e..f1bb1de206 100644 --- a/web/app/components/develop/template/template_chat.en.mdx +++ b/web/app/components/develop/template/template_chat.en.mdx @@ -74,6 +74,12 @@ Chat applications support session persistence, allowing previous chat history to Auto-generate title, default is `true`. If set to `false`, can achieve async title generation by calling the conversation rename API and setting `auto_generate` to `true`. + + (Optional) Trace ID. Used for integration with existing business trace components to achieve end-to-end distributed tracing. If not provided, the system will automatically generate a trace_id. Supports the following three ways to pass, in order of priority:
    + - Header: via HTTP Header X-Trace-Id, highest priority.
    + - Query parameter: via URL query parameter trace_id.
    + - Request Body: via request body field trace_id (i.e., this field).
    +
    ### Response @@ -1234,11 +1240,30 @@ Chat applications support session persistence, allowing previous chat history to - `default` (string) Default value - `options` (array[string]) Option values - `file_upload` (object) File upload configuration - - `image` (object) Image settings - Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) Whether it is enabled - - `number_limits` (int) Image number limit, default is 3 - - `transfer_methods` (array[string]) List of transfer methods, remote_url, local_file, must choose one + - `document` (object) Document settings + Currently only supports document types: `txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Document number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `image` (object) Image settings + Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Image number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `audio` (object) Audio settings + Currently only supports audio types: `mp3`, `m4a`, `wav`, `webm`, `amr`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Audio number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `video` (object) Video settings + Currently only supports video types: `mp4`, `mov`, `mpeg`, `mpga`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Video number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `custom` (object) Custom settings + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Custom number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. - `system_parameters` (object) System parameters - `file_size_limit` (int) Document upload size limit (MB) - `image_file_size_limit` (int) Image file upload size limit (MB) diff --git a/web/app/components/develop/template/template_chat.ja.mdx b/web/app/components/develop/template/template_chat.ja.mdx index 8368326e40..06e88782d9 100644 --- a/web/app/components/develop/template/template_chat.ja.mdx +++ b/web/app/components/develop/template/template_chat.ja.mdx @@ -74,6 +74,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from タイトルを自動生成します。デフォルトは`true`です。 `false`に設定すると、会話のリネームAPIを呼び出し、`auto_generate`を`true`に設定することで非同期タイトル生成を実現できます。 + + (オプション)トレースID。既存の業務システムのトレースコンポーネントと連携し、エンドツーエンドの分散トレーシングを実現するために使用します。指定がない場合、システムが自動的に trace_id を生成します。以下の3つの方法で渡すことができ、優先順位は次のとおりです:
    + - Header:HTTPヘッダー X-Trace-Id で渡す(最優先)。
    + - クエリパラメータ:URLクエリパラメータ trace_id で渡す。
    + - リクエストボディ:リクエストボディの trace_id フィールドで渡す(本フィールド)。
    +
    ### 応答 @@ -1224,12 +1230,31 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `required` (bool) 必須かどうか - `default` (string) デフォルト値 - `options` (array[string]) オプション値 - - `file_upload` (object) ファイルアップロード構成 - - `image` (object) 画像設定 - 現在サポートされている画像タイプ:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 有効かどうか - - `number_limits` (int) 画像数の制限、デフォルトは3 - - `transfer_methods` (array[string]) 転送方法のリスト、remote_url, local_file、いずれかを選択する必要があります + - `file_upload` (object) ファイルアップロード設定 + - `document` (object) ドキュメント設定 + 現在サポートされているドキュメントタイプ:`txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) ドキュメント数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `image` (object) 画像設定 + 現在サポートされている画像タイプ:`png`, `jpg`, `jpeg`, `webp`, `gif`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `audio` (object) オーディオ設定 + 現在サポートされているオーディオタイプ:`mp3`, `m4a`, `wav`, `webm`, `amr`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) オーディオ数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `video` (object) ビデオ設定 + 現在サポートされているビデオタイプ:`mp4`, `mov`, `mpeg`, `mpga`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) ビデオ数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `custom` (object) カスタム設定 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) カスタム数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 - `system_parameters` (object) システムパラメータ - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx index 325470ac62..a7127d614b 100644 --- a/web/app/components/develop/template/template_chat.zh.mdx +++ b/web/app/components/develop/template/template_chat.zh.mdx @@ -73,6 +73,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' (选填)自动生成标题,默认 `true`。 若设置为 `false`,则可通过调用会话重命名接口并设置 `auto_generate` 为 `true` 实现异步生成标题。 + + (选填)链路追踪ID。适用于与业务系统已有的trace组件打通,实现端到端分布式追踪等场景。如果未指定,系统会自动生成trace_id。支持以下三种方式传递,具体优先级依次为:
    + - Header:通过 HTTP Header X-Trace-Id 传递,优先级最高。
    + - Query 参数:通过 URL 查询参数 trace_id 传递。
    + - Request Body:通过请求体字段 trace_id 传递(即本字段)。
    +
    ### Response @@ -1237,11 +1243,30 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `default` (string) 默认值 - `options` (array[string]) 选项值 - `file_upload` (object) 文件上传配置 - - `image` (object) 图片设置 - 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 是否开启 - - `number_limits` (int) 图片数量限制,默认 3 - - `transfer_methods` (array[string]) 传递方式列表,remote_url , local_file,必选一个 + - `document` (object) 文档设置 + 当前仅支持文档类型:`txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 文档数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `image` (object) 图片设置 + 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 图片数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `audio` (object) 音频设置 + 当前仅支持音频类型:`mp3`, `m4a`, `wav`, `webm`, `amr`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 音频数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `video` (object) 视频设置 + 当前仅支持视频类型:`mp4`, `mov`, `mpeg`, `mpga`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 视频数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `custom` (object) 自定义设置 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 自定义数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 - `system_parameters` (object) 系统参数 - `file_size_limit` (int) 文档上传大小限制 (MB) - `image_file_size_limit` (int) 图片文件上传大小限制(MB) diff --git a/web/app/components/develop/template/template_workflow.en.mdx b/web/app/components/develop/template/template_workflow.en.mdx index 77409c1284..8ac1db3287 100644 --- a/web/app/components/develop/template/template_workflow.en.mdx +++ b/web/app/components/develop/template/template_workflow.en.mdx @@ -66,6 +66,12 @@ Workflow applications offers non-session support and is ideal for translation, a Should be uniquely defined by the developer within the application.
    The user identifier should be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp. + - `files` (array[object]) Optional + - `trace_id` (string) Optional + Trace ID. Used for integration with existing business trace components to achieve end-to-end distributed tracing. If not provided, the system will automatically generate a trace_id. Supports the following three ways to pass, in order of priority: + 1. Header: via HTTP Header `X-Trace-Id`, highest priority. + 2. Query parameter: via URL query parameter `trace_id`. + 3. Request Body: via request body field `trace_id` (i.e., this field). ### Response When `response_mode` is `blocking`, return a CompletionResponse object. @@ -690,11 +696,30 @@ Workflow applications offers non-session support and is ideal for translation, a - `default` (string) Default value - `options` (array[string]) Option values - `file_upload` (object) File upload configuration - - `image` (object) Image settings - Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) Whether it is enabled - - `number_limits` (int) Image number limit, default is 3 - - `transfer_methods` (array[string]) List of transfer methods, remote_url, local_file, must choose one + - `document` (object) Document settings + Currently only supports document types: `txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Document number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `image` (object) Image settings + Currently only supports image types: `png`, `jpg`, `jpeg`, `webp`, `gif`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Image number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `audio` (object) Audio settings + Currently only supports audio types: `mp3`, `m4a`, `wav`, `webm`, `amr`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Audio number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `video` (object) Video settings + Currently only supports video types: `mp4`, `mov`, `mpeg`, `mpga`. + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Video number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. + - `custom` (object) Custom settings + - `enabled` (bool) Whether it is enabled + - `number_limits` (int) Custom number limit, default is 3 + - `transfer_methods` (array[string]) List of transfer methods: `remote_url`, `local_file`. Must choose one. - `system_parameters` (object) System parameters - `file_size_limit` (int) Document upload size limit (MB) - `image_file_size_limit` (int) Image file upload size limit (MB) diff --git a/web/app/components/develop/template/template_workflow.ja.mdx b/web/app/components/develop/template/template_workflow.ja.mdx index a83e21aef7..0c32467ce8 100644 --- a/web/app/components/develop/template/template_workflow.ja.mdx +++ b/web/app/components/develop/template/template_workflow.ja.mdx @@ -66,6 +66,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用されます。 アプリケーション内で開発者によって一意に定義される必要があります。 - `files` (array[object]) オプション + - `trace_id` (string) オプション + トレースID。既存の業務システムのトレースコンポーネントと連携し、エンドツーエンドの分散トレーシングを実現するために使用します。指定がない場合、システムが自動的に trace_id を生成します。以下の3つの方法で渡すことができ、優先順位は次のとおりです: + 1. Header:HTTPヘッダー `X-Trace-Id` で渡す(最優先)。 + 2. クエリパラメータ:URLクエリパラメータ `trace_id` で渡す。 + 3. リクエストボディ:リクエストボディの `trace_id` フィールドで渡す(本フィールド)。 ### 応答 @@ -691,11 +696,30 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `default` (string) デフォルト値 - `options` (array[string]) オプション値 - `file_upload` (object) ファイルアップロード設定 - - `image` (object) 画像設定 - 現在サポートされている画像タイプのみ:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 有効かどうか - - `number_limits` (int) 画像数の制限、デフォルトは3 - - `transfer_methods` (array[string]) 転送方法のリスト、remote_url, local_file、いずれかを選択する必要があります + - `document` (object) ドキュメント設定 + 現在サポートされているドキュメントタイプ:`txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) ドキュメント数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `image` (object) 画像設定 + 現在サポートされている画像タイプ:`png`, `jpg`, `jpeg`, `webp`, `gif`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `audio` (object) オーディオ設定 + 現在サポートされているオーディオタイプ:`mp3`, `m4a`, `wav`, `webm`, `amr`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) オーディオ数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `video` (object) ビデオ設定 + 現在サポートされているビデオタイプ:`mp4`, `mov`, `mpeg`, `mpga`。 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) ビデオ数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 + - `custom` (object) カスタム設定 + - `enabled` (bool) 有効かどうか + - `number_limits` (int) カスタム数の上限。デフォルトは 3 + - `transfer_methods` (array[string]) 転送方法リスト:`remote_url`, `local_file`。いずれかを選択する必要があります。 - `system_parameters` (object) システムパラメータ - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) diff --git a/web/app/components/develop/template/template_workflow.zh.mdx b/web/app/components/develop/template/template_workflow.zh.mdx index 42922610da..236da62e88 100644 --- a/web/app/components/develop/template/template_workflow.zh.mdx +++ b/web/app/components/develop/template/template_workflow.zh.mdx @@ -60,7 +60,12 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - `user` (string) Required 用户标识,用于定义终端用户的身份,方便检索、统计。 由开发者定义规则,需保证用户标识在应用内唯一。API 无法访问 WebApp 创建的会话。 - + - `files` (array[object]) 可选 + - `trace_id` (string) Optional + 链路追踪ID。适用于与业务系统已有的trace组件打通,实现端到端分布式追踪等场景。如果未指定,系统将自动生成 `trace_id`。支持以下三种方式传递,具体优先级依次为: + 1. Header:推荐通过 HTTP Header `X-Trace-Id` 传递,优先级最高。 + 2. Query 参数:通过 URL 查询参数 `trace_id` 传递。 + 3. Request Body:通过请求体字段 `trace_id` 传递(即本字段)。 ### Response 当 `response_mode` 为 `blocking` 时,返回 CompletionResponse object。 @@ -678,11 +683,30 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - `default` (string) 默认值 - `options` (array[string]) 选项值 - `file_upload` (object) 文件上传配置 - - `image` (object) 图片设置 - 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif` - - `enabled` (bool) 是否开启 - - `number_limits` (int) 图片数量限制,默认 3 - - `transfer_methods` (array[string]) 传递方式列表,remote_url , local_file,必选一个 + - `document` (object) 文档设置 + 当前仅支持文档类型:`txt`, `md`, `markdown`, `pdf`, `html`, `xlsx`, `xls`, `docx`, `csv`, `eml`, `msg`, `pptx`, `ppt`, `xml`, `epub`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 文档数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `image` (object) 图片设置 + 当前仅支持图片类型:`png`, `jpg`, `jpeg`, `webp`, `gif`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 图片数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `audio` (object) 音频设置 + 当前仅支持音频类型:`mp3`, `m4a`, `wav`, `webm`, `amr`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 音频数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `video` (object) 视频设置 + 当前仅支持视频类型:`mp4`, `mov`, `mpeg`, `mpga`。 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 视频数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 + - `custom` (object) 自定义设置 + - `enabled` (bool) 是否启用 + - `number_limits` (int) 自定义数量限制,默认为 3 + - `transfer_methods` (array[string]) 传输方式列表:`remote_url`, `local_file`,必须选择一个。 - `system_parameters` (object) 系统参数 - `file_size_limit` (int) 文档上传大小限制 (MB) - `image_file_size_limit` (int) 图片文件上传大小限制(MB) diff --git a/web/app/components/header/account-about/index.tsx b/web/app/components/header/account-about/index.tsx index 280e276be9..2eb8cdf82f 100644 --- a/web/app/components/header/account-about/index.tsx +++ b/web/app/components/header/account-about/index.tsx @@ -12,16 +12,16 @@ import { noop } from 'lodash-es' import { useGlobalPublicStore } from '@/context/global-public-context' type IAccountSettingProps = { - langeniusVersionInfo: LangGeniusVersionResponse + langGeniusVersionInfo: LangGeniusVersionResponse onCancel: () => void } export default function AccountAbout({ - langeniusVersionInfo, + langGeniusVersionInfo, onCancel, }: IAccountSettingProps) { const { t } = useTranslation() - const isLatest = langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version + const isLatest = langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return ( @@ -43,7 +43,7 @@ export default function AccountAbout({ /> : } -
    Version {langeniusVersionInfo?.current_version}
    +
    Version {langGeniusVersionInfo?.current_version}
    © {dayjs().year()} LangGenius, Inc., Contributors.
    @@ -63,8 +63,8 @@ export default function AccountAbout({
    { isLatest - ? t('common.about.latestAvailable', { version: langeniusVersionInfo.latest_version }) - : t('common.about.nowAvailable', { version: langeniusVersionInfo.latest_version }) + ? t('common.about.latestAvailable', { version: langGeniusVersionInfo.latest_version }) + : t('common.about.nowAvailable', { version: langGeniusVersionInfo.latest_version }) }
    @@ -80,7 +80,7 @@ export default function AccountAbout({ !isLatest && !IS_CE_EDITION && (
    @@ -217,7 +217,7 @@ export default function AppSelector() { } { - aboutVisible && setAboutVisible(false)} langeniusVersionInfo={langeniusVersionInfo} /> + aboutVisible && setAboutVisible(false)} langGeniusVersionInfo={langGeniusVersionInfo} /> }
    ) diff --git a/web/app/components/header/account-dropdown/support.tsx b/web/app/components/header/account-dropdown/support.tsx index e4731d2b6e..6435bcaeb4 100644 --- a/web/app/components/header/account-dropdown/support.tsx +++ b/web/app/components/header/account-dropdown/support.tsx @@ -16,7 +16,7 @@ export default function Support() { ` const { t } = useTranslation() const { plan } = useProviderContext() - const { userProfile, langeniusVersionInfo } = useAppContext() + const { userProfile, langGeniusVersionInfo } = useAppContext() const canEmailSupport = plan.type === Plan.professional || plan.type === Plan.team || plan.type === Plan.enterprise return @@ -53,7 +53,7 @@ export default function Support() { className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover', )} - href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)} + href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo.current_version)} target='_blank' rel='noopener noreferrer'>
    {t('common.userProfile.emailSupport')}
    diff --git a/web/app/components/header/dataset-nav/index.tsx b/web/app/components/header/dataset-nav/index.tsx index 85223f9f37..d10bf94ebe 100644 --- a/web/app/components/header/dataset-nav/index.tsx +++ b/web/app/components/header/dataset-nav/index.tsx @@ -14,7 +14,6 @@ import Nav from '../nav' import type { NavItem } from '../nav/nav-selector' import { fetchDatasetDetail, fetchDatasets } from '@/service/datasets' import type { DataSetListResponse } from '@/models/datasets' -import { basePath } from '@/utils/var' const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { if (!pageIndex || previousPageData.has_more) @@ -57,7 +56,7 @@ const DatasetNav = () => { icon_background: dataset.icon_background, })) as NavItem[]} createText={t('common.menus.newDataset')} - onCreate={() => router.push(`${basePath}/datasets/create`)} + onCreate={() => router.push('/datasets/create')} onLoadmore={handleLoadmore} isApp={false} /> diff --git a/web/app/components/header/env-nav/index.tsx b/web/app/components/header/env-nav/index.tsx index 3f0b0f01dd..e7535c69f0 100644 --- a/web/app/components/header/env-nav/index.tsx +++ b/web/app/components/header/env-nav/index.tsx @@ -12,8 +12,8 @@ const headerEnvClassName: { [k: string]: string } = { const EnvNav = () => { const { t } = useTranslation() - const { langeniusVersionInfo } = useAppContext() - const showEnvTag = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT' + const { langGeniusVersionInfo } = useAppContext() + const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT' if (!showEnvTag) return null @@ -21,10 +21,10 @@ const EnvNav = () => { return (
    { - langeniusVersionInfo.current_env === 'TESTING' && ( + langGeniusVersionInfo.current_env === 'TESTING' && ( <>
    {t('common.environment.testing')}
    @@ -32,7 +32,7 @@ const EnvNav = () => { ) } { - langeniusVersionInfo.current_env === 'DEVELOPMENT' && ( + langGeniusVersionInfo.current_env === 'DEVELOPMENT' && ( <>
    {t('common.environment.development')}
    diff --git a/web/app/components/i18n.tsx b/web/app/components/i18n.tsx index f04f8d6cbe..374b1f608f 100644 --- a/web/app/components/i18n.tsx +++ b/web/app/components/i18n.tsx @@ -1,10 +1,13 @@ 'use client' import type { FC } from 'react' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import I18NContext from '@/context/i18n' import type { Locale } from '@/i18n' import { setLocaleOnClient } from '@/i18n' +import Loading from './base/loading' +import { usePrefetchQuery } from '@tanstack/react-query' +import { getSystemFeatures } from '@/service/common' export type II18nProps = { locale: Locale @@ -14,10 +17,22 @@ const I18n: FC = ({ locale, children, }) => { + const [loading, setLoading] = useState(true) + + usePrefetchQuery({ + queryKey: ['systemFeatures'], + queryFn: getSystemFeatures, + }) + useEffect(() => { - setLocaleOnClient(locale, false) + setLocaleOnClient(locale, false).then(() => { + setLoading(false) + }) }, [locale]) + if (loading) + return
    + return ( = ({ + status, + deprecatedReason, + alternativePluginId, + alternativePluginURL, + locale, + className, + innerWrapperClassName, + iconWrapperClassName, + textClassName, +}) => { + const { t } = useMixedTranslation(locale) + + const deprecatedReasonKey = useMemo(() => { + if (!deprecatedReason) return '' + return camelCase(deprecatedReason) + }, [deprecatedReason]) + + // Check if the deprecatedReasonKey exists in i18n + const hasValidDeprecatedReason = useMemo(() => { + if (!deprecatedReason || !deprecatedReasonKey) return false + + // Define valid reason keys that exist in i18n + const validReasonKeys = ['businessAdjustments', 'ownershipTransferred', 'noMaintainer'] + return validReasonKeys.includes(deprecatedReasonKey) + }, [deprecatedReason, deprecatedReasonKey]) + + if (status !== 'deleted') + return null + + return ( +
    +
    +
    +
    + +
    +
    + { + hasValidDeprecatedReason && alternativePluginId && ( + + ), + }} + values={{ + deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}`), + alternativePluginId, + }} + /> + ) + } + { + hasValidDeprecatedReason && !alternativePluginId && ( + + {t(`${i18nPrefix}.onlyReason`, { deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}`) })} + + ) + } + { + !hasValidDeprecatedReason && ( + {t(`${i18nPrefix}.noReason`)} + ) + } +
    +
    +
    + ) +} + +export default React.memo(DeprecationNotice) 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/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index 1ddc52ced9..ff4bb8de90 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -48,7 +48,6 @@ const Installed: FC = ({ useEffect(() => { if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier) onInstalled() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasInstalled]) const [isInstalling, setIsInstalling] = React.useState(false) @@ -105,12 +104,12 @@ const Installed: FC = ({ } } - const { langeniusVersionInfo } = useAppContext() + const { langGeniusVersionInfo } = useAppContext() const isDifyVersionCompatible = useMemo(() => { - if (!langeniusVersionInfo.current_version) + if (!langGeniusVersionInfo.current_version) return true - return gte(langeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0') - }, [langeniusVersionInfo.current_version, payload.meta.minimum_dify_version]) + return gte(langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0') + }, [langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version]) return ( <> diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index dbc7c97d88..3bbf8c9a39 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -59,7 +59,6 @@ const Installed: FC = ({ useEffect(() => { if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier) onInstalled() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasInstalled]) const handleCancel = () => { @@ -120,12 +119,12 @@ const Installed: FC = ({ } } - const { langeniusVersionInfo } = useAppContext() + const { langGeniusVersionInfo } = useAppContext() const { data: pluginDeclaration } = usePluginDeclarationFromMarketPlace(uniqueIdentifier) const isDifyVersionCompatible = useMemo(() => { - if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true - return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') - }, [langeniusVersionInfo.current_version, pluginDeclaration]) + if (!pluginDeclaration || !langGeniusVersionInfo.current_version) return true + return gte(langGeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') + }, [langGeniusVersionInfo.current_version, pluginDeclaration]) const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' }) return ( diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index 7a29556bda..d6189a92a1 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -6,7 +6,7 @@ import PluginTypeSwitch from './plugin-type-switch' import ListWrapper from './list/list-wrapper' import type { SearchParams } from './types' import { getMarketplaceCollectionsAndPlugins } from './utils' -import { TanstackQueryIniter } from '@/context/query-client' +import { TanstackQueryInitializer } from '@/context/query-client' type MarketplaceProps = { locale: string @@ -39,7 +39,7 @@ const Marketplace = async ({ } return ( - + - + ) } 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 124e133c2b..e5d1458140 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -29,7 +29,7 @@ import Toast from '@/app/components/base/toast' import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin' import { Github } from '@/app/components/base/icons/src/public/common' import { uninstallPlugin } from '@/service/plugins' -import { useGetLanguage } from '@/context/i18n' +import { useGetLanguage, useI18N } from '@/context/i18n' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { useInvalidateAllToolProviders } from '@/service/use-tools' @@ -39,6 +39,12 @@ import { getMarketplaceUrl } from '@/utils/var' import { PluginAuth } from '@/app/components/plugins/plugin-auth' import { AuthCategory } from '@/app/components/plugins/plugin-auth' import { useAllToolProviders } from '@/service/use-tools' +import DeprecationNotice from '../base/deprecation-notice' +import { AutoUpdateLine } from '../../base/icons/src/vender/system' +import { convertUTCDaySecondsToLocalSeconds, timeOfDayToDayjs } from '../reference-setting-modal/auto-update-setting/utils' +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' const i18nPrefix = 'plugin.action' @@ -54,8 +60,11 @@ const DetailHeader = ({ onUpdate, }: Props) => { const { t } = useTranslation() + const { userProfile: { timezone } } = useAppContext() + const { theme } = useTheme() const locale = useGetLanguage() + const { locale: currentLocale } = useI18N() const { checkForUpdates, fetchReleases } = useGitHubReleases() const { setShowUpdatePluginModal } = useModalContext() const { refreshModelProviders } = useProviderContext() @@ -70,6 +79,9 @@ const DetailHeader = ({ latest_version, meta, plugin_id, + status, + deprecated_reason, + alternative_plugin_id, } = detail const { author, category, name, label, description, icon, verified, tool } = detail.declaration const isTool = category === PluginType.tool @@ -98,7 +110,7 @@ const DetailHeader = ({ if (isFromGitHub) return `https://github.com/${meta!.repo}` if (isFromMarketplace) - return getMarketplaceUrl(`/plugins/${author}/${name}`, { theme }) + return getMarketplaceUrl(`/plugins/${author}/${name}`, { language: currentLocale, theme }) return '' }, [author, isFromGitHub, isFromMarketplace, meta, name, theme]) @@ -107,8 +119,24 @@ const DetailHeader = ({ setFalse: hideUpdateModal, }] = useBoolean(false) - const handleUpdate = async () => { + const { referenceSetting } = useReferenceSetting() + const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {} + const isAutoUpgradeEnabled = useMemo(() => { + if (!autoUpgradeInfo || !isFromMarketplace) + 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, isFromMarketplace]) + + const [isDowngrade, setIsDowngrade] = useState(false) + const handleUpdate = async (isDowngrade?: boolean) => { if (isFromMarketplace) { + setIsDowngrade(!!isDowngrade) showUpdateModal() return } @@ -175,9 +203,6 @@ const DetailHeader = ({ } }, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders]) - // #plugin TODO# used in apps - // const usedInApps = 3 - return (
    @@ -196,7 +221,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) && (
    + {isFromMarketplace && ( + + )} { category === PluginType.tool && ( @@ -310,6 +356,7 @@ const DetailHeader = ({ { isShowUpdateModal && ( ) } 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 9c7c7a0c41..d2797b99f4 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 @@ -265,7 +265,7 @@ const ToolSelector: FC = ({ /> )} - +
    <>
    {t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}
    @@ -309,15 +309,15 @@ const ToolSelector: FC = ({ {currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && ( <> -
    - +
    +
    )} diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 058f1783f2..c228ca4db4 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { useTheme } from 'next-themes' import { RiArrowRightUpLine, @@ -55,6 +55,8 @@ const PluginItem: FC = ({ endpoints_active, meta, plugin_id, + status, + deprecated_reason, } = plugin const { category, author, name, label, description, icon, verified, meta: declarationMeta } = plugin.declaration @@ -62,17 +64,22 @@ const PluginItem: FC = ({ return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : '' }, [source, author]) - const { langeniusVersionInfo } = useAppContext() + const { langGeniusVersionInfo } = useAppContext() const isDifyVersionCompatible = useMemo(() => { - if (!langeniusVersionInfo.current_version) + if (!langGeniusVersionInfo.current_version) return true - return gte(langeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0') - }, [declarationMeta.minimum_dify_version, langeniusVersionInfo.current_version]) + return gte(langGeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0') + }, [declarationMeta.minimum_dify_version, langGeniusVersionInfo.current_version]) - const handleDelete = () => { + const isDeprecated = useMemo(() => { + return status === 'deleted' && !!deprecated_reason + }, [status, deprecated_reason]) + + const handleDelete = useCallback(() => { refreshPluginList({ category } as any) - } + }, [category, refreshPluginList]) + const getValueFromI18nObject = useRenderI18nObject() const title = getValueFromI18nObject(label) const descriptionText = getValueFromI18nObject(description) @@ -81,7 +88,7 @@ const PluginItem: FC = ({ return (
    = ({ setCurrentPluginID(plugin.plugin_id) }} > -
    +
    {/* Header */} -
    +
    = ({ alt={`plugin-${plugin_unique_identifier}-logo`} />
    -
    -
    +
    +
    - {verified && <RiVerifiedBadgeLine className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" />} + {verified && <RiVerifiedBadgeLine className='ml-0.5 h-4 w-4 shrink-0 text-text-accent' />} {!isDifyVersionCompatible && <Tooltip popupContent={ t('plugin.difyVersionNotCompatible', { minimalDifyVersion: declarationMeta.minimum_dify_version }) - }><RiErrorWarningLine color='red' className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /></Tooltip>} + }><RiErrorWarningLine color='red' className='ml-0.5 h-4 w-4 shrink-0 text-text-accent' /></Tooltip>} <Badge className='ml-1 shrink-0' text={source === PluginSource.github ? plugin.meta!.version : plugin.version} hasRedCornerMark={(source === PluginSource.marketplace) && !!plugin.latest_version && plugin.latest_version !== plugin.version} @@ -135,10 +142,11 @@ const PluginItem: FC<Props> = ({ </div> </div> </div> - <div className='mb-1 mt-1.5 flex h-4 items-center justify-between px-4'> - <div className='flex items-center'> + <div className='mb-1 mt-1.5 flex h-4 items-center gap-x-2 px-4'> + {/* Organization & Name */} + <div className='flex grow items-center overflow-hidden'> <OrgInfo - className="mt-0.5" + className='mt-0.5' orgName={orgName} packageName={name} packageNameClassName='w-auto max-w-[150px]' @@ -146,15 +154,20 @@ const PluginItem: FC<Props> = ({ {category === PluginType.extension && ( <> <div className='system-xs-regular mx-2 text-text-quaternary'>·</div> - <div className='system-xs-regular flex space-x-1 text-text-tertiary'> - <RiLoginCircleLine className='h-4 w-4' /> - <span>{t('plugin.endpointsEnabled', { num: endpoints_active })}</span> + <div className='system-xs-regular flex space-x-1 overflow-hidden text-text-tertiary'> + <RiLoginCircleLine className='h-4 w-4 shrink-0' /> + <span + className='truncate' + title={t('plugin.endpointsEnabled', { num: endpoints_active })} + > + {t('plugin.endpointsEnabled', { num: endpoints_active })} + </span> </div> </> )} </div> - - <div className='flex items-center'> + {/* Source */} + <div className='flex shrink-0 items-center'> {source === PluginSource.github && <> <a href={`https://github.com/${meta!.repo}`} target='_blank' className='flex items-center gap-1'> @@ -192,7 +205,20 @@ const PluginItem: FC<Props> = ({ </> } </div> + {/* Deprecated */} + {source === PluginSource.marketplace && enable_marketplace && isDeprecated && ( + <div className='system-2xs-medium-uppercase flex shrink-0 items-center gap-x-2'> + <span className='text-text-tertiary'>·</span> + <span className='text-text-warning'> + {t('plugin.deprecated')} + </span> + </div> + )} </div> + {/* BG Effect for Deprecated Plugin */} + {source === PluginSource.marketplace && enable_marketplace && isDeprecated && ( + <div className='absolute bottom-[-71px] right-[-45px] z-0 size-40 bg-components-badge-status-light-warning-halo opacity-60 blur-[120px]' /> + )} </div> ) } diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index 94fd3fee9b..894a8b7f45 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -17,14 +17,14 @@ import { } from './context' import InstallPluginDropdown from './install-plugin-dropdown' import { useUploader } from './use-uploader' -import usePermission from './use-permission' +import useReferenceSetting from './use-reference-setting' import DebugInfo from './debug-info' import PluginTasks from './plugin-tasks' import Button from '@/app/components/base/button' import TabSlider from '@/app/components/base/tab-slider' import Tooltip from '@/app/components/base/tooltip' import cn from '@/utils/classnames' -import PermissionSetModal from '@/app/components/plugins/permission-setting-modal/modal' +import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal/modal' import InstallFromMarketplace from '../install-plugin/install-from-marketplace' import { useRouter, @@ -121,16 +121,16 @@ const PluginPage = ({ }, [packageId, bundleInfo]) const { + referenceSetting, canManagement, canDebugger, canSetPermissions, - permissions, - setPermissions, - } = usePermission() + setReferenceSettings, + } = useReferenceSetting() const [showPluginSettingModal, { setTrue: setShowPluginSettingModal, setFalse: setHidePluginSettingModal, - }] = useBoolean() + }] = useBoolean(false) const [currentFile, setCurrentFile] = useState<File | null>(null) const containerRef = usePluginPageContext(v => v.containerRef) const options = usePluginPageContext(v => v.options) @@ -276,10 +276,10 @@ const PluginPage = ({ } {showPluginSettingModal && ( - <PermissionSetModal - payload={permissions!} + <ReferenceSettingModal + payload={referenceSetting!} onHide={setHidePluginSettingModal} - onSave={setPermissions} + onSave={setReferenceSettings} /> )} diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index a5f411c37e..ef4911e523 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -36,6 +36,9 @@ const PluginsPanel = () => { ...plugin, latest_version: installedLatestVersion?.versions[plugin.plugin_id]?.version ?? '', latest_unique_identifier: installedLatestVersion?.versions[plugin.plugin_id]?.unique_identifier ?? '', + status: installedLatestVersion?.versions[plugin.plugin_id]?.status ?? 'active', + deprecated_reason: installedLatestVersion?.versions[plugin.plugin_id]?.deprecated_reason ?? '', + alternative_plugin_id: installedLatestVersion?.versions[plugin.plugin_id]?.alternative_plugin_id ?? '', })) || [] }, [pluginList, installedLatestVersion]) @@ -66,20 +69,25 @@ const PluginsPanel = () => { onFilterChange={handleFilterChange} /> </div> - {isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? ( - <div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch px-12'> - <div className='w-full'> - <List pluginList={filteredList || []} /> - </div> - {!isLastPage && !isFetching && ( - <Button onClick={loadNextPage}> - {t('workflow.common.loadMore')} - </Button> + {isPluginListLoading && <Loading type='app' />} + {!isPluginListLoading && ( + <> + {(filteredList?.length ?? 0) > 0 ? ( + <div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch px-12'> + <div className='w-full'> + <List pluginList={filteredList || []} /> + </div> + {!isLastPage && !isFetching && ( + <Button onClick={loadNextPage}> + {t('workflow.common.loadMore')} + </Button> + )} + {isFetching && <div className='system-md-semibold text-text-secondary'>{t('appLog.detail.loading')}</div>} + </div> + ) : ( + <Empty /> )} - {isFetching && <div className='system-md-semibold text-text-secondary'>{t('appLog.detail.loading')}</div>} - </div> - ) : ( - <Empty /> + </> )} <PluginDetailPanel detail={currentPluginDetail} diff --git a/web/app/components/plugins/plugin-page/use-permission.ts b/web/app/components/plugins/plugin-page/use-reference-setting.ts similarity index 68% rename from web/app/components/plugins/plugin-page/use-permission.ts rename to web/app/components/plugins/plugin-page/use-reference-setting.ts index 918813fb44..dbf5097c07 100644 --- a/web/app/components/plugins/plugin-page/use-permission.ts +++ b/web/app/components/plugins/plugin-page/use-reference-setting.ts @@ -2,7 +2,7 @@ import { PermissionType } from '../types' import { useAppContext } from '@/context/app-context' import Toast from '../../base/toast' import { useTranslation } from 'react-i18next' -import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins' +import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins' import { useMemo } from 'react' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -19,14 +19,16 @@ const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean) return isAdmin } -const usePermission = () => { +const useReferenceSetting = () => { const { t } = useTranslation() const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext() - const { data: permissions } = usePermissions() - const invalidatePermissions = useInvalidatePermissions() - const { mutate: updatePermission, isPending: isUpdatePending } = useMutationPermissions({ + const { data } = useReferenceSettings() + // console.log(data) + const { permission: permissions } = data || {} + const invalidateReferenceSettings = useInvalidateReferenceSettings() + const { mutate: updateReferenceSetting, isPending: isUpdatePending } = useMutationReferenceSettings({ onSuccess: () => { - invalidatePermissions() + invalidateReferenceSettings() Toast.notify({ type: 'success', message: t('common.api.actionSuccess'), @@ -36,18 +38,18 @@ const usePermission = () => { const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner return { + referenceSetting: data, + setReferenceSettings: updateReferenceSetting, canManagement: hasPermission(permissions?.install_permission, isAdmin), canDebugger: hasPermission(permissions?.debug_permission, isAdmin), canSetPermissions: isAdmin, - permissions, - setPermissions: updatePermission, isUpdatePending, } } export const useCanInstallPluginFromMarketplace = () => { const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) - const { canManagement } = usePermission() + const { canManagement } = useReferenceSetting() const canInstallPluginFromMarketplace = useMemo(() => { return enable_marketplace && canManagement @@ -58,4 +60,4 @@ export const useCanInstallPluginFromMarketplace = () => { } } -export default usePermission +export default useReferenceSetting diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/config.ts b/web/app/components/plugins/reference-setting-modal/auto-update-setting/config.ts new file mode 100644 index 0000000000..084dfc9731 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/config.ts @@ -0,0 +1,9 @@ +import type { AutoUpdateConfig } from './types' +import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from './types' +export const defaultValue: AutoUpdateConfig = { + strategy_setting: AUTO_UPDATE_STRATEGY.disabled, + upgrade_time_of_day: 0, + upgrade_mode: AUTO_UPDATE_MODE.update_all, + exclude_plugins: [], + include_plugins: [], +} diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx new file mode 100644 index 0000000000..a4875fc11b --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx @@ -0,0 +1,185 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useMemo } from 'react' +import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY, type AutoUpdateConfig } from './types' +import Label from '../label' +import StrategyPicker from './strategy-picker' +import { Trans, useTranslation } from 'react-i18next' +import TimePicker from '@/app/components/base/date-and-time-picker/time-picker' +import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' +import PluginsPicker from './plugins-picker' +import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from './utils' +import { useAppContext } from '@/context/app-context' +import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types' +import { RiTimeLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs' +import { useModalContextSelector } from '@/context/modal-context' + +const i18nPrefix = 'plugin.autoUpdate' + +type Props = { + payload: AutoUpdateConfig + onChange: (payload: AutoUpdateConfig) => void +} + +const SettingTimeZone: FC<{ + children?: React.ReactNode +}> = ({ + children, +}) => { + const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) + return ( + <span className='body-xs-regular cursor-pointer text-text-accent' onClick={() => setShowAccountSettingModal({ payload: 'language' })} >{children}</span> + ) +} +const AutoUpdateSetting: FC<Props> = ({ + payload, + onChange, +}) => { + const { t } = useTranslation() + const { userProfile: { timezone } } = useAppContext() + + const { + strategy_setting, + upgrade_time_of_day, + upgrade_mode, + exclude_plugins, + include_plugins, + } = payload + + const minuteFilter = useCallback((minutes: string[]) => { + return minutes.filter((m) => { + const time = Number.parseInt(m, 10) + return time % 15 === 0 + }) + }, []) + const strategyDescription = useMemo(() => { + switch (strategy_setting) { + case AUTO_UPDATE_STRATEGY.fixOnly: + return t(`${i18nPrefix}.strategy.fixOnly.selectedDescription`) + case AUTO_UPDATE_STRATEGY.latest: + return t(`${i18nPrefix}.strategy.latest.selectedDescription`) + default: + return '' + } + }, [strategy_setting, t]) + + const plugins = useMemo(() => { + switch (upgrade_mode) { + case AUTO_UPDATE_MODE.partial: + return include_plugins + case AUTO_UPDATE_MODE.exclude: + return exclude_plugins + default: + return [] + } + }, [upgrade_mode, exclude_plugins, include_plugins]) + + const handlePluginsChange = useCallback((newPlugins: string[]) => { + if (upgrade_mode === AUTO_UPDATE_MODE.partial) { + onChange({ + ...payload, + include_plugins: newPlugins, + }) + } + else if (upgrade_mode === AUTO_UPDATE_MODE.exclude) { + onChange({ + ...payload, + exclude_plugins: newPlugins, + }) + } + }, [payload, upgrade_mode, onChange]) + const handleChange = useCallback((key: keyof AutoUpdateConfig) => { + return (value: AutoUpdateConfig[keyof AutoUpdateConfig]) => { + onChange({ + ...payload, + [key]: value, + }) + } + }, [payload, onChange]) + + const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => { + return ( + <div + className='group float-right flex h-8 w-[160px] cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal px-2 hover:bg-state-base-hover-alt' + onClick={onClick} + > + <div className='flex w-0 grow items-center gap-x-1'> + <RiTimeLine className={cn( + 'h-4 w-4 shrink-0 text-text-tertiary', + isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary', + )} /> + {inputElem} + </div> + <div className='system-sm-regular text-text-tertiary'>{convertTimezoneToOffsetStr(timezone)}</div> + </div> + ) + }, [timezone]) + + return ( + <div className='self-stretch px-6'> + <div className='my-3 flex items-center'> + <div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.updateSettings`)}</div> + <div className='ml-2 h-px grow bg-divider-subtle'></div> + </div> + + <div className='space-y-4'> + <div className='flex items-center justify-between'> + <Label label={t(`${i18nPrefix}.automaticUpdates`)} description={strategyDescription} /> + <StrategyPicker value={strategy_setting} onChange={handleChange('strategy_setting')} /> + </div> + {strategy_setting !== AUTO_UPDATE_STRATEGY.disabled && ( + <> + <div className='flex items-center justify-between'> + <Label label={t(`${i18nPrefix}.updateTime`)} /> + <div className='flex flex-col justify-start'> + <TimePicker + value={timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(upgrade_time_of_day, timezone!))} + timezone={timezone} + onChange={v => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(dayjsToTimeOfDay(v), timezone!))} + onClear={() => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(0, timezone!))} + popupClassName='z-[99]' + title={t(`${i18nPrefix}.updateTime`)} + minuteFilter={minuteFilter} + renderTrigger={renderTimePickerTrigger} + /> + <div className='body-xs-regular mt-1 text-right text-text-tertiary'> + <Trans + i18nKey={`${i18nPrefix}.changeTimezone`} + components={{ + setTimezone: <SettingTimeZone />, + }} + /> + </div> + </div> + </div> + <div> + <Label label={t(`${i18nPrefix}.specifyPluginsToUpdate`)} /> + <div className='mt-1 flex w-full items-start justify-between gap-2'> + {[AUTO_UPDATE_MODE.update_all, AUTO_UPDATE_MODE.exclude, AUTO_UPDATE_MODE.partial].map(option => ( + <OptionCard + key={option} + title={t(`${i18nPrefix}.upgradeMode.${option}`)} + onSelect={() => handleChange('upgrade_mode')(option)} + selected={upgrade_mode === option} + className="flex-1" + /> + ))} + </div> + + {upgrade_mode !== AUTO_UPDATE_MODE.update_all && ( + <PluginsPicker + value={plugins} + onChange={handlePluginsChange} + updateMode={upgrade_mode} + /> + )} + </div> + </> + )} + </div> + </div> + ) +} +export default React.memo(AutoUpdateSetting) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-data-placeholder.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-data-placeholder.tsx new file mode 100644 index 0000000000..979dc626e8 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-data-placeholder.tsx @@ -0,0 +1,31 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import cn from '@/utils/classnames' +import { Group } from '@/app/components/base/icons/src/vender/other' +import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general' +import { useTranslation } from 'react-i18next' + +type Props = { + className: string + noPlugins?: boolean +} + +const NoDataPlaceholder: FC<Props> = ({ + className, + noPlugins, +}) => { + const { t } = useTranslation() + const icon = noPlugins ? (<Group className='size-6 text-text-quaternary' />) : (<SearchMenu className='size-8 text-text-tertiary' />) + const text = t(`plugin.autoUpdate.noPluginPlaceholder.${noPlugins ? 'noInstalled' : 'noFound'}`) + return ( + <div className={cn('flex items-center justify-center', className)}> + <div className='flex flex-col items-center'> + {icon} + <div className='system-sm-regular mt-2 text-text-tertiary'>{text}</div> + </div> + </div> + ) +} + +export default React.memo(NoDataPlaceholder) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-plugin-selected.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-plugin-selected.tsx new file mode 100644 index 0000000000..e255be0525 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-plugin-selected.tsx @@ -0,0 +1,22 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { AUTO_UPDATE_MODE } from './types' +import { useTranslation } from 'react-i18next' + +type Props = { + updateMode: AUTO_UPDATE_MODE +} + +const NoPluginSelected: FC<Props> = ({ + updateMode, +}) => { + const { t } = useTranslation() + const text = `${t(`plugin.autoUpdate.upgradeModePlaceholder.${updateMode === AUTO_UPDATE_MODE.partial ? 'partial' : 'exclude'}`)}` + return ( + <div className='system-xs-regular rounded-[10px] border border-[divider-subtle] bg-background-section p-3 text-center text-text-tertiary'> + {text} + </div> + ) +} +export default React.memo(NoPluginSelected) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker.tsx new file mode 100644 index 0000000000..77ffd66670 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker.tsx @@ -0,0 +1,69 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import NoPluginSelected from './no-plugin-selected' +import { AUTO_UPDATE_MODE } from './types' +import PluginsSelected from './plugins-selected' +import Button from '@/app/components/base/button' +import { RiAddLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { useBoolean } from 'ahooks' +import ToolPicker from './tool-picker' + +const i18nPrefix = 'plugin.autoUpdate' + +type Props = { + updateMode: AUTO_UPDATE_MODE + value: string[] // plugin ids + onChange: (value: string[]) => void +} + +const PluginsPicker: FC<Props> = ({ + updateMode, + value, + onChange, +}) => { + const { t } = useTranslation() + const hasSelected = value.length > 0 + const isExcludeMode = updateMode === AUTO_UPDATE_MODE.exclude + const handleClear = () => { + onChange([]) + } + + const [isShowToolPicker, { + set: setToolPicker, + }] = useBoolean(false) + return ( + <div className='mt-2 rounded-[10px] bg-background-section-burn p-2.5'> + {hasSelected ? ( + <div className='flex justify-between text-text-tertiary'> + <div className='system-xs-medium'>{t(`${i18nPrefix}.${isExcludeMode ? 'excludeUpdate' : 'partialUPdate'}`, { num: value.length })}</div> + <div className='system-xs-medium cursor-pointer' onClick={handleClear}>{t(`${i18nPrefix}.operation.clearAll`)}</div> + </div> + ) : ( + <NoPluginSelected updateMode={updateMode} /> + )} + + {hasSelected && ( + <PluginsSelected + className='mt-2' + plugins={value} + /> + )} + + <ToolPicker + trigger={ + <Button className='mt-2 w-[412px]' size='small' variant='secondary-accent'> + <RiAddLine className='size-3.5' /> + {t(`${i18nPrefix}.operation.select`)} + </Button> + } + value={value} + onChange={onChange} + isShow={isShowToolPicker} + onShowChange={setToolPicker} + /> + </div> + ) +} +export default React.memo(PluginsPicker) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-selected.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-selected.tsx new file mode 100644 index 0000000000..42c2a34ee8 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-selected.tsx @@ -0,0 +1,29 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import cn from '@/utils/classnames' +import { MARKETPLACE_API_PREFIX } from '@/config' +import Icon from '@/app/components/plugins/card/base/card-icon' + +const MAX_DISPLAY_COUNT = 14 +type Props = { + className?: string + plugins: string[] +} + +const PluginsSelected: FC<Props> = ({ + className, + plugins, +}) => { + const isShowAll = plugins.length < MAX_DISPLAY_COUNT + const displayPlugins = plugins.slice(0, MAX_DISPLAY_COUNT) + return ( + <div className={cn('flex items-center space-x-1', className)}> + {displayPlugins.map(plugin => ( + <Icon key={plugin} size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin}/icon`} /> + ))} + {!isShowAll && <div className='system-xs-medium text-text-tertiary'>+{plugins.length - MAX_DISPLAY_COUNT}</div>} + </div> + ) +} +export default React.memo(PluginsSelected) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/strategy-picker.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/strategy-picker.tsx new file mode 100644 index 0000000000..c8227520f3 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/strategy-picker.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiArrowDownSLine, + RiCheckLine, +} from '@remixicon/react' +import { AUTO_UPDATE_STRATEGY } from './types' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import Button from '@/app/components/base/button' +const i18nPrefix = 'plugin.autoUpdate.strategy' + +type Props = { + value: AUTO_UPDATE_STRATEGY + onChange: (value: AUTO_UPDATE_STRATEGY) => void +} +const StrategyPicker = ({ + value, + onChange, +}: Props) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const options = [ + { + value: AUTO_UPDATE_STRATEGY.disabled, + label: t(`${i18nPrefix}.disabled.name`), + description: t(`${i18nPrefix}.disabled.description`), + }, + { + value: AUTO_UPDATE_STRATEGY.fixOnly, + label: t(`${i18nPrefix}.fixOnly.name`), + description: t(`${i18nPrefix}.fixOnly.description`), + }, + { + value: AUTO_UPDATE_STRATEGY.latest, + label: t(`${i18nPrefix}.latest.name`), + description: t(`${i18nPrefix}.latest.description`), + }, + ] + const selectedOption = options.find(option => option.value === value) + + return ( + <PortalToFollowElem + open={open} + onOpenChange={setOpen} + placement='top-end' + offset={4} + > + <PortalToFollowElemTrigger onClick={(e) => { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + setOpen(v => !v) + }}> + <Button + size='small' + > + {selectedOption?.label} + <RiArrowDownSLine className='h-3.5 w-3.5' /> + </Button> + </PortalToFollowElemTrigger> + <PortalToFollowElemContent className='z-[99]'> + <div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'> + { + options.map(option => ( + <div + key={option.value} + className='flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover' + onClick={(e) => { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + onChange(option.value) + setOpen(false) + }} + > + <div className='mr-1 w-4 shrink-0'> + { + value === option.value && ( + <RiCheckLine className='h-4 w-4 text-text-accent' /> + ) + } + </div> + <div className='grow'> + <div className='system-sm-semibold mb-0.5 text-text-secondary'>{option.label}</div> + <div className='system-xs-regular text-text-tertiary'>{option.description}</div> + </div> + </div> + )) + } + </div> + </PortalToFollowElemContent> + </PortalToFollowElem> + ) +} + +export default StrategyPicker diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx new file mode 100644 index 0000000000..99a01bcd0f --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx @@ -0,0 +1,45 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import type { PluginDetail } from '@/app/components/plugins/types' +import Icon from '@/app/components/plugins/card/base/card-icon' +import { renderI18nObject } from '@/i18n' +import { useGetLanguage } from '@/context/i18n' +import { MARKETPLACE_API_PREFIX } from '@/config' +import Checkbox from '@/app/components/base/checkbox' + +type Props = { + payload: PluginDetail + isChecked?: boolean + onCheckChange: () => void +} + +const ToolItem: FC<Props> = ({ + payload, + isChecked, + onCheckChange, +}) => { + const language = useGetLanguage() + + const { plugin_id, declaration } = payload + const { label, author: org } = declaration + return ( + <div className='p-1'> + <div + className='flex w-full select-none items-center rounded-lg pr-2 hover:bg-state-base-hover' + > + <div className='flex h-8 grow items-center space-x-2 pl-3 pr-2'> + <Icon size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin_id}/icon`} /> + <div className='system-sm-medium max-w-[150px] shrink-0 truncate text-text-primary'>{renderI18nObject(label, language)}</div> + <div className='system-xs-regular max-w-[150px] shrink-0 truncate text-text-quaternary'>{org}</div> + </div> + <Checkbox + checked={isChecked} + onCheck={onCheckChange} + className='shrink-0' + /> + </div> + </div> + ) +} +export default React.memo(ToolItem) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx new file mode 100644 index 0000000000..02c1ecaf35 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx @@ -0,0 +1,167 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useMemo, useState } from 'react' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import { useInstalledPluginList } from '@/service/use-plugins' +import { PLUGIN_TYPE_SEARCH_MAP } from '../../marketplace/plugin-type-switch' +import SearchBox from '@/app/components/plugins/marketplace/search-box' +import { useTranslation } from 'react-i18next' +import cn from '@/utils/classnames' +import ToolItem from './tool-item' +import Loading from '@/app/components/base/loading' +import NoDataPlaceholder from './no-data-placeholder' +import { PluginSource } from '../../types' + +type Props = { + trigger: React.ReactNode + value: string[] + onChange: (value: string[]) => void + isShow: boolean + onShowChange: (isShow: boolean) => void + +} + +const ToolPicker: FC<Props> = ({ + trigger, + value, + onChange, + isShow, + onShowChange, +}) => { + const { t } = useTranslation() + const toggleShowPopup = useCallback(() => { + onShowChange(!isShow) + }, [onShowChange, isShow]) + + const tabs = [ + { + key: PLUGIN_TYPE_SEARCH_MAP.all, + name: t('plugin.category.all'), + }, + { + key: PLUGIN_TYPE_SEARCH_MAP.model, + name: t('plugin.category.models'), + }, + { + key: PLUGIN_TYPE_SEARCH_MAP.tool, + name: t('plugin.category.tools'), + }, + { + key: PLUGIN_TYPE_SEARCH_MAP.agent, + name: t('plugin.category.agents'), + }, + { + key: PLUGIN_TYPE_SEARCH_MAP.extension, + name: t('plugin.category.extensions'), + }, + { + key: PLUGIN_TYPE_SEARCH_MAP.bundle, + name: t('plugin.category.bundles'), + }, + ] + + const [pluginType, setPluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all) + const [query, setQuery] = useState('') + const [tags, setTags] = useState<string[]>([]) + const { data, isLoading } = useInstalledPluginList() + const filteredList = useMemo(() => { + const list = data ? data.plugins : [] + return list.filter((plugin) => { + const isFromMarketPlace = plugin.source === PluginSource.marketplace + return ( + isFromMarketPlace && (pluginType === PLUGIN_TYPE_SEARCH_MAP.all || plugin.declaration.category === pluginType) + && (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag))) + && (query === '' || plugin.plugin_id.toLowerCase().includes(query.toLowerCase())) + ) + }) + }, [data, pluginType, query, tags]) + const handleCheckChange = useCallback((pluginId: string) => { + return () => { + const newValue = value.includes(pluginId) + ? value.filter(id => id !== pluginId) + : [...value, pluginId] + onChange(newValue) + } + }, [onChange, value]) + + const listContent = ( + <div className='max-h-[396px] overflow-y-auto'> + {filteredList.map(item => ( + <ToolItem + key={item.plugin_id} + payload={item} + isChecked={value.includes(item.plugin_id)} + onCheckChange={handleCheckChange(item.plugin_id)} + /> + ))} + </div> + ) + + const loadingContent = ( + <div className='flex h-[396px] items-center justify-center'> + <Loading /> + </div> + ) + + const noData = ( + <NoDataPlaceholder className='h-[396px]' noPlugins={!query} /> + ) + + return ( + <PortalToFollowElem + placement='top' + offset={0} + open={isShow} + onOpenChange={onShowChange} + > + <PortalToFollowElemTrigger + onClick={toggleShowPopup} + > + {trigger} + </PortalToFollowElemTrigger> + <PortalToFollowElemContent className='z-[1000]'> + <div className={cn('relative min-h-20 w-[436px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-2 shadow-lg backdrop-blur-sm')}> + <div className='p-2 pb-1'> + <SearchBox + search={query} + onSearchChange={setQuery} + tags={tags} + onTagsChange={setTags} + size='small' + placeholder={t('plugin.searchTools')!} + inputClassName='w-full' + /> + </div> + <div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'> + <div className='flex h-8 items-center space-x-1'> + { + tabs.map(tab => ( + <div + className={cn( + 'flex h-6 cursor-pointer items-center rounded-md px-2 hover:bg-state-base-hover', + 'text-xs font-medium text-text-secondary', + pluginType === tab.key && 'bg-state-base-hover-alt', + )} + key={tab.key} + onClick={() => setPluginType(tab.key)} + > + {tab.name} + </div> + )) + } + </div> + </div> + {!isLoading && filteredList.length > 0 && listContent} + {!isLoading && filteredList.length === 0 && noData} + {isLoading && loadingContent} + </div> + </PortalToFollowElemContent> + </PortalToFollowElem> + ) +} + +export default React.memo(ToolPicker) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts b/web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts new file mode 100644 index 0000000000..b734150b49 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts @@ -0,0 +1,19 @@ +export enum AUTO_UPDATE_STRATEGY { + fixOnly = 'fix_only', + disabled = 'disabled', + latest = 'latest', +} + +export enum AUTO_UPDATE_MODE { + partial = 'partial', + exclude = 'exclude', + update_all = 'all', +} + +export type AutoUpdateConfig = { + strategy_setting: AUTO_UPDATE_STRATEGY + upgrade_time_of_day: number + upgrade_mode: AUTO_UPDATE_MODE + exclude_plugins: string[] + include_plugins: string[] +} diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/utils.spec.ts b/web/app/components/plugins/reference-setting-modal/auto-update-setting/utils.spec.ts new file mode 100644 index 0000000000..f813338c98 --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/utils.spec.ts @@ -0,0 +1,14 @@ +import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds } from './utils' + +describe('convertLocalSecondsToUTCDaySeconds', () => { + it('should convert local seconds to UTC day seconds correctly', () => { + const localTimezone = 'Asia/Shanghai' + const utcSeconds = convertLocalSecondsToUTCDaySeconds(0, localTimezone) + expect(utcSeconds).toBe((24 - 8) * 3600) + }) + + it('should convert local seconds to UTC day seconds for a specific time', () => { + const localTimezone = 'Asia/Shanghai' + expect(convertUTCDaySecondsToLocalSeconds(convertLocalSecondsToUTCDaySeconds(0, localTimezone), localTimezone)).toBe(0) + }) +}) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/utils.ts b/web/app/components/plugins/reference-setting-modal/auto-update-setting/utils.ts new file mode 100644 index 0000000000..23c067285f --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/utils.ts @@ -0,0 +1,37 @@ +import type { Dayjs } from 'dayjs' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import timezone from 'dayjs/plugin/timezone' + +dayjs.extend(utc) +dayjs.extend(timezone) + +export const timeOfDayToDayjs = (timeOfDay: number): Dayjs => { + const hours = Math.floor(timeOfDay / 3600) + const minutes = (timeOfDay - hours * 3600) / 60 + const res = dayjs().startOf('day').hour(hours).minute(minutes) + return res +} + +export const convertLocalSecondsToUTCDaySeconds = (secondsInDay: number, localTimezone: string): number => { + const localDayStart = dayjs().tz(localTimezone).startOf('day') + const localTargetTime = localDayStart.add(secondsInDay, 'second') + const utcTargetTime = localTargetTime.utc() + const utcDayStart = utcTargetTime.startOf('day') + const secondsFromUTCMidnight = utcTargetTime.diff(utcDayStart, 'second') + return secondsFromUTCMidnight +} + +export const dayjsToTimeOfDay = (date?: Dayjs): number => { + if (!date) return 0 + return date.hour() * 3600 + date.minute() * 60 +} + +export const convertUTCDaySecondsToLocalSeconds = (utcDaySeconds: number, localTimezone: string): number => { + const utcDayStart = dayjs().utc().startOf('day') + const utcTargetTime = utcDayStart.add(utcDaySeconds, 'second') + const localTargetTime = utcTargetTime.tz(localTimezone) + const localDayStart = localTargetTime.startOf('day') + const secondsInLocalDay = localTargetTime.diff(localDayStart, 'second') + return secondsInLocalDay +} diff --git a/web/app/components/plugins/reference-setting-modal/label.tsx b/web/app/components/plugins/reference-setting-modal/label.tsx new file mode 100644 index 0000000000..6444bf801d --- /dev/null +++ b/web/app/components/plugins/reference-setting-modal/label.tsx @@ -0,0 +1,28 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import cn from '@/utils/classnames' + +type Props = { + label: string + description?: string +} + +const Label: FC<Props> = ({ + label, + description, +}) => { + return ( + <div> + <div className={cn('flex h-6 items-center', description && 'h-4')}> + <span className='system-sm-semibold text-text-secondary'>{label}</span> + </div> + {description && ( + <div className='body-xs-regular mt-1 text-text-tertiary'> + {description} + </div> + )} + </div> + ) +} +export default React.memo(Label) diff --git a/web/app/components/plugins/permission-setting-modal/modal.tsx b/web/app/components/plugins/reference-setting-modal/modal.tsx similarity index 74% rename from web/app/components/plugins/permission-setting-modal/modal.tsx rename to web/app/components/plugins/reference-setting-modal/modal.tsx index 6fd4d8c2dc..9fefbdbb55 100644 --- a/web/app/components/plugins/permission-setting-modal/modal.tsx +++ b/web/app/components/plugins/reference-setting-modal/modal.tsx @@ -5,14 +5,18 @@ import { useTranslation } from 'react-i18next' import Modal from '@/app/components/base/modal' import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' import Button from '@/app/components/base/button' -import type { Permissions } from '@/app/components/plugins/types' +import type { Permissions, ReferenceSetting } from '@/app/components/plugins/types' import { PermissionType } from '@/app/components/plugins/types' +import type { AutoUpdateConfig } from './auto-update-setting/types' +import AutoUpdateSetting from './auto-update-setting' +import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config' +import Label from './label' const i18nPrefix = 'plugin.privilege' type Props = { - payload: Permissions + payload: ReferenceSetting onHide: () => void - onSave: (payload: Permissions) => void + onSave: (payload: ReferenceSetting) => void } const PluginSettingModal: FC<Props> = ({ @@ -21,7 +25,9 @@ const PluginSettingModal: FC<Props> = ({ onSave, }) => { const { t } = useTranslation() - const [tempPrivilege, setTempPrivilege] = useState<Permissions>(payload) + const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {} + const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege) + const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue) const handlePrivilegeChange = useCallback((key: string) => { return (value: PermissionType) => { setTempPrivilege({ @@ -32,18 +38,21 @@ const PluginSettingModal: FC<Props> = ({ }, [tempPrivilege]) const handleSave = useCallback(async () => { - await onSave(tempPrivilege) + await onSave({ + permission: tempPrivilege, + auto_upgrade: tempAutoUpdateConfig, + }) onHide() - }, [onHide, onSave, tempPrivilege]) + }, [onHide, onSave, tempAutoUpdateConfig, tempPrivilege]) return ( <Modal isShow onClose={onHide} closable - className='w-[420px] !p-0' + className='w-[480px] !p-0' > - <div className='shadows-shadow-xl flex w-[420px] flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg'> + <div className='shadows-shadow-xl flex w-[480px] flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg'> <div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'> <span className='title-2xl-semi-bold self-stretch text-text-primary'>{t(`${i18nPrefix}.title`)}</span> </div> @@ -53,9 +62,7 @@ const PluginSettingModal: FC<Props> = ({ { title: t(`${i18nPrefix}.whoCanDebug`), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne }, ].map(({ title, key, value }) => ( <div key={key} className='flex flex-col items-start gap-1 self-stretch'> - <div className='flex h-6 items-center gap-0.5'> - <span className='system-sm-semibold text-text-secondary'>{title}</span> - </div> + <Label label={title} /> <div className='flex w-full items-start justify-between gap-2'> {[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => ( <OptionCard @@ -70,6 +77,8 @@ const PluginSettingModal: FC<Props> = ({ </div> ))} </div> + + <AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} /> <div className='flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5'> <Button className='min-w-[72px]' diff --git a/web/app/components/plugins/permission-setting-modal/style.module.css b/web/app/components/plugins/reference-setting-modal/style.module.css similarity index 100% rename from web/app/components/plugins/permission-setting-modal/style.module.css rename to web/app/components/plugins/reference-setting-modal/style.module.css diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index cd4d5e9ece..162bf236d8 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -2,6 +2,7 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p import type { ToolCredential } from '@/app/components/tools/types' import type { Locale } from '@/i18n' import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types' +import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types' export enum PluginType { tool = 'tool', model = 'model', @@ -118,6 +119,9 @@ export type PluginDetail = { latest_unique_identifier: string source: PluginSource meta?: MetaData + status: 'active' | 'deleted' + deprecated_reason: string + alternative_plugin_id: string } export type PluginInfoFromMarketPlace = { @@ -167,6 +171,11 @@ export type Permissions = { debug_permission: PermissionType } +export type ReferenceSetting = { + permission: Permissions + auto_upgrade: AutoUpdateConfig +} + export type UpdateFromMarketPlacePayload = { category: PluginType originalPackageInfo: { @@ -343,6 +352,9 @@ export type InstalledLatestVersionResponse = { [plugin_id: string]: { unique_identifier: string version: string + status: 'active' | 'deleted' + deprecated_reason: string + alternative_plugin_id: string } | null } } diff --git a/web/app/components/plugins/update-plugin/downgrade-warning.tsx b/web/app/components/plugins/update-plugin/downgrade-warning.tsx new file mode 100644 index 0000000000..a5f6e40e41 --- /dev/null +++ b/web/app/components/plugins/update-plugin/downgrade-warning.tsx @@ -0,0 +1,35 @@ +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' + +const i18nPrefix = 'plugin.autoUpdate.pluginDowngradeWarning' + +type Props = { + onCancel: () => void + onJustDowngrade: () => void + onExcludeAndDowngrade: () => void +} +const DowngradeWarningModal = ({ + onCancel, + onJustDowngrade, + onExcludeAndDowngrade, +}: Props) => { + const { t } = useTranslation() + + return ( + <> + <div className='flex flex-col items-start gap-2 self-stretch'> + <div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.title`)}</div> + <div className='system-md-regular text-text-secondary'> + {t(`${i18nPrefix}.description`)} + </div> + </div> + <div className='mt-9 flex items-start justify-end space-x-2 self-stretch'> + <Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button> + <Button variant='secondary' destructive onClick={onJustDowngrade}>{t(`${i18nPrefix}.downgrade`)}</Button> + <Button variant='primary' onClick={onExcludeAndDowngrade}>{t(`${i18nPrefix}.exclude`)}</Button> + </div> + </> + ) +} + +export default DowngradeWarningModal diff --git a/web/app/components/plugins/update-plugin/from-market-place.tsx b/web/app/components/plugins/update-plugin/from-market-place.tsx index 98994d9b9c..70bc7399f5 100644 --- a/web/app/components/plugins/update-plugin/from-market-place.tsx +++ b/web/app/components/plugins/update-plugin/from-market-place.tsx @@ -13,13 +13,18 @@ import { updateFromMarketPlace } from '@/service/plugins' import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status' import { usePluginTaskList } from '@/service/use-plugins' import Toast from '../../base/toast' +import DowngradeWarningModal from './downgrade-warning' +import { useInvalidateReferenceSettings, useRemoveAutoUpgrade } from '@/service/use-plugins' +import cn from '@/utils/classnames' const i18nPrefix = 'plugin.upgrade' type Props = { payload: UpdateFromMarketPlacePayload + pluginId: string onSave: () => void onCancel: () => void + isShowDowngradeWarningModal?: boolean } enum UploadStep { @@ -30,8 +35,10 @@ enum UploadStep { const UpdatePluginModal: FC<Props> = ({ payload, + pluginId, onSave, onCancel, + isShowDowngradeWarningModal, }) => { const { originalPackageInfo, @@ -103,51 +110,74 @@ const UpdatePluginModal: FC<Props> = ({ onSave() }, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id]) + const { mutateAsync } = useRemoveAutoUpgrade() + const invalidateReferenceSettings = useInvalidateReferenceSettings() + const handleExcludeAndDownload = async () => { + await mutateAsync({ + plugin_id: pluginId, + }) + invalidateReferenceSettings() + handleConfirm() + } + const doShowDowngradeWarningModal = isShowDowngradeWarningModal && uploadStep === UploadStep.notStarted + return ( <Modal isShow={true} onClose={onCancel} - className='min-w-[560px]' + className={cn('min-w-[560px]', doShowDowngradeWarningModal && 'min-w-[640px]')} closable - title={t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)} + title={!doShowDowngradeWarningModal && t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)} > - <div className='system-md-regular mb-2 mt-3 text-text-secondary'> - {t(`${i18nPrefix}.description`)} - </div> - <div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> - <Card - installed={uploadStep === UploadStep.installed} - payload={pluginManifestToCardPluginProps({ - ...originalPackageInfo.payload, - icon: icon!, - })} - className='w-full' - titleLeft={ - <> - <Badge className='mx-1' size="s" state={BadgeState.Warning}> - {`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`} - </Badge> - </> - } + {doShowDowngradeWarningModal && ( + <DowngradeWarningModal + onCancel={onCancel} + onJustDowngrade={handleConfirm} + onExcludeAndDowngrade={handleExcludeAndDownload} /> - </div> - <div className='flex items-center justify-end gap-2 self-stretch pt-5'> - {uploadStep === UploadStep.notStarted && ( + )} + {!doShowDowngradeWarningModal && ( + <> + <div className='system-md-regular mb-2 mt-3 text-text-secondary'> + {t(`${i18nPrefix}.description`)} + </div> + <div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> + <Card + installed={uploadStep === UploadStep.installed} + payload={pluginManifestToCardPluginProps({ + ...originalPackageInfo.payload, + icon: icon!, + })} + className='w-full' + titleLeft={ + <> + <Badge className='mx-1' size="s" state={BadgeState.Warning}> + {`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`} + </Badge> + </> + } + /> + </div> + <div className='flex items-center justify-end gap-2 self-stretch pt-5'> + {uploadStep === UploadStep.notStarted && ( + <Button + onClick={handleCancel} + > + {t('common.operation.cancel')} + </Button> + )} <Button - onClick={handleCancel} + variant='primary' + loading={uploadStep === UploadStep.upgrading} + onClick={handleConfirm} + disabled={uploadStep === UploadStep.upgrading} > - {t('common.operation.cancel')} + {configBtnText} </Button> - )} - <Button - variant='primary' - loading={uploadStep === UploadStep.upgrading} - onClick={handleConfirm} - disabled={uploadStep === UploadStep.upgrading} - > - {configBtnText} - </Button> - </div> + </div> + </> + )} + </Modal> ) } diff --git a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx index 424f76d790..36a4faace1 100644 --- a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx +++ b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx @@ -15,6 +15,7 @@ import type { import { useVersionListOfPlugin } from '@/service/use-plugins' import useTimestamp from '@/hooks/use-timestamp' import cn from '@/utils/classnames' +import { lt } from 'semver' type Props = { disabled?: boolean @@ -28,9 +29,11 @@ type Props = { onSelect: ({ version, unique_identifier, + isDowngrade, }: { version: string unique_identifier: string + isDowngrade: boolean }) => void } @@ -59,13 +62,14 @@ const PluginVersionPicker: FC<Props> = ({ const { data: res } = useVersionListOfPlugin(pluginID) - const handleSelect = useCallback(({ version, unique_identifier }: { + const handleSelect = useCallback(({ version, unique_identifier, isDowngrade }: { version: string unique_identifier: string + isDowngrade: boolean }) => { if (currentVersion === version) return - onSelect({ version, unique_identifier }) + onSelect({ version, unique_identifier, isDowngrade }) onShowChange(false) }, [currentVersion, onSelect, onShowChange]) @@ -99,6 +103,7 @@ const PluginVersionPicker: FC<Props> = ({ onClick={() => handleSelect({ version: version.version, unique_identifier: version.unique_identifier, + isDowngrade: lt(version.version, currentVersion), })} > <div className='flex grow items-center'> diff --git a/web/app/components/sentry-initor.tsx b/web/app/components/sentry-initializer.tsx similarity index 85% rename from web/app/components/sentry-initor.tsx rename to web/app/components/sentry-initializer.tsx index 457a1cf7c7..10c056f21b 100644 --- a/web/app/components/sentry-initor.tsx +++ b/web/app/components/sentry-initializer.tsx @@ -5,9 +5,9 @@ import * as Sentry from '@sentry/react' const isDevelopment = process.env.NODE_ENV === 'development' -const SentryInit = ({ +const SentryInitializer = ({ children, -}: { children: React.ReactNode }) => { +}: { children: React.ReactElement }) => { useEffect(() => { const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn') if (!isDevelopment && SENTRY_DSN) { @@ -26,4 +26,4 @@ const SentryInit = ({ return children } -export default SentryInit +export default SentryInitializer diff --git a/web/app/components/swr-initor.tsx b/web/app/components/swr-initializer.tsx similarity index 95% rename from web/app/components/swr-initor.tsx rename to web/app/components/swr-initializer.tsx index 8f9c5b4e05..3592a0e017 100644 --- a/web/app/components/swr-initor.tsx +++ b/web/app/components/swr-initializer.tsx @@ -10,12 +10,12 @@ import { EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, } from '@/app/education-apply/constants' -type SwrInitorProps = { +type SwrInitializerProps = { children: ReactNode } -const SwrInitor = ({ +const SwrInitializer = ({ children, -}: SwrInitorProps) => { +}: SwrInitializerProps) => { const router = useRouter() const searchParams = useSearchParams() const consoleToken = decodeURIComponent(searchParams.get('access_token') || '') @@ -86,4 +86,4 @@ const SwrInitor = ({ : null } -export default SwrInitor +export default SwrInitializer diff --git a/web/app/components/tools/mcp/hooks.ts b/web/app/components/tools/mcp/hooks.ts index b2b521557f..562be6f079 100644 --- a/web/app/components/tools/mcp/hooks.ts +++ b/web/app/components/tools/mcp/hooks.ts @@ -1,7 +1,10 @@ import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' import { useCallback } from 'react' import { useI18N } from '@/context/i18n' +dayjs.extend(relativeTime) + export const useFormatTimeFromNow = () => { const { locale } = useI18N() const formatTimeFromNow = useCallback((time: number) => { diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx index f90f30e7ce..ecc67885d1 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx @@ -41,7 +41,7 @@ const Field: FC<Props> = ({ <Tooltip popupContent={t('app.structOutput.moreFillTip')} disabled={depth !== MAX_DEPTH + 1}> <div className={cn('flex items-center justify-between rounded-md pr-2', !readonly && 'hover:bg-state-base-hover', depth !== MAX_DEPTH + 1 && 'cursor-pointer')} - onClick={() => !readonly && onSelect?.([...valueSelector, name])} + onMouseDown={() => !readonly && onSelect?.([...valueSelector, name])} > <div className='flex grow items-stretch'> <TreeIndentLine depth={depth} /> diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.spec.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.spec.tsx new file mode 100644 index 0000000000..5c6ffb7a52 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.spec.tsx @@ -0,0 +1,178 @@ +/** + * Workflow Panel Width Persistence Tests + * Tests for GitHub issue #22745: Panel width persistence bug fix + */ + +import '@testing-library/jest-dom' + +type PanelWidthSource = 'user' | 'system' + +// Mock localStorage for testing +const createMockLocalStorage = () => { + const storage: Record<string, string> = {} + return { + getItem: jest.fn((key: string) => storage[key] || null), + setItem: jest.fn((key: string, value: string) => { + storage[key] = value + }), + removeItem: jest.fn((key: string) => { + delete storage[key] + }), + clear: jest.fn(() => { + Object.keys(storage).forEach(key => delete storage[key]) + }), + get storage() { return { ...storage } }, + } +} + +// Core panel width logic extracted from the component +const createPanelWidthManager = (storageKey: string) => { + return { + updateWidth: (width: number, source: PanelWidthSource = 'user') => { + const newValue = Math.max(400, Math.min(width, 800)) + if (source === 'user') + localStorage.setItem(storageKey, `${newValue}`) + + return newValue + }, + getStoredWidth: () => { + const stored = localStorage.getItem(storageKey) + return stored ? Number.parseFloat(stored) : 400 + }, + } +} + +describe('Workflow Panel Width Persistence', () => { + let mockLocalStorage: ReturnType<typeof createMockLocalStorage> + + beforeEach(() => { + mockLocalStorage = createMockLocalStorage() + Object.defineProperty(globalThis, 'localStorage', { + value: mockLocalStorage, + writable: true, + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('Node Panel Width Management', () => { + const storageKey = 'workflow-node-panel-width' + + it('should save user resize to localStorage', () => { + const manager = createPanelWidthManager(storageKey) + + const result = manager.updateWidth(500, 'user') + + expect(result).toBe(500) + expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, '500') + }) + + it('should not save system compression to localStorage', () => { + const manager = createPanelWidthManager(storageKey) + + const result = manager.updateWidth(200, 'system') + + expect(result).toBe(400) // Respects minimum width + expect(localStorage.setItem).not.toHaveBeenCalled() + }) + + it('should enforce minimum width of 400px', () => { + const manager = createPanelWidthManager(storageKey) + + // User tries to set below minimum + const userResult = manager.updateWidth(300, 'user') + expect(userResult).toBe(400) + expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, '400') + + // System compression below minimum + const systemResult = manager.updateWidth(150, 'system') + expect(systemResult).toBe(400) + expect(localStorage.setItem).toHaveBeenCalledTimes(1) // Only user call + }) + + it('should preserve user preferences during system compression', () => { + localStorage.setItem(storageKey, '600') + const manager = createPanelWidthManager(storageKey) + + // System compresses panel + manager.updateWidth(200, 'system') + + // User preference should remain unchanged + expect(localStorage.getItem(storageKey)).toBe('600') + }) + }) + + describe('Bug Scenario Reproduction', () => { + it('should reproduce original bug behavior (for comparison)', () => { + const storageKey = 'workflow-node-panel-width' + + // Original buggy behavior - always saves regardless of source + const buggyUpdate = (width: number) => { + localStorage.setItem(storageKey, `${width}`) + return Math.max(400, width) + } + + localStorage.setItem(storageKey, '500') // User preference + buggyUpdate(200) // System compression pollutes localStorage + + expect(localStorage.getItem(storageKey)).toBe('200') // Bug: corrupted state + }) + + it('should verify fix prevents localStorage pollution', () => { + const storageKey = 'workflow-node-panel-width' + const manager = createPanelWidthManager(storageKey) + + localStorage.setItem(storageKey, '500') // User preference + manager.updateWidth(200, 'system') // System compression + + expect(localStorage.getItem(storageKey)).toBe('500') // Fix: preserved state + }) + }) + + describe('Edge Cases', () => { + it('should handle multiple rapid operations correctly', () => { + const manager = createPanelWidthManager('workflow-node-panel-width') + + // Rapid system adjustments + manager.updateWidth(300, 'system') + manager.updateWidth(250, 'system') + manager.updateWidth(180, 'system') + + // Single user adjustment + manager.updateWidth(550, 'user') + + expect(localStorage.setItem).toHaveBeenCalledTimes(1) + expect(localStorage.setItem).toHaveBeenCalledWith('workflow-node-panel-width', '550') + }) + + it('should handle corrupted localStorage gracefully', () => { + localStorage.setItem('workflow-node-panel-width', '150') // Below minimum + const manager = createPanelWidthManager('workflow-node-panel-width') + + const storedWidth = manager.getStoredWidth() + expect(storedWidth).toBe(150) // Returns raw value + + // User can correct the preference + const correctedWidth = manager.updateWidth(500, 'user') + expect(correctedWidth).toBe(500) + expect(localStorage.getItem('workflow-node-panel-width')).toBe('500') + }) + }) + + describe('TypeScript Type Safety', () => { + it('should enforce source parameter type', () => { + const manager = createPanelWidthManager('workflow-node-panel-width') + + // Valid source values + manager.updateWidth(500, 'user') + manager.updateWidth(500, 'system') + + // Default to 'user' + manager.updateWidth(500) + + expect(localStorage.setItem).toHaveBeenCalledTimes(2) // user + default + }) + }) +}) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index d50b99e213..93fab83172 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -99,15 +99,18 @@ const BasePanel: FC<BasePanelProps> = ({ return Math.max(available, 400) }, [workflowCanvasWidth, otherPanelWidth]) - const updateNodePanelWidth = useCallback((width: number) => { + const updateNodePanelWidth = useCallback((width: number, source: 'user' | 'system' = 'user') => { // Ensure the width is within the min and max range const newValue = Math.max(400, Math.min(width, maxNodePanelWidth)) - localStorage.setItem('workflow-node-panel-width', `${newValue}`) + + if (source === 'user') + localStorage.setItem('workflow-node-panel-width', `${newValue}`) + setNodePanelWidth(newValue) }, [maxNodePanelWidth, setNodePanelWidth]) const handleResize = useCallback((width: number) => { - updateNodePanelWidth(width) + updateNodePanelWidth(width, 'user') }, [updateNodePanelWidth]) const { @@ -121,7 +124,10 @@ const BasePanel: FC<BasePanelProps> = ({ onResize: debounce(handleResize), }) - const debounceUpdate = debounce(updateNodePanelWidth) + const debounceUpdate = debounce((width: number) => { + updateNodePanelWidth(width, 'system') + }) + useEffect(() => { if (!workflowCanvasWidth) return @@ -132,7 +138,7 @@ const BasePanel: FC<BasePanelProps> = ({ const target = Math.max(workflowCanvasWidth - otherPanelWidth - reservedCanvasWidth, 400) debounceUpdate(target) } - }, [nodePanelWidth, otherPanelWidth, workflowCanvasWidth, updateNodePanelWidth]) + }, [nodePanelWidth, otherPanelWidth, workflowCanvasWidth, debounceUpdate]) const { handleNodeSelect } = useNodesInteractions() const { nodesReadOnly } = useNodesReadOnly() diff --git a/web/app/components/workflow/nodes/agent/default.ts b/web/app/components/workflow/nodes/agent/default.ts index 4f68cfe87c..51955dc6c2 100644 --- a/web/app/components/workflow/nodes/agent/default.ts +++ b/web/app/components/workflow/nodes/agent/default.ts @@ -7,7 +7,7 @@ import { renderI18nObject } from '@/i18n' const nodeDefault: NodeDefault<AgentNodeType> = { defaultValue: { - version: '2', + tool_node_version: '2', }, getAvailablePrevNodes(isChatMode) { return isChatMode @@ -62,27 +62,29 @@ const nodeDefault: NodeDefault<AgentNodeType> = { const userSettings = toolValue.settings const reasoningConfig = toolValue.parameters const version = payload.version + const toolNodeVersion = payload.tool_node_version + const mergeVersion = version || toolNodeVersion schemas.forEach((schema: any) => { if (schema?.required) { - if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) { + if (schema.form === 'form' && !mergeVersion && !userSettings[schema.name]?.value) { return { isValid: false, errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), } } - if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) { + if (schema.form === 'form' && mergeVersion && !userSettings[schema.name]?.value.value) { return { isValid: false, errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), } } - if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) { + if (schema.form === 'llm' && !mergeVersion && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) { return { isValid: false, errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), } } - if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) { + if (schema.form === 'llm' && mergeVersion && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) { return { isValid: false, errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), diff --git a/web/app/components/workflow/nodes/agent/types.ts b/web/app/components/workflow/nodes/agent/types.ts index 5a13a4a4f3..f163b3572a 100644 --- a/web/app/components/workflow/nodes/agent/types.ts +++ b/web/app/components/workflow/nodes/agent/types.ts @@ -12,6 +12,7 @@ export type AgentNodeType = CommonNodeType & { plugin_unique_identifier?: string memory?: Memory version?: string + tool_node_version?: string } export enum AgentFeature { diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index a8b80c0348..dd9236f24f 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -129,7 +129,7 @@ const useConfig = (id: string, payload: AgentNodeType) => { } const formattingLegacyData = () => { - if (inputs.version) + if (inputs.version || inputs.tool_node_version) return inputs const newData = produce(inputs, (draft) => { const schemas = currentStrategy?.parameters || [] @@ -140,7 +140,7 @@ const useConfig = (id: string, payload: AgentNodeType) => { if (targetSchema?.type === FormTypeEnum.multiToolSelector) draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool)) }) - draft.version = '2' + draft.tool_node_version = '2' }) return newData } diff --git a/web/app/components/workflow/nodes/tool/default.ts b/web/app/components/workflow/nodes/tool/default.ts index 1fdb9eed2d..1d4056be6d 100644 --- a/web/app/components/workflow/nodes/tool/default.ts +++ b/web/app/components/workflow/nodes/tool/default.ts @@ -10,7 +10,7 @@ const nodeDefault: NodeDefault<ToolNodeType> = { defaultValue: { tool_parameters: {}, tool_configurations: {}, - version: '2', + tool_node_version: '2', }, getAvailablePrevNodes(isChatMode: boolean) { const nodes = isChatMode diff --git a/web/app/components/workflow/nodes/tool/types.ts b/web/app/components/workflow/nodes/tool/types.ts index 4584645a1e..6294b9b689 100644 --- a/web/app/components/workflow/nodes/tool/types.ts +++ b/web/app/components/workflow/nodes/tool/types.ts @@ -23,4 +23,5 @@ export type ToolNodeType = CommonNodeType & { output_schema: Record<string, any> paramSchemas?: Record<string, any>[] version?: string + tool_node_version?: string } diff --git a/web/app/components/workflow/panel/debug-and-preview/index.spec.tsx b/web/app/components/workflow/panel/debug-and-preview/index.spec.tsx new file mode 100644 index 0000000000..1ac70d1ab3 --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/index.spec.tsx @@ -0,0 +1,145 @@ +/** + * Debug and Preview Panel Width Persistence Tests + * Tests for GitHub issue #22745: Panel width persistence bug fix + */ + +import '@testing-library/jest-dom' + +type PanelWidthSource = 'user' | 'system' + +// Mock localStorage for testing +const createMockLocalStorage = () => { + const storage: Record<string, string> = {} + return { + getItem: jest.fn((key: string) => storage[key] || null), + setItem: jest.fn((key: string, value: string) => { + storage[key] = value + }), + removeItem: jest.fn((key: string) => { + delete storage[key] + }), + clear: jest.fn(() => { + Object.keys(storage).forEach(key => delete storage[key]) + }), + get storage() { return { ...storage } }, + } +} + +// Preview panel width logic +const createPreviewPanelManager = () => { + const storageKey = 'debug-and-preview-panel-width' + + return { + updateWidth: (width: number, source: PanelWidthSource = 'user') => { + const newValue = Math.max(400, Math.min(width, 800)) + if (source === 'user') + localStorage.setItem(storageKey, `${newValue}`) + + return newValue + }, + getStoredWidth: () => { + const stored = localStorage.getItem(storageKey) + return stored ? Number.parseFloat(stored) : 400 + }, + } +} + +describe('Debug and Preview Panel Width Persistence', () => { + let mockLocalStorage: ReturnType<typeof createMockLocalStorage> + + beforeEach(() => { + mockLocalStorage = createMockLocalStorage() + Object.defineProperty(globalThis, 'localStorage', { + value: mockLocalStorage, + writable: true, + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('Preview Panel Width Management', () => { + it('should save user resize to localStorage', () => { + const manager = createPreviewPanelManager() + + const result = manager.updateWidth(450, 'user') + + expect(result).toBe(450) + expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '450') + }) + + it('should not save system compression to localStorage', () => { + const manager = createPreviewPanelManager() + + const result = manager.updateWidth(300, 'system') + + expect(result).toBe(400) // Respects minimum width + expect(localStorage.setItem).not.toHaveBeenCalled() + }) + + it('should behave identically to Node Panel', () => { + const manager = createPreviewPanelManager() + + // Both user and system operations should behave consistently + manager.updateWidth(500, 'user') + expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '500') + + manager.updateWidth(200, 'system') + expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500') + }) + }) + + describe('Dual Panel Scenario', () => { + it('should maintain independence from Node Panel', () => { + localStorage.setItem('workflow-node-panel-width', '600') + localStorage.setItem('debug-and-preview-panel-width', '450') + + const manager = createPreviewPanelManager() + + // System compresses preview panel + manager.updateWidth(200, 'system') + + // Only preview panel storage key should be unaffected + expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('450') + expect(localStorage.getItem('workflow-node-panel-width')).toBe('600') + }) + + it('should handle F12 scenario consistently', () => { + const manager = createPreviewPanelManager() + + // User sets preference + manager.updateWidth(500, 'user') + expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500') + + // F12 opens causing viewport compression + manager.updateWidth(180, 'system') + + // User preference preserved + expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500') + }) + }) + + describe('Consistency with Node Panel', () => { + it('should enforce same minimum width rules', () => { + const manager = createPreviewPanelManager() + + // Same 400px minimum as Node Panel + const result = manager.updateWidth(300, 'user') + expect(result).toBe(400) + expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '400') + }) + + it('should use same source parameter pattern', () => { + const manager = createPreviewPanelManager() + + // Default to 'user' when source not specified + manager.updateWidth(500) + expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '500') + + // Explicit 'system' source + manager.updateWidth(300, 'system') + expect(localStorage.setItem).toHaveBeenCalledTimes(1) // Only user call + }) + }) +}) diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index ff09f48625..baf4c21dcd 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -53,8 +53,9 @@ const DebugAndPreview = () => { const nodePanelWidth = useStore(s => s.nodePanelWidth) const panelWidth = useStore(s => s.previewPanelWidth) const setPanelWidth = useStore(s => s.setPreviewPanelWidth) - const handleResize = useCallback((width: number) => { - localStorage.setItem('debug-and-preview-panel-width', `${width}`) + const handleResize = useCallback((width: number, source: 'user' | 'system' = 'user') => { + if (source === 'user') + localStorage.setItem('debug-and-preview-panel-width', `${width}`) setPanelWidth(width) }, [setPanelWidth]) const maxPanelWidth = useMemo(() => { @@ -74,7 +75,9 @@ const DebugAndPreview = () => { triggerDirection: 'left', minWidth: 400, maxWidth: maxPanelWidth, - onResize: debounce(handleResize), + onResize: debounce((width: number) => { + handleResize(width, 'user') + }), }) return ( diff --git a/web/app/components/workflow/utils/workflow-init.ts b/web/app/components/workflow/utils/workflow-init.ts index dc22d61ca5..92233f8d08 100644 --- a/web/app/components/workflow/utils/workflow-init.ts +++ b/web/app/components/workflow/utils/workflow-init.ts @@ -286,8 +286,8 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { } } - if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version) { - (node as Node<ToolNodeType>).data.version = '2' + if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version && !(node as Node<ToolNodeType>).data.tool_node_version) { + (node as Node<ToolNodeType>).data.tool_node_version = '2' const toolConfigurations = (node as Node<ToolNodeType>).data.tool_configurations if (toolConfigurations && Object.keys(toolConfigurations).length > 0) { diff --git a/web/app/dev-only/i18n-checker/page.tsx b/web/app/dev-only/i18n-checker/page.tsx index 5ed0c86b82..d821979bb9 100644 --- a/web/app/dev-only/i18n-checker/page.tsx +++ b/web/app/dev-only/i18n-checker/page.tsx @@ -1,13 +1,19 @@ 'use client' -import { resources } from '@/i18n/i18next-config' -import { useEffect, useState } from 'react' +import { loadLangResources } from '@/i18n/i18next-config' +import { useCallback, useEffect, useState } from 'react' import cn from '@/utils/classnames' +import { LanguagesSupported } from '@/i18n/language' export default function I18nTest() { const [langs, setLangs] = useState<Lang[]>([]) + const getLangs = useCallback(async () => { + const langs = await genLangs() + setLangs(langs) + }, []) + useEffect(() => { - setLangs(genLangs()) + getLangs() }, []) return ( @@ -107,10 +113,15 @@ export default function I18nTest() { ) } -function genLangs() { +async function genLangs() { const langs_: Lang[] = [] let en!: Lang + const resources: Record<string, any> = {} + // Initialize empty resource object + for (const lang of LanguagesSupported) + resources[lang] = await loadLangResources(lang) + for (const [key, value] of Object.entries(resources)) { const keys = getNestedKeys(value.translation) const lang: Lang = { diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 525445db30..f086499ca4 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,10 +1,10 @@ import RoutePrefixHandle from './routePrefixHandle' import type { Viewport } from 'next' import I18nServer from './components/i18n-server' -import BrowserInitor from './components/browser-initor' -import SentryInitor from './components/sentry-initor' +import BrowserInitializer from './components/browser-initializer' +import SentryInitializer from './components/sentry-initializer' import { getLocaleOnServer } from '@/i18n/server' -import { TanstackQueryIniter } from '@/context/query-client' +import { TanstackQueryInitializer } from '@/context/query-client' import { ThemeProvider } from 'next-themes' import './styles/globals.css' import './styles/markdown.scss' @@ -62,9 +62,9 @@ const LocaleLayout = async ({ className="color-scheme h-full select-auto" {...datasetMap} > - <BrowserInitor> - <SentryInitor> - <TanstackQueryIniter> + <BrowserInitializer> + <SentryInitializer> + <TanstackQueryInitializer> <ThemeProvider attribute='data-theme' defaultTheme='system' @@ -77,9 +77,9 @@ const LocaleLayout = async ({ </GlobalPublicStoreProvider> </I18nServer> </ThemeProvider> - </TanstackQueryIniter> - </SentryInitor> - </BrowserInitor> + </TanstackQueryInitializer> + </SentryInitializer> + </BrowserInitializer> <RoutePrefixHandle /> </body> </html> diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index 1ff1c7d671..ea35900968 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -57,7 +57,7 @@ export default function InviteSettingsPage() { if (res.result === 'success') { localStorage.setItem('console_token', res.data.access_token) localStorage.setItem('refresh_token', res.data.refresh_token) - setLocaleOnClient(language, false) + await setLocaleOnClient(language, false) router.replace('/apps') } } diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 9b95b0f1eb..f941cb43b4 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -1,20 +1,15 @@ 'use client' -import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import useSWR from 'swr' import { createContext, useContext, useContextSelector } from 'use-context-selector' import type { FC, ReactNode } from 'react' -import { fetchAppList } from '@/service/apps' -import Loading from '@/app/components/base/loading' -import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common' -import type { App } from '@/types/app' +import { fetchCurrentWorkspace, fetchLangGeniusVersion, fetchUserProfile } from '@/service/common' import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import MaintenanceNotice from '@/app/components/header/maintenance-notice' import { noop } from 'lodash-es' export type AppContextValue = { - apps: App[] - mutateApps: VoidFunction userProfile: UserProfileResponse mutateUserProfile: VoidFunction currentWorkspace: ICurrentWorkspace @@ -23,13 +18,21 @@ export type AppContextValue = { isCurrentWorkspaceEditor: boolean isCurrentWorkspaceDatasetOperator: boolean mutateCurrentWorkspace: VoidFunction - pageContainerRef: React.RefObject<HTMLDivElement> - langeniusVersionInfo: LangGeniusVersionResponse + langGeniusVersionInfo: LangGeniusVersionResponse useSelector: typeof useSelector isLoadingCurrentWorkspace: boolean } -const initialLangeniusVersionInfo = { +const userProfilePlaceholder = { + id: '', + name: '', + email: '', + avatar: '', + avatar_url: '', + is_password_set: false, + } + +const initialLangGeniusVersionInfo = { current_env: '', current_version: '', latest_version: '', @@ -50,16 +53,7 @@ const initialWorkspaceInfo: ICurrentWorkspace = { } const AppContext = createContext<AppContextValue>({ - apps: [], - mutateApps: noop, - userProfile: { - id: '', - name: '', - email: '', - avatar: '', - avatar_url: '', - is_password_set: false, - }, + userProfile: userProfilePlaceholder, currentWorkspace: initialWorkspaceInfo, isCurrentWorkspaceManager: false, isCurrentWorkspaceOwner: false, @@ -67,8 +61,7 @@ const AppContext = createContext<AppContextValue>({ isCurrentWorkspaceDatasetOperator: false, mutateUserProfile: noop, mutateCurrentWorkspace: noop, - pageContainerRef: createRef(), - langeniusVersionInfo: initialLangeniusVersionInfo, + langGeniusVersionInfo: initialLangGeniusVersionInfo, useSelector, isLoadingCurrentWorkspace: false, }) @@ -82,14 +75,11 @@ export type AppContextProviderProps = { } export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => { - const pageContainerRef = useRef<HTMLDivElement>(null) - - const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1, limit: 30, name: '' } }, fetchAppList) const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile) const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace) - const [userProfile, setUserProfile] = useState<UserProfileResponse>() - const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo) + const [userProfile, setUserProfile] = useState<UserProfileResponse>(userProfilePlaceholder) + const [langGeniusVersionInfo, setLangGeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangGeniusVersionInfo) const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo) const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role]) const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role]) @@ -101,8 +91,8 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => setUserProfile(result) const current_version = userProfileResponse.headers.get('x-version') const current_env = process.env.NODE_ENV === 'development' ? 'DEVELOPMENT' : userProfileResponse.headers.get('x-env') - const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } }) - setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env }) + const versionData = await fetchLangGeniusVersion({ url: '/version', params: { current_version } }) + setLangGeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env }) } }, [userProfileResponse]) @@ -115,17 +105,11 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => setCurrentWorkspace(currentWorkspaceResponse) }, [currentWorkspaceResponse]) - if (!appList || !userProfile) - return <Loading type='app' /> - return ( <AppContext.Provider value={{ - apps: appList.data, - mutateApps, userProfile, mutateUserProfile, - pageContainerRef, - langeniusVersionInfo, + langGeniusVersionInfo, useSelector, currentWorkspace, isCurrentWorkspaceManager, @@ -137,7 +121,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => }}> <div className='flex h-full flex-col overflow-y-auto'> {globalThis.document?.body?.getAttribute('data-public-maintenance-notice') && <MaintenanceNotice />} - <div ref={pageContainerRef} className='relative flex grow flex-col overflow-y-auto overflow-x-hidden bg-background-body'> + <div className='relative flex grow flex-col overflow-y-auto overflow-x-hidden bg-background-body'> {children} </div> </div> diff --git a/web/context/i18n.ts b/web/context/i18n.ts index ef53a4b481..932beb9936 100644 --- a/web/context/i18n.ts +++ b/web/context/i18n.ts @@ -9,13 +9,15 @@ import { noop } from 'lodash-es' type II18NContext = { locale: Locale i18n: Record<string, any> - setLocaleOnClient: (_lang: Locale, _reloadPage?: boolean) => void + setLocaleOnClient: (_lang: Locale, _reloadPage?: boolean) => Promise<void> } const I18NContext = createContext<II18NContext>({ locale: 'en-US', i18n: {}, - setLocaleOnClient: noop, + setLocaleOnClient: async (_lang: Locale, _reloadPage?: boolean) => { + noop() + }, }) export const useI18N = () => useContext(I18NContext) diff --git a/web/context/query-client.tsx b/web/context/query-client.tsx index f85930515c..3deccba439 100644 --- a/web/context/query-client.tsx +++ b/web/context/query-client.tsx @@ -14,7 +14,7 @@ const client = new QueryClient({ }, }) -export const TanstackQueryIniter: FC<PropsWithChildren> = (props) => { +export const TanstackQueryInitializer: FC<PropsWithChildren> = (props) => { const { children } = props return <QueryClientProvider client={client}> {children} diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index b276289ae8..8f1598e871 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -8,6 +8,7 @@ import storybook from 'eslint-plugin-storybook' import tailwind from 'eslint-plugin-tailwindcss' import reactHooks from 'eslint-plugin-react-hooks' import sonar from 'eslint-plugin-sonarjs' +import oxlint from 'eslint-plugin-oxlint' // import reactRefresh from 'eslint-plugin-react-refresh' @@ -245,4 +246,5 @@ export default combine( 'tailwindcss/migration-from-tailwind-2': 'warn', }, }, + oxlint.configs['flat/recommended'], ) diff --git a/web/i18n/README.md b/web/i18n/README.md index b81ffbf4c3..5e7058d829 100644 --- a/web/i18n/README.md +++ b/web/i18n/README.md @@ -28,7 +28,7 @@ This directory contains the internationalization (i18n) files for this project. │   ├── [ 52] layout.ts │   ├── [2.3K] login.ts │   ├── [ 52] register.ts -│   ├── [2.5K] share-app.ts +│   ├── [2.5K] share.ts │   └── [2.8K] tools.ts ├── [1.6K] i18next-config.ts ├── [ 634] index.ts diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index 95f2722640..c28fcb2be5 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -78,7 +78,7 @@ const translation = { optional: 'Wahlfrei', noTemplateFound: 'Keine Vorlagen gefunden', workflowUserDescription: 'Autonome KI-Arbeitsabläufe visuell per Drag-and-Drop erstellen.', - foundResults: '{{Anzahl}} Befund', + foundResults: '{{count}} Befund', chatbotShortDescription: 'LLM-basierter Chatbot mit einfacher Einrichtung', completionUserDescription: 'Erstellen Sie schnell einen KI-Assistenten für Textgenerierungsaufgaben mit einfacher Konfiguration.', noAppsFound: 'Keine Apps gefunden', @@ -92,7 +92,7 @@ const translation = { noTemplateFoundTip: 'Versuchen Sie, mit verschiedenen Schlüsselwörtern zu suchen.', advancedUserDescription: 'Workflow mit Speicherfunktionen und Chatbot-Oberfläche.', chatbotUserDescription: 'Erstellen Sie schnell einen LLM-basierten Chatbot mit einfacher Konfiguration. Sie können später zu Chatflow wechseln.', - foundResult: '{{Anzahl}} Ergebnis', + foundResult: '{{count}} Ergebnis', agentUserDescription: 'Ein intelligenter Agent, der in der Lage ist, iteratives Denken zu führen und autonome Werkzeuge zu verwenden, um Aufgabenziele zu erreichen.', agentShortDescription: 'Intelligenter Agent mit logischem Denken und autonomer Werkzeugnutzung', dropDSLToCreateApp: 'Ziehen Sie die DSL-Datei hierher, um die App zu erstellen', diff --git a/web/i18n/de-DE/plugin.ts b/web/i18n/de-DE/plugin.ts index 2a79241d00..6fa6999ae5 100644 --- a/web/i18n/de-DE/plugin.ts +++ b/web/i18n/de-DE/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointDeleteTip: 'Endpunkt entfernen', serviceOk: 'Service in Ordnung', switchVersion: 'Version wechseln', + deprecation: { + reason: { + noMaintainer: 'kein Wartender', + ownershipTransferred: 'Eigentum übertragen', + businessAdjustments: 'Geschäftsanpassungen', + }, + onlyReason: 'Dieses Plugin wurde aufgrund von {{deprecatedReason}} abgelehnt und wird nicht länger aktualisiert.', + fullMessage: 'Dieses Plugin wurde aufgrund von {{deprecatedReason}} eingestellt und wird nicht mehr aktualisiert. Bitte verwenden Sie stattdessen <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>.', + noReason: 'Dieses Plugin wurde eingestellt und wird nicht mehr aktualisiert.', + }, }, debugInfo: { title: 'Debuggen', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: 'Nachdem die Anmeldeinformationen konfiguriert wurden, können alle Mitglieder des Arbeitsbereichs dieses Tool beim Orchestrieren von Anwendungen verwenden.', authRemoved: 'Die Authentifizierung wurde entfernt.', }, + deprecated: 'Abgelehnt', + autoUpdate: { + strategy: { + disabled: { + description: 'Plugins werden nicht automatisch aktualisiert', + name: 'Behindert', + }, + fixOnly: { + name: 'Nur fixieren', + selectedDescription: 'Auto-Update nur für Patch-Versionen', + }, + latest: { + description: 'Immer auf die neueste Version aktualisieren', + selectedDescription: 'Immer auf die neueste Version aktualisieren', + name: 'Neueste', + }, + }, + upgradeMode: { + exclude: 'Ausgewählte ausschließen', + partial: 'Nur ausgewählt', + all: 'Alle aktualisieren', + }, + upgradeModePlaceholder: { + exclude: 'Ausgewählte Plugins werden nicht automatisch aktualisiert', + partial: 'Nur ausgewählte Plugins werden automatisch aktualisiert. Derzeit sind keine Plugins ausgewählt, daher werden keine Plugins automatisch aktualisiert.', + }, + operation: { + clearAll: 'Alles löschen', + select: 'Plugins auswählen', + }, + pluginDowngradeWarning: { + downgrade: 'Trotzdem downgraden', + title: 'Plugin Downgrade', + exclude: 'Von der automatischen Aktualisierung ausschließen', + description: 'Die automatische Aktualisierung ist derzeit für dieses Plugin aktiviert. Ein Downgrade der Version kann dazu führen, dass Ihre Änderungen während des nächsten automatischen Updates überschrieben werden.', + }, + noPluginPlaceholder: { + noInstalled: 'Keine Plugins installiert', + noFound: 'Keine Plugins gefunden.', + }, + automaticUpdates: 'Automatische Updates', + updateTimeTitle: 'Aktualisierungszeit', + updateTime: 'Aktualisierungszeit', + excludeUpdate: 'Die folgenden {{num}} Plugins werden nicht automatisch aktualisiert.', + changeTimezone: 'Um die Zeitzone zu ändern, gehen Sie zu <setTimezone> Einstellungen </setTimezone>', + nextUpdateTime: 'Nächstes automatisches Update: {{time}}', + partialUPdate: 'Nur die folgenden {{num}} Plugins werden automatisch aktualisiert', + specifyPluginsToUpdate: 'Geben Sie die zu aktualisierenden Plugins an', + updateSettings: 'Einstellungen aktualisieren', + }, } export default translation diff --git a/web/i18n/de-DE/share-app.ts b/web/i18n/de-DE/share.ts similarity index 100% rename from web/i18n/de-DE/share-app.ts rename to web/i18n/de-DE/share.ts diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 2984f5f77c..aa127eaf13 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -29,6 +29,7 @@ const translation = { searchTools: 'Search tools...', installPlugin: 'Install plugin', installFrom: 'INSTALL FROM', + deprecated: 'Deprecated', list: { noInstalled: 'No plugins installed', notFound: 'No plugins found', @@ -99,6 +100,16 @@ const translation = { configureApp: 'Configure App', configureModel: 'Configure model', configureTool: 'Configure tool', + deprecation: { + fullMessage: 'This plugin has been deprecated due to {{deprecatedReason}}, and will no longer be updated. Please use <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> instead.', + onlyReason: 'This plugin has been deprecated due to {{deprecatedReason}} and will no longer be updated.', + noReason: 'This plugin has been deprecated and will no longer be updated.', + reason: { + businessAdjustments: 'business adjustments', + ownershipTransferred: 'ownership transferred', + noMaintainer: 'no maintainer', + }, + }, }, install: '{{num}} installs', installAction: 'Install', @@ -114,6 +125,56 @@ const translation = { admins: 'Admins', noone: 'No one', }, + autoUpdate: { + automaticUpdates: 'Automatic updates', + updateTime: 'Update time', + specifyPluginsToUpdate: 'Specify plugins to update', + strategy: { + disabled: { + name: 'Disabled', + description: 'Plugins will not auto-update', + }, + fixOnly: { + name: 'Fix Only', + description: 'Auto-update for patch versions only (e.g., 1.0.1 → 1.0.2). Minor version changes won\'t trigger updates.', + selectedDescription: 'Auto-update for patch versions only', + }, + latest: { + name: 'Latest', + description: 'Always update to latest version', + selectedDescription: 'Always update to latest version', + }, + }, + updateTimeTitle: 'Update time', + upgradeMode: { + all: 'Update all', + exclude: 'Exclude selected', + partial: 'Only selected', + }, + upgradeModePlaceholder: { + exclude: 'Selected plugins will not auto-update', + partial: 'Only selected plugins will auto-update. No plugins are currently selected, so no plugins will auto-update.', + }, + excludeUpdate: 'The following {{num}} plugins will not auto-update', + partialUPdate: 'Only the following {{num}} plugins will auto-update', + operation: { + clearAll: 'Clear all', + select: 'Select plugins', + }, + nextUpdateTime: 'Next auto-update: {{time}}', + pluginDowngradeWarning: { + title: 'Plugin Downgrade', + description: 'Auto-update is currently enabled for this plugin. Downgrading the version may cause your changes to be overwritten during the next automatic update.', + downgrade: 'Downgrade anyway', + exclude: 'Exclude from auto-update', + }, + noPluginPlaceholder: { + noFound: 'No plugins were found', + noInstalled: 'No plugins installed', + }, + updateSettings: 'Update Settings', + changeTimezone: 'To change time zone, go to <setTimezone>Settings</setTimezone>', + }, pluginInfoModal: { title: 'Plugin info', repository: 'Repository', diff --git a/web/i18n/en-US/share-app.ts b/web/i18n/en-US/share.ts similarity index 100% rename from web/i18n/en-US/share-app.ts rename to web/i18n/en-US/share.ts diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts index 337232bea0..eba82dd384 100644 --- a/web/i18n/es-ES/common.ts +++ b/web/i18n/es-ES/common.ts @@ -690,6 +690,7 @@ const translation = { license: { expiring: 'Caduca en un día', expiring_plural: 'Caducando en {{count}} días', + unlimited: 'Ilimitado', }, pagination: { perPage: 'Elementos por página', diff --git a/web/i18n/es-ES/dataset.ts b/web/i18n/es-ES/dataset.ts index 16745b56d7..e27fe0cbf9 100644 --- a/web/i18n/es-ES/dataset.ts +++ b/web/i18n/es-ES/dataset.ts @@ -204,6 +204,7 @@ const translation = { name: 'Nombre', description: 'Puedes gestionar todos los metadatos en este conocimiento aquí. Las modificaciones se sincronizarán en todos los documentos.', disabled: 'desactivar', + builtIn: 'Integrado', }, documentMetadata: { technicalParameters: 'Parámetros técnicos', diff --git a/web/i18n/es-ES/plugin.ts b/web/i18n/es-ES/plugin.ts index 630ff3e405..6299684851 100644 --- a/web/i18n/es-ES/plugin.ts +++ b/web/i18n/es-ES/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointsDocLink: 'Ver el documento', endpointsEmpty: 'Haga clic en el botón \'+\' para agregar un punto de conexión', configureApp: 'Configurar la aplicación', + deprecation: { + reason: { + ownershipTransferred: 'propiedad transferida', + noMaintainer: 'sin mantenedor', + businessAdjustments: 'ajustes comerciales', + }, + noReason: 'Este complemento ha sido descontinuado y ya no se actualizará.', + onlyReason: 'Este complemento ha sido desaprobado debido a {{deprecatedReason}} y ya no se actualizará.', + fullMessage: 'Este complemento ha sido descontinuado debido a {{deprecatedReason}}, y ya no será actualizado. Por favor, utilice <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> en su lugar.', + }, }, debugInfo: { title: 'Depuración', @@ -235,6 +245,57 @@ const translation = { saveOnly: 'Guardar solo', clientInfo: 'Como no se encontraron secretos de cliente del sistema para este proveedor de herramientas, se requiere configurarlo manualmente. Para redirect_uri, por favor utiliza', oauthClientSettings: 'Configuración del cliente OAuth', + default: 'Predeterminado', + }, + deprecated: 'Obsoleto', + autoUpdate: { + strategy: { + disabled: { + description: 'Los plugins no se actualizarán automáticamente', + name: 'Discapacitado', + }, + fixOnly: { + name: 'Arreglar Solo', + selectedDescription: 'Actualización automática solo para versiones de parches', + }, + latest: { + selectedDescription: 'Siempre actualiza a la última versión', + description: 'Siempre actualiza a la última versión', + name: 'último', + }, + }, + upgradeMode: { + partial: 'Solo seleccionado', + all: 'Actualizar todo', + exclude: 'Excluir seleccionado', + }, + upgradeModePlaceholder: { + exclude: 'Los plugins seleccionados no se actualizarán automáticamente', + partial: 'Solo los plugins seleccionados se actualizarán automáticamente. Actualmente no hay plugins seleccionados, por lo que no se actualizarán automáticamente.', + }, + operation: { + clearAll: 'Borrar todo', + select: 'Seleccionar complementos', + }, + pluginDowngradeWarning: { + title: 'Degradar plugin', + exclude: 'Excluir de la actualización automática', + downgrade: 'De todas formas, degradar', + description: 'La actualización automática está actualmente habilitada para este complemento. Downgradear la versión puede hacer que tus cambios se sobrescriban durante la próxima actualización automática.', + }, + noPluginPlaceholder: { + noFound: 'No se encontraron complementos', + noInstalled: 'No hay plugins instalados', + }, + updateTimeTitle: 'Hora de actualización', + nextUpdateTime: 'Próxima autoactualización: {{time}}', + specifyPluginsToUpdate: 'Especifique qué complementos actualizar', + updateTime: 'Actualizar tiempo', + updateSettings: 'Actualizar configuraciones', + excludeUpdate: 'Los siguientes {{num}} complementos no se actualizarán automáticamente', + partialUPdate: 'Solo los siguientes {{num}} complementos se actualizarán automáticamente', + changeTimezone: 'Para cambiar la zona horaria, ve a <setTimezone>Configuración</setTimezone>.', + automaticUpdates: 'Actualizaciones automáticas', }, } diff --git a/web/i18n/es-ES/share-app.ts b/web/i18n/es-ES/share.ts similarity index 100% rename from web/i18n/es-ES/share-app.ts rename to web/i18n/es-ES/share.ts diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index 535f92b0b1..3c509934df 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -475,10 +475,12 @@ const translation = { options: { disabled: { subTitle: 'No habilitar el filtrado de metadatos', + title: 'Deshabilitado', }, automatic: { subTitle: 'Generar automáticamente condiciones de filtrado de metadatos basadas en la consulta del usuario', desc: 'Generar automáticamente condiciones de filtrado de metadatos basadas en la variable de consulta', + title: 'Automático', }, manual: { title: 'Manual', @@ -898,6 +900,7 @@ const translation = { error_other: '{{count}} Errores', loopVariables: 'Variables de bucle', variableName: 'Nombre de Variable', + input: 'Entrada', }, }, tracing: { @@ -916,6 +919,7 @@ const translation = { onlyShowNamedVersions: 'Solo muestra versiones nombradas', empty: 'No se encontró un historial de versiones coincidente.', reset: 'Restablecer filtro', + all: 'Todo', }, editField: { titleLengthLimit: 'El título no puede exceder {{limit}} caracteres', @@ -939,6 +943,7 @@ const translation = { deletionTip: 'La eliminación es irreversible, por favor confirma.', currentDraft: 'Borrador Actual', editVersionInfo: 'Editar información de la versión', + latest: 'Último', }, debug: { noData: { @@ -951,6 +956,7 @@ const translation = { stop: 'Detén la carrera', normal: 'Inspeccionar Variable', cached: 'Ver variables en caché', + clear: 'Claro', }, envNode: 'Medio ambiente', chatNode: 'Conversación', @@ -963,6 +969,7 @@ const translation = { resetConversationVar: 'Restablecer la variable de conversación al valor predeterminado', clearNode: 'Limpiar variable en caché', emptyTip: 'Después de recorrer un nodo en el lienzo o ejecutar un nodo paso a paso, puedes ver el valor actual de la variable del nodo en Inspección de Variables.', + edited: 'Editado', }, lastRunTab: 'Última ejecución', settingsTab: 'Ajustes', diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index 890dae5cae..d8dfba3d81 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -77,10 +77,10 @@ const translation = { appCreateDSLErrorPart1: 'تفاوت قابل توجهی در نسخه های DSL مشاهده شده است. اجبار به واردات ممکن است باعث اختلال در عملکرد برنامه شود.', appCreateDSLWarning: 'احتیاط: تفاوت نسخه DSL ممکن است بر ویژگی های خاصی تأثیر بگذارد', completionShortDescription: 'دستیار هوش مصنوعی برای تسک های تولید متن', - foundResult: '{{تعداد}} نتیجه', + foundResult: '{{count}} نتیجه', chatbotUserDescription: 'به سرعت یک چت بات مبتنی بر LLM با پیکربندی ساده بسازید. بعدا می توانید به Chatflow بروید.', chooseAppType: 'انتخاب نوع برنامه', - foundResults: '{{تعداد}} نتیجه', + foundResults: '{{count}} نتیجه', noIdeaTip: 'ایده ای ندارید؟ قالب های ما را بررسی کنید', forBeginners: 'انواع برنامه‌های پایه‌تر', noAppsFound: 'هیچ برنامه ای یافت نشد', diff --git a/web/i18n/fa-IR/plugin.ts b/web/i18n/fa-IR/plugin.ts index aebe39d508..5e1cbe02bf 100644 --- a/web/i18n/fa-IR/plugin.ts +++ b/web/i18n/fa-IR/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointModalDesc: 'پس از پیکربندی، می توان از ویژگی های ارائه شده توسط افزونه از طریق نقاط پایانی API استفاده کرد.', switchVersion: 'نسخه سوئیچ', endpointDeleteContent: 'آیا می خواهید {{name}} را حذف کنید؟', + deprecation: { + reason: { + ownershipTransferred: 'مالکیت منتقل شد', + businessAdjustments: 'تنظیمات کسب و کار', + noMaintainer: 'بدون نگهدارنده', + }, + noReason: 'این افزونه منسوخ شده است و دیگر به روز رسانی نخواهد شد.', + onlyReason: 'این افزونه به دلیل {{deprecatedReason}} منسوخ شده و دیگر به‌روزرسانی نخواهد شد.', + fullMessage: 'این افزونه به دلیل {{deprecatedReason}} منسوخ شده است و دیگر به‌روزرسانی نخواهد شد. لطفا به‌جای آن از <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> استفاده کنید.', + }, }, debugInfo: { title: 'اشکال زدایی', @@ -237,6 +247,56 @@ const translation = { clientInfo: 'از آنجایی که هیچ راز مشتری سیستم برای این ارائه‌دهنده ابزار پیدا نشد، تنظیم دستی آن ضروری است، لطفاً برای redirect_uri از', useApiAuthDesc: 'پس از پیکربندی اعتبارنامه‌ها، تمامی اعضای درون فضای کاری می‌توانند از این ابزار هنگام نظم‌دهی به برنامه‌ها استفاده کنند.', }, + deprecated: 'منسوخ شده', + autoUpdate: { + strategy: { + disabled: { + name: 'ناتوان', + description: 'پلاگین‌ها به‌طور خودکار به‌روزرسانی نخواهند شد', + }, + fixOnly: { + name: 'فقط تعمیر کنید', + selectedDescription: 'به‌روزرسانی خودکار تنها برای نسخه‌های وصله', + }, + latest: { + name: 'جدیدترین', + selectedDescription: 'همیشه به آخرین نسخه بروزرسانی کنید', + description: 'همیشه به آخرین نسخه بروزرسانی کنید', + }, + }, + upgradeMode: { + all: 'همه را بروزرسانی کن', + partial: 'فقط انتخاب شده', + exclude: 'انتخاب شده را استثنا کن', + }, + upgradeModePlaceholder: { + exclude: 'افزونه‌های انتخاب شده به‌صورت خودکار به‌روزرسانی نخواهند شد', + partial: 'فقط پلاگین‌های انتخاب شده به‌روزرسانی خودکار خواهند داشت. در حال حاضر هیچ پلاگینی انتخاب نشده است، بنابراین هیچ پلاگینی به‌روزرسانی خودکار نخواهد شد.', + }, + operation: { + select: 'افزونه‌ها را انتخاب کنید', + clearAll: 'همه را پاک کن', + }, + pluginDowngradeWarning: { + title: 'کاهش نسخه افزونه', + downgrade: 'به هر حال تنزل دهید', + exclude: 'از بروزرسانی خودکار مستثنی شود', + description: 'به‌روزرسانی خودکار برای این افزونه در حال حاضر فعال است. کاهش نسخه ممکن است باعث شود تغییرات شما در حین به‌روزرسانی خودکار بعدی نادیده گرفته شود.', + }, + noPluginPlaceholder: { + noFound: 'هیچ افزونه‌ای یافت نشد', + noInstalled: 'هیچ افزونه‌ای نصب نشده است', + }, + updateTimeTitle: 'زمان به‌روزرسانی', + specifyPluginsToUpdate: 'ماژول‌هایی را برای به‌روزرسانی مشخص کنید', + updateTime: 'زمان به‌روزرسانی', + automaticUpdates: 'بروز رسانی خودکار', + updateSettings: 'تنظیمات را به‌روزرسانی کنید', + changeTimezone: 'برای تغییر منطقه زمانی، به <setTimezone>تنظیمات</setTimezone> بروید', + excludeUpdate: 'پلاگین‌های زیر {{num}} به‌طور خودکار به‌روزرسانی نخواهند شد', + nextUpdateTime: 'به‌روزرسانی خودکار بعدی: {{time}}', + partialUPdate: 'تنها {{num}} پلاگین زیر به‌طور خودکار به‌روزرسانی خواهد شد.', + }, } export default translation diff --git a/web/i18n/fa-IR/share-app.ts b/web/i18n/fa-IR/share.ts similarity index 100% rename from web/i18n/fa-IR/share-app.ts rename to web/i18n/fa-IR/share.ts diff --git a/web/i18n/fr-FR/plugin.ts b/web/i18n/fr-FR/plugin.ts index dcab0a1ead..255171058a 100644 --- a/web/i18n/fr-FR/plugin.ts +++ b/web/i18n/fr-FR/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointModalTitle: 'Configurer le point de terminaison', serviceOk: 'Service OK', endpointModalDesc: 'Une fois configuré, les fonctionnalités fournies par le plugin via les points de terminaison de l’API peuvent être utilisées.', + deprecation: { + reason: { + ownershipTransferred: 'propriété transférée', + businessAdjustments: 'ajustements commerciaux', + noMaintainer: 'aucun mainteneur', + }, + noReason: 'Ce plugin a été abandonné et ne sera plus mis à jour.', + onlyReason: 'Ce plugin a été déprécié en raison de {{deprecatedReason}} et ne sera plus mis à jour.', + fullMessage: 'Ce plugin a été déprécié en raison de {{deprecatedReason}}, et ne sera plus mis à jour. Veuillez utiliser <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> à la place.', + }, }, debugInfo: { title: 'Débogage', @@ -237,6 +247,56 @@ const translation = { authorization: 'Autorisation', useApi: 'Utilisez la clé API', }, + deprecated: 'Obsolète', + autoUpdate: { + strategy: { + disabled: { + description: 'Les plugins ne se mettront pas à jour automatiquement', + name: 'désactivé', + }, + fixOnly: { + selectedDescription: 'Mise à jour automatique uniquement pour les versions de correctif', + name: 'Réparer seulement', + }, + latest: { + name: 'Dernier', + selectedDescription: 'Mettez toujours à jour vers la dernière version', + description: 'Mettez toujours à jour vers la dernière version', + }, + }, + upgradeMode: { + exclude: 'Exclure sélectionné', + all: 'Mettre à jour tout', + partial: 'Seulement sélectionné', + }, + upgradeModePlaceholder: { + partial: 'Seuls les plugins sélectionnés se mettront à jour automatiquement. Aucun plugin n\'est actuellement sélectionné, donc aucun plugin ne se mettra à jour automatiquement.', + exclude: 'Les plugins sélectionnés ne se mettront pas à jour automatiquement.', + }, + operation: { + clearAll: 'Tout effacer', + select: 'Sélectionner des plugins', + }, + pluginDowngradeWarning: { + title: 'Baisse de version du plugin', + exclude: 'Exclure de la mise à jour automatique', + downgrade: 'Dégradez de toute façon', + description: 'La mise à jour automatique est actuellement activée pour ce plugin. Le fait de rétrograder la version peut entraîner la perte de vos modifications lors de la prochaine mise à jour automatique.', + }, + noPluginPlaceholder: { + noInstalled: 'Aucun plugin installé', + noFound: 'Aucun plugin n\'a été trouvé', + }, + updateTime: 'Temps de mise à jour', + specifyPluginsToUpdate: 'Spécifiez les plugins à mettre à jour', + updateTimeTitle: 'Temps de mise à jour', + changeTimezone: 'Pour changer de fuseau horaire, allez dans <setTimezone>Paramètres</setTimezone>', + automaticUpdates: 'Mises à jour automatiques', + updateSettings: 'Mettre à jour les paramètres', + excludeUpdate: 'Les {{num}} plugins suivants ne se mettront pas à jour automatiquement', + partialUPdate: 'Seuls les {{num}} plugins suivants se mettront à jour automatiquement', + nextUpdateTime: 'Prochaine mise à jour automatique : {{time}}', + }, } export default translation diff --git a/web/i18n/fr-FR/share-app.ts b/web/i18n/fr-FR/share.ts similarity index 100% rename from web/i18n/fr-FR/share-app.ts rename to web/i18n/fr-FR/share.ts diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index f1fd1a54fa..dcd5e54bdc 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -74,12 +74,12 @@ const translation = { appCreateDSLErrorPart2: 'क्या आप जारी रखना चाहते हैं?', learnMore: 'और जानो', forBeginners: 'नए उपयोगकर्ताओं के लिए बुनियादी ऐप प्रकार', - foundResults: '{{गिनती}} परिणाम', + foundResults: '{{count}} परिणाम', forAdvanced: 'उन्नत उपयोगकर्ताओं के लिए', agentUserDescription: 'पुनरावृत्त तर्क और स्वायत्त उपकरण में सक्षम एक बुद्धिमान एजेंट कार्य लक्ष्यों को प्राप्त करने के लिए उपयोग करता है।', optional: 'वैकल्पिक', chatbotShortDescription: 'सरल सेटअप के साथ एलएलएम-आधारित चैटबॉट', - foundResult: '{{गिनती}} परिणाम', + foundResult: '{{count}} परिणाम', completionUserDescription: 'सरल कॉन्फ़िगरेशन के साथ पाठ निर्माण कार्यों के लिए त्वरित रूप से AI सहायक बनाएं।', noIdeaTip: 'कोई विचार नहीं? हमारे टेम्प्लेट देखें', noTemplateFound: 'कोई टेम्पलेट नहीं मिला', diff --git a/web/i18n/hi-IN/plugin.ts b/web/i18n/hi-IN/plugin.ts index cd5540d5a9..ae4547421c 100644 --- a/web/i18n/hi-IN/plugin.ts +++ b/web/i18n/hi-IN/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointModalTitle: 'एंडपॉइंट सेटअप करें', strategyNum: '{{num}} {{रणनीति}} शामिल', endpointsTip: 'यह प्लगइन एंडपॉइंट्स के माध्यम से विशिष्ट कार्यक्षमताएँ प्रदान करता है, और आप वर्तमान कार्यक्षेत्र के लिए कई एंडपॉइंट सेट कॉन्फ़िगर कर सकते हैं।', + deprecation: { + reason: { + noMaintainer: 'कोई देखभाल करने वाला नहीं', + ownershipTransferred: 'स्वामित्व स्थानांतरित किया गया', + businessAdjustments: 'व्यवसाय समायोजन', + }, + noReason: 'यह प्लगइन अप्रचलित हो गया है और इसे अब अपडेट नहीं किया जाएगा।', + onlyReason: 'इस प्लगइन को {{deprecatedReason}} के कारण अमान्य कर दिया गया है और इसे अब अपडेट नहीं किया जाएगा।', + fullMessage: 'इस प्लगइन को {{deprecatedReason}} के कारण अमान्य कर दिया गया है, और इसे अब अपडेट नहीं किया जाएगा। कृपया इसके बजाय <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> का उपयोग करें।', + }, }, debugInfo: { viewDocs: 'दस्तावेज़ देखें', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: 'क्रेडेंशियल्स कॉन्फ़िगर करने के बाद, कार्यक्षेत्र के सभी सदस्यों को एप्लिकेशन को व्यवस्थित करते समय इस उपकरण का उपयोग करने की अनुमति होती है।', clientInfo: 'चूंकि इस टूल प्रदाता के लिए कोई सिस्टम क्लाइंट रहस्य नहीं पाए गए हैं, इसलिए इसे मैन्युअल रूप से सेटअप करना आवश्यक है, कृपया redirect_uri का उपयोग करें', }, + deprecated: 'अनुशंसित नहीं', + autoUpdate: { + strategy: { + disabled: { + name: 'अक्षम', + description: 'प्लगइन्स स्वचालित रूप से अपडेट नहीं होंगे', + }, + fixOnly: { + name: 'केवल ठीक करें', + selectedDescription: 'केवल पैच संस्करणों के लिए स्वचालित अपडेट', + }, + latest: { + name: 'नवीनतम', + selectedDescription: 'हमेशा नवीनतम संस्करण पर अद्यतन करें', + description: 'हमेशा नवीनतम संस्करण पर अद्यतन करें', + }, + }, + upgradeMode: { + all: 'सभी अपडेट करें', + partial: 'केवल चयनित', + exclude: 'चुने हुए को बाहर करें', + }, + upgradeModePlaceholder: { + partial: 'केवल चयनित प्लगइन्स स्वतः अपडेट होंगे। वर्तमान में कोई प्लगइन चयनित नहीं है, इसलिए कोई प्लगइन स्वतः अपडेट नहीं होगा।', + exclude: 'चुने हुए प्लगइन्स अपने आप अपडेट नहीं होंगे', + }, + operation: { + clearAll: 'सभी हटाएं', + select: 'प्लगइन्स चुनें', + }, + pluginDowngradeWarning: { + downgrade: 'फिर भी डाउनग्रेड करें', + title: 'प्लगइन डाउनग्रेड', + exclude: 'स्वतः अपडेट से बाहर करें', + description: 'इस प्लगइन के लिए ऑटो-अपडेट वर्तमान में सक्षम है। संस्करण को डाउनग्रेड करने से आपके परिवर्तनों को अगली स्वचालित अद्यतन के दौरान ओवरराइट किया जा सकता है।', + }, + noPluginPlaceholder: { + noFound: 'कोई प्लगइन्स नहीं मिले', + noInstalled: 'कोई प्लगइन स्थापित नहीं है', + }, + updateTimeTitle: 'अद्यतन समय', + updateSettings: 'सेटिंग्स अपडेट करें', + automaticUpdates: 'स्वचालित अपडेट', + partialUPdate: 'केवल निम्नलिखित {{num}} प्लगइन्स स्वचालित रूप से अपडेट होंगे', + nextUpdateTime: 'अगली ऑटो-अपडेट: {{time}}', + updateTime: 'अद्यतन समय', + specifyPluginsToUpdate: 'अपडेट करने के लिए प्लगइन्स निर्दिष्ट करें', + changeTimezone: 'समय क्षेत्र बदलने के लिए, <setTimezone>सेटिंग्स</setTimezone> पर जाएं', + excludeUpdate: 'निम्नलिखित {{num}} प्लगइन्स स्वचालित रूप से अपडेट नहीं होंगे', + }, } export default translation diff --git a/web/i18n/hi-IN/share-app.ts b/web/i18n/hi-IN/share.ts similarity index 100% rename from web/i18n/hi-IN/share-app.ts rename to web/i18n/hi-IN/share.ts diff --git a/web/i18n/i18next-config.ts b/web/i18n/i18next-config.ts index 8c5583bf9a..8c5bd375a7 100644 --- a/web/i18n/i18next-config.ts +++ b/web/i18n/i18next-config.ts @@ -1,65 +1,74 @@ 'use client' import i18n from 'i18next' +import { camelCase } from 'lodash-es' import { initReactI18next } from 'react-i18next' -import { LanguagesSupported } from '@/i18n/language' - -const requireSilent = (lang: string) => { +const requireSilent = async (lang: string, namespace: string) => { let res try { - res = require(`./${lang}/education`).default + res = (await import(`./${lang}/${namespace}`)).default } catch { - res = require('./en-US/education').default + res = (await import(`./en-US/${namespace}`)).default } return res } -const loadLangResources = (lang: string) => ({ - translation: { - common: require(`./${lang}/common`).default, - layout: require(`./${lang}/layout`).default, - login: require(`./${lang}/login`).default, - register: require(`./${lang}/register`).default, - app: require(`./${lang}/app`).default, - appOverview: require(`./${lang}/app-overview`).default, - appDebug: require(`./${lang}/app-debug`).default, - appApi: require(`./${lang}/app-api`).default, - appLog: require(`./${lang}/app-log`).default, - appAnnotation: require(`./${lang}/app-annotation`).default, - share: require(`./${lang}/share-app`).default, - dataset: require(`./${lang}/dataset`).default, - datasetDocuments: require(`./${lang}/dataset-documents`).default, - datasetHitTesting: require(`./${lang}/dataset-hit-testing`).default, - datasetSettings: require(`./${lang}/dataset-settings`).default, - datasetCreation: require(`./${lang}/dataset-creation`).default, - explore: require(`./${lang}/explore`).default, - billing: require(`./${lang}/billing`).default, - custom: require(`./${lang}/custom`).default, - tools: require(`./${lang}/tools`).default, - workflow: require(`./${lang}/workflow`).default, - runLog: require(`./${lang}/run-log`).default, - plugin: require(`./${lang}/plugin`).default, - pluginTags: require(`./${lang}/plugin-tags`).default, - time: require(`./${lang}/time`).default, - education: requireSilent(lang), - }, -}) +const NAMESPACES = [ + 'app-annotation', + 'app-api', + 'app-debug', + 'app-log', + 'app-overview', + 'app', + 'billing', + 'common', + 'custom', + 'dataset-creation', + 'dataset-documents', + 'dataset-hit-testing', + 'dataset-settings', + 'dataset', + 'education', + 'explore', + 'layout', + 'login', + 'plugin-tags', + 'plugin', + 'register', + 'run-log', + 'share', + 'time', + 'tools', + 'workflow', +] -type Resource = Record<string, ReturnType<typeof loadLangResources>> -// Automatically generate the resources object -export const resources = LanguagesSupported.reduce<Resource>((acc, lang) => { - acc[lang] = loadLangResources(lang) - return acc -}, {}) +export const loadLangResources = async (lang: string) => { + const modules = await Promise.all(NAMESPACES.map(ns => requireSilent(lang, ns))) + const resources = modules.reduce((acc, mod, index) => { + acc[camelCase(NAMESPACES[index])] = mod + return acc + }, {} as Record<string, any>) + return { + translation: resources, + } +} i18n.use(initReactI18next) .init({ lng: undefined, fallbackLng: 'en-US', - resources, }) -export const changeLanguage = i18n.changeLanguage +export const changeLanguage = async (lng?: string) => { + const resolvedLng = lng ?? 'en-US' + const resources = { + [resolvedLng]: await loadLangResources(resolvedLng), + } + if (!i18n.hasResourceBundle(resolvedLng, 'translation')) + i18n.addResourceBundle(resolvedLng, 'translation', resources[resolvedLng].translation, true, true) + await i18n.changeLanguage(resolvedLng) +} + export default i18n diff --git a/web/i18n/index.ts b/web/i18n/index.ts index eb49759097..27ed3022ad 100644 --- a/web/i18n/index.ts +++ b/web/i18n/index.ts @@ -11,9 +11,9 @@ export const i18n = { export type Locale = typeof i18n['locales'][number] -export const setLocaleOnClient = (locale: Locale, reloadPage = true) => { +export const setLocaleOnClient = async (locale: Locale, reloadPage = true) => { Cookies.set(LOCALE_COOKIE_NAME, locale, { expires: 365 }) - changeLanguage(locale) + await changeLanguage(locale) reloadPage && location.reload() } diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index a874d2b71f..63a25dccc6 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -232,6 +232,7 @@ const translation = { structuredTip: 'Le Uscite Strutturate sono una funzione che garantisce che il modello generi sempre risposte che aderiscano al tuo Schema JSON fornito.', notConfiguredTip: 'L\'output strutturato non è stato ancora configurato.', modelNotSupportedTip: 'Il modello attuale non supporta questa funzione e viene automaticamente downgradato a iniezione di prompt.', + required: 'Necessario', }, accessItemsDescription: { anyone: 'Chiunque può accedere all\'app web', diff --git a/web/i18n/it-IT/dataset.ts b/web/i18n/it-IT/dataset.ts index 9f66ee8e43..c8d5482a4d 100644 --- a/web/i18n/it-IT/dataset.ts +++ b/web/i18n/it-IT/dataset.ts @@ -211,6 +211,7 @@ const translation = { deleteContent: 'Sei sicuro di voler eliminare i metadati "{{name}}"?', builtInDescription: 'I metadati incorporati vengono estratti e generati automaticamente. Devono essere abilitati prima dell\'uso e non possono essere modificati.', description: 'Puoi gestire tutti i metadati in questa conoscenza qui. Le modifiche saranno sincronizzate con ogni documento.', + builtIn: 'Integrato', }, documentMetadata: { documentInformation: 'Informazioni sul documento', diff --git a/web/i18n/it-IT/plugin.ts b/web/i18n/it-IT/plugin.ts index f2aa0977f0..e7b6b147fa 100644 --- a/web/i18n/it-IT/plugin.ts +++ b/web/i18n/it-IT/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointDeleteTip: 'Rimuovi punto finale', endpointsEmpty: 'Fare clic sul pulsante \'+\' per aggiungere un punto finale', actionNum: '{{num}} {{azione}} INCLUSO', + deprecation: { + reason: { + noMaintainer: 'nessun manutentore', + ownershipTransferred: 'proprietà trasferita', + businessAdjustments: 'adeguamenti aziendali', + }, + onlyReason: 'Questo plugin è stato deprecato a causa di {{deprecatedReason}} e non verrà più aggiornato.', + fullMessage: 'Questo plugin è stato deprecato a causa di {{deprecatedReason}} e non verrà più aggiornato. Si prega di utilizzare <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> invece.', + noReason: 'Questo plugin è stato deprecato e non sarà più aggiornato.', + }, }, debugInfo: { title: 'Debug', @@ -237,6 +247,56 @@ const translation = { useApiAuth: 'Configurazione dell\'autorizzazione della chiave API', clientInfo: 'Poiché non sono stati trovati segreti client di sistema per questo fornitore di strumenti, è necessario configurarlo manualmente. Per redirect_uri, si prega di utilizzare', }, + deprecated: 'Deprecato', + autoUpdate: { + strategy: { + disabled: { + name: 'Disabile', + description: 'I plugin non si aggiorneranno automaticamente', + }, + fixOnly: { + name: 'Ripara solo', + selectedDescription: 'Aggiornamento automatico solo per versioni patch', + }, + latest: { + selectedDescription: 'Aggiorna sempre all\'ultima versione', + description: 'Aggiorna sempre all\'ultima versione', + name: 'Ultimo', + }, + }, + upgradeMode: { + exclude: 'Escludi selezionato', + all: 'Aggiorna tutto', + partial: 'Solo selezionati', + }, + upgradeModePlaceholder: { + exclude: 'I plugin selezionati non verranno aggiornati automaticamente', + partial: 'Solo i plugin selezionati si aggiorneranno automaticamente. Attualmente non ci sono plugin selezionati, quindi nessun plugin si aggiornerà automaticamente.', + }, + operation: { + clearAll: 'Cancella tutto', + select: 'Seleziona i plugin', + }, + pluginDowngradeWarning: { + title: 'Downgrade del plugin', + downgrade: 'Comunque esegui il downgrade', + exclude: 'Escludi dall\'aggiornamento automatico', + description: 'L\'aggiornamento automatico è attualmente abilitato per questo plugin. Il downgrade della versione potrebbe causare la sovrascrittura delle tue modifiche durante il prossimo aggiornamento automatico.', + }, + noPluginPlaceholder: { + noFound: 'Nessun plugin trovato', + noInstalled: 'Nessun plugin installato', + }, + specifyPluginsToUpdate: 'Specifica i plugin da aggiornare', + updateTime: 'Tempo di aggiornamento', + automaticUpdates: 'Aggiornamenti automatici', + updateSettings: 'Aggiorna impostazioni', + nextUpdateTime: 'Prossimo aggiornamento automatico: {{time}}', + partialUPdate: 'Solo i seguenti {{num}} plugin si aggiorneranno automaticamente', + changeTimezone: 'Per cambiare il fuso orario, vai su <setTimezone>Impostazioni</setTimezone>', + excludeUpdate: 'I seguenti {{num}} plugin non si aggiorneranno automaticamente', + updateTimeTitle: 'Tempo di aggiornamento', + }, } export default translation diff --git a/web/i18n/it-IT/share-app.ts b/web/i18n/it-IT/share.ts similarity index 100% rename from web/i18n/it-IT/share-app.ts rename to web/i18n/it-IT/share.ts diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index 024ee8b90c..97e4bc14f2 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -497,6 +497,7 @@ const translation = { automatic: { subTitle: 'Genera automaticamente condizioni di filtraggio dei metadati in base alla query dell\'utente', desc: 'Genera automaticamente condizioni di filtraggio dei metadati basate sulla variabile di query', + title: 'Automatico', }, manual: { title: 'Manuale', @@ -611,6 +612,7 @@ const translation = { 'exists': 'Esiste', 'not exists': 'non esiste', 'after': 'dopo', + 'before': 'prima', }, enterValue: 'Inserisci valore', addCondition: 'Aggiungi Condizione', diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index 7b6afc99f5..e03e9e1177 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -79,11 +79,11 @@ const translation = { appCreateDSLErrorTitle: 'バージョンの非互換性', appCreateDSLWarning: '注意:DSL のバージョンの違いは、特定の機能に影響を与える可能性があります', appCreateDSLErrorPart1: 'DSL バージョンに大きな違いが検出されました。インポートを強制すると、アプリケーションが誤動作する可能性があります。', - optional: '随意', + optional: '任意', forBeginners: '初心者向けの基本的なアプリタイプ', noTemplateFoundTip: '別のキーワードを使用して検索してみてください。', agentShortDescription: '推論と自律的なツールの使用を備えたインテリジェントエージェント', - foundResults: '{{カウント}}業績', + foundResults: '{{count}}件の結果', noTemplateFound: 'テンプレートが見つかりません', noAppsFound: 'アプリが見つかりませんでした', workflowShortDescription: 'インテリジェントな自動化のためのエージェントフロー', @@ -91,7 +91,7 @@ const translation = { advancedUserDescription: '追加のメモリ機能とチャットボットインターフェースを備えたワークフロー', advancedShortDescription: 'メモリを使用した複雑なマルチターン対話のワークフロー', agentUserDescription: 'タスクの目標を達成するために反復的な推論と自律的なツールを使用できるインテリジェントエージェント。', - foundResult: '{{カウント}}結果', + foundResult: '{{count}}件の結果', forAdvanced: '上級ユーザー向け', chooseAppType: 'アプリタイプを選択', learnMore: '詳細情報', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index 74c84e616a..c346984932 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -43,7 +43,7 @@ const translation = { log: 'ログ', learnMore: '詳細はこちら', params: 'パラメータ', - duplicate: '重複', + duplicate: '複製', rename: '名前の変更', audioSourceUnavailable: 'AudioSource が利用できません', zoomIn: 'ズームインする', @@ -229,7 +229,7 @@ const translation = { permanentlyDeleteButton: 'アカウントを完全に削除', feedbackTitle: 'フィードバック', feedbackLabel: 'アカウントを削除した理由を教えてください。', - feedbackPlaceholder: '随意', + feedbackPlaceholder: '任意', sendVerificationButton: '確認コードの送信', editWorkspaceInfo: 'ワークスペース情報を編集', workspaceName: 'ワークスペース名', diff --git a/web/i18n/ja-JP/plugin.ts b/web/i18n/ja-JP/plugin.ts index e39479ea74..38b73a847e 100644 --- a/web/i18n/ja-JP/plugin.ts +++ b/web/i18n/ja-JP/plugin.ts @@ -2,7 +2,7 @@ const translation = { category: { extensions: '拡張機能', all: 'すべて', - tools: '道具', + tools: 'ツール', bundles: 'バンドル', agents: 'エージェント戦略', models: 'モデル', @@ -11,7 +11,7 @@ const translation = { agent: 'エージェント戦略', model: 'モデル', bundle: 'バンドル', - tool: '道具', + tool: 'ツール', extension: '拡張', }, list: { @@ -60,7 +60,7 @@ const translation = { uninstalledTitle: 'ツールがインストールされていません', empty: 'ツールを追加するには「+」ボタンをクリックしてください。複数のツールを追加できます。', paramsTip1: 'LLM 推論パラメータを制御します。', - toolLabel: '道具', + toolLabel: 'ツール', unsupportedTitle: 'サポートされていないアクション', toolSetting: 'ツール設定', unsupportedMCPTool: '現在選択されているエージェント戦略プラグインのバージョンはMCPツールをサポートしていません。', @@ -84,6 +84,16 @@ const translation = { actionNum: '{{num}} {{action}} が含まれています', endpointsDocLink: 'ドキュメントを表示する', switchVersion: 'バージョンの切り替え', + deprecation: { + fullMessage: 'このプラグインは{{deprecatedReason}}のため非推奨となり、新しいバージョンはリリースされません。代わりに<CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>をご利用ください。', + onlyReason: 'このプラグインは{{deprecatedReason}}のため非推奨となり、新しいバージョンはリリースされません。', + noReason: 'このプラグインは廃止されており、今後更新されることはありません。', + reason: { + businessAdjustments: '事業調整', + ownershipTransferred: '所有権移転', + noMaintainer: 'メンテナーの不足', + }, + }, }, debugInfo: { title: 'デバッグ', @@ -198,6 +208,7 @@ const translation = { install: '{{num}} インストール', installAction: 'インストール', installFrom: 'インストール元', + deprecated: '非推奨', searchPlugins: '検索プラグイン', search: '検索', endpointsEnabled: '{{num}} セットのエンドポイントが有効になりました', @@ -237,6 +248,55 @@ const translation = { useApiAuthDesc: '認証情報を設定した後、ワークスペース内のすべてのメンバーは、アプリケーションをオーケストレーションする際にこのツールを使用できます。', clientInfo: 'このツールプロバイダーにシステムクライアントシークレットが見つからないため、手動で設定する必要があります。redirect_uriには、次を使用してください。', }, + autoUpdate: { + strategy: { + disabled: { + name: '無効', + description: 'プラグインは自動更新されません', + }, + fixOnly: { + name: '修正のみ', + selectedDescription: 'パッチバージョンのみの自動更新', + }, + latest: { + name: '最新', + selectedDescription: '常に最新バージョンに更新してください', + description: '常に最新バージョンに更新してください', + }, + }, + upgradeMode: { + partial: '選択されたもののみ', + exclude: '選択したものを除外する', + all: 'すべてを更新する', + }, + upgradeModePlaceholder: { + exclude: '選択されたプラグインは自動更新されません', + partial: '選択されたプラグインのみが自動更新されます。現在選択されているプラグインはないため、プラグインは自動更新されません。', + }, + operation: { + clearAll: 'すべてクリア', + select: 'プラグインを選択する', + }, + pluginDowngradeWarning: { + title: 'プラグインのダウングレード', + downgrade: 'とにかくダウングレードする', + exclude: '自動更新から除外する', + description: 'このプラグインは現在、自動更新が有効です。バージョンをダウングレードすると、次回の自動更新中に変更が上書きされる可能性があります。', + }, + noPluginPlaceholder: { + noInstalled: 'プラグインがインストールされていません', + noFound: 'プラグインが見つかりませんでした', + }, + updateTimeTitle: '更新時刻', + automaticUpdates: '自動更新', + updateTime: '更新時刻', + updateSettings: '設定を更新する', + nextUpdateTime: '次の自動更新: {{time}}', + excludeUpdate: '以下の{{num}}プラグインは自動更新されません', + changeTimezone: 'タイムゾーンを変更するには、<setTimezone>設定</setTimezone>に移動してください。', + specifyPluginsToUpdate: '更新するプラグインを指定してください', + partialUPdate: '以下の{{num}}プラグインのみが自動更新されます', + }, } export default translation diff --git a/web/i18n/ja-JP/share-app.ts b/web/i18n/ja-JP/share.ts similarity index 100% rename from web/i18n/ja-JP/share-app.ts rename to web/i18n/ja-JP/share.ts diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 58cd2e3f58..035bba61a6 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -887,7 +887,7 @@ const translation = { modelNotSelected: 'モデルが選択されていません', toolNotAuthorizedTooltip: '{{tool}} 認可されていません', toolNotInstallTooltip: '{{tool}}はインストールされていません', - tools: '道具', + tools: 'ツール', learnMore: 'もっと学ぶ', configureModel: 'モデルを設定する', model: 'モデル', diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index 40c3183d91..bcc18e70f0 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -90,12 +90,12 @@ const translation = { noTemplateFound: '템플릿을 찾을 수 없습니다.', completionShortDescription: '텍스트 생성 작업을 위한 AI 도우미', learnMore: '더 알아보세요', - foundResults: '{{개수}} 결과', + foundResults: '{{count}} 결과', agentShortDescription: '추론 및 자율적인 도구 사용 기능이 있는 지능형 에이전트', advancedShortDescription: '다중 대화를 위해 강화된 워크플로우', noAppsFound: '앱을 찾을 수 없습니다.', - foundResult: '{{개수}} 결과', + foundResult: '{{count}} 결과', completionUserDescription: '간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.', chatbotUserDescription: diff --git a/web/i18n/ko-KR/plugin.ts b/web/i18n/ko-KR/plugin.ts index 6050448fbf..1f60f1365b 100644 --- a/web/i18n/ko-KR/plugin.ts +++ b/web/i18n/ko-KR/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpoints: '끝점', serviceOk: '서비스 정상', endpointDisableTip: '엔드포인트 비활성화', + deprecation: { + reason: { + ownershipTransferred: '소유권 이전', + businessAdjustments: '사업 조정', + noMaintainer: '유지보수자 없음', + }, + noReason: '이 플러그인은 더 이상 지원되지 않으며 업데이트되지 않을 것입니다.', + onlyReason: '이 플러그인은 {{deprecatedReason}}로 인해 사용 중단되었으며 더 이상 업데이트되지 않습니다.', + fullMessage: '이 플러그인은 {{deprecatedReason}}로 인해 사용 중단되었으며 더 이상 업데이트되지 않습니다. 대신 <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>를 사용하십시오.', + }, }, debugInfo: { title: '디버깅', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: '자격증명을 구성한 후에는 작업 공간 내의 모든 구성원이 애플리케이션을 조정할 때 이 도구를 사용할 수 있습니다.', clientInfo: '이 도구 공급자에 대한 시스템 클라이언트 비밀이 발견되지 않았으므로 수동으로 설정해야 하며, redirect_uri는 다음을 사용하십시오.', }, + deprecated: '사용 중단됨', + autoUpdate: { + strategy: { + disabled: { + name: '장애인', + description: '플러그인이 자동으로 업데이트되지 않습니다.', + }, + fixOnly: { + name: '수정만 하기', + selectedDescription: '패치 버전만 자동 업데이트', + }, + latest: { + name: '최신', + description: '항상 최신 버전으로 업데이트하세요.', + selectedDescription: '항상 최신 버전으로 업데이트하세요.', + }, + }, + upgradeMode: { + partial: '선택된 것만', + all: '모두 업데이트하기', + exclude: '선택한 항목 제외', + }, + upgradeModePlaceholder: { + partial: '선택된 플러그인만 자동 업데이트됩니다. 현재 선택된 플러그인이 없으므로 자동 업데이트되는 플러그인은 없습니다.', + exclude: '선택한 플러그인은 자동으로 업데이트되지 않습니다.', + }, + operation: { + clearAll: '모두 지우기', + select: '플러그인을 선택하세요', + }, + pluginDowngradeWarning: { + exclude: '자동 업데이트에서 제외', + title: '플러그인 다운그레이드', + downgrade: '어쨌든 다운그레이드', + description: '이 플러그인은 현재 자동 업데이트가 활성화되어 있습니다. 버전을 다운그레이드하면 다음 자동 업데이트 중에 변경 사항이 덮어써질 수 있습니다.', + }, + noPluginPlaceholder: { + noFound: '플러그인이 없습니다.', + noInstalled: '설치된 플러그인이 없습니다.', + }, + updateTimeTitle: '업데이트 시간', + automaticUpdates: '자동 업데이트', + updateTime: '업데이트 시간', + nextUpdateTime: '다음 자동 업데이트: {{time}}', + updateSettings: '설정 업데이트', + partialUPdate: '다음 {{num}} 플러그인만 자동 업데이트됩니다.', + changeTimezone: '시간대를 변경하려면 <setTimezone>설정</setTimezone>으로 이동하세요.', + specifyPluginsToUpdate: '업데이트할 플러그인을 지정하십시오.', + excludeUpdate: '다음 {{num}} 플러그인은 자동 업데이트되지 않습니다.', + }, } export default translation diff --git a/web/i18n/ko-KR/share-app.ts b/web/i18n/ko-KR/share.ts similarity index 100% rename from web/i18n/ko-KR/share-app.ts rename to web/i18n/ko-KR/share.ts diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index f5fec6caeb..040789424c 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -80,7 +80,7 @@ const translation = { appCreateDSLErrorPart1: 'Wykryto istotną różnicę w wersjach DSL. Wymuszenie importu może spowodować nieprawidłowe działanie aplikacji.', noTemplateFoundTip: 'Spróbuj wyszukać za pomocą różnych słów kluczowych.', noAppsFound: 'Nie znaleziono aplikacji', - foundResults: '{{liczba}} Wyniki', + foundResults: '{{count}} Wyniki', noTemplateFound: 'Nie znaleziono szablonów', chatbotUserDescription: 'Szybko zbuduj chatbota opartego na LLM z prostą konfiguracją. Możesz przełączyć się na Chatflow później.', optional: 'Fakultatywny', @@ -91,7 +91,7 @@ const translation = { completionShortDescription: 'Asystent AI do zadań generowania tekstu', noIdeaTip: 'Nie masz pomysłów? Sprawdź nasze szablony', forAdvanced: 'DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW', - foundResult: '{{liczba}} Wynik', + foundResult: '{{count}} Wynik', advancedShortDescription: 'Przepływ ulepszony dla wieloturowych czatów', learnMore: 'Dowiedz się więcej', chatbotShortDescription: 'Chatbot oparty na LLM z prostą konfiguracją', diff --git a/web/i18n/pl-PL/plugin.ts b/web/i18n/pl-PL/plugin.ts index a4b194b757..10944a339b 100644 --- a/web/i18n/pl-PL/plugin.ts +++ b/web/i18n/pl-PL/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointDeleteContent: 'Czy chcesz usunąć {{name}}?', endpointsTip: 'Ta wtyczka zapewnia określone funkcje za pośrednictwem punktów końcowych i można skonfigurować wiele zestawów punktów końcowych dla bieżącego obszaru roboczego.', modelNum: '{{liczba}} MODELE W ZESTAWIE', + deprecation: { + reason: { + businessAdjustments: 'dostosowania biznesowe', + ownershipTransferred: 'własność przekazana', + noMaintainer: 'brak opiekuna', + }, + onlyReason: 'Ten plugin został wycofany z użycia z powodu {{deprecatedReason}} i nie będzie już aktualizowany.', + noReason: 'Ten wtyczka została przestarzała i nie będzie dłużej aktualizowana.', + fullMessage: 'Ten plugin został wycofany z użycia z powodu {{deprecatedReason}} i nie będzie już aktualizowany. Proszę użyć zamiast tego <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>.', + }, }, debugInfo: { viewDocs: 'Wyświetlanie dokumentów', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: 'Po skonfigurowaniu poświadczeń wszyscy członkowie w przestrzeni roboczej mogą korzystać z tego narzędzia podczas orkiestracji aplikacji.', clientInfo: 'Ponieważ nie znaleziono tajemnic klientów systemu dla tego dostawcy narzędzi, wymagane jest ręczne skonfigurowanie, dla redirect_uri proszę użyć', }, + deprecated: 'Nieaktualny', + autoUpdate: { + strategy: { + disabled: { + description: 'Wtyczki nie będą się automatycznie aktualizować', + name: 'Niepełnosprawny', + }, + fixOnly: { + selectedDescription: 'Automatyczna aktualizacja tylko dla wersji poprawek', + name: 'Napraw tylko', + }, + latest: { + name: 'Najświeższy', + description: 'Zawsze aktualizuj do najnowszej wersji', + selectedDescription: 'Zawsze aktualizuj do najnowszej wersji', + }, + }, + upgradeMode: { + all: 'Zaktualizuj wszystko', + partial: 'Tylko wybrane', + exclude: 'Wyłącz wybrane', + }, + upgradeModePlaceholder: { + exclude: 'Wybrane wtyczki nie będą aktualizować się automatycznie.', + partial: 'Tylko wybrane wtyczki będą się aktualizować automatycznie. Obecnie nie wybrano żadnych wtyczek, więc żadna wtyczka nie będzie się automatycznie aktualizować.', + }, + operation: { + clearAll: 'Wyczyść wszystko', + select: 'Wybierz wtyczki', + }, + pluginDowngradeWarning: { + exclude: 'Wyłącz z automatycznej aktualizacji', + downgrade: 'Zrób downgrade tak czy inaczej', + title: 'Obniżenie wersji wtyczki', + description: 'Automatyczna aktualizacja jest obecnie włączona dla tej wtyczki. Obniżenie wersji może spowodować, że twoje zmiany zostaną nadpisane podczas następnej automatycznej aktualizacji.', + }, + noPluginPlaceholder: { + noInstalled: 'Brak zainstalowanych wtyczek', + noFound: 'Nie znaleziono wtyczek', + }, + updateTime: 'Czas aktualizacji', + updateSettings: 'Zaktualizuj ustawienia', + updateTimeTitle: 'Czas aktualizacji', + specifyPluginsToUpdate: 'Określ wtyczki do zaktualizowania', + nextUpdateTime: 'Następna automatyczna aktualizacja: {{time}}', + automaticUpdates: 'Automatyczne aktualizacje', + excludeUpdate: 'Następujące {{num}} wtyczki nie będą aktualizować się automatycznie', + changeTimezone: 'Aby zmienić strefę czasową, przejdź do <setTimezone>Ustawienia</setTimezone>', + partialUPdate: 'Tylko następujące {{num}} wtyczki będą się automatycznie aktualizować', + }, } export default translation diff --git a/web/i18n/pl-PL/share-app.ts b/web/i18n/pl-PL/share.ts similarity index 100% rename from web/i18n/pl-PL/share-app.ts rename to web/i18n/pl-PL/share.ts diff --git a/web/i18n/pt-BR/plugin.ts b/web/i18n/pt-BR/plugin.ts index c03acac2ec..47490d218c 100644 --- a/web/i18n/pt-BR/plugin.ts +++ b/web/i18n/pt-BR/plugin.ts @@ -84,6 +84,16 @@ const translation = { configureTool: 'Ferramenta de configuração', endpointsDocLink: 'Veja o documento', endpointModalTitle: 'Ponto de extremidade de configuração', + deprecation: { + reason: { + businessAdjustments: 'ajustes de negócios', + ownershipTransferred: 'propriedade transferida', + noMaintainer: 'sem mantenedor', + }, + onlyReason: 'Este plugin foi descontinuado devido a {{deprecatedReason}} e não será mais atualizado.', + noReason: 'Este plugin foi descontinuado e não será mais atualizado.', + fullMessage: 'Este plugin foi descontinuado devido a {{deprecatedReason}}, e não receberá mais atualizações. Por favor, use <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> em vez disso.', + }, }, debugInfo: { title: 'Depuração', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: 'Após configurar as credenciais, todos os membros dentro do espaço de trabalho podem usar esta ferramenta ao orquestrar aplicações.', clientInfo: 'Como não foram encontrados segredos de cliente do sistema para este provedor de ferramentas, é necessário configurá-lo manualmente. Para redirect_uri, use', }, + deprecated: 'Obsoleto', + autoUpdate: { + strategy: { + disabled: { + name: 'Desativado', + description: 'Os plugins não atualizarão automaticamente', + }, + fixOnly: { + selectedDescription: 'Atualização automática apenas para versões de patch', + name: 'Reparar Apenas', + }, + latest: { + description: 'Sempre atualize para a versão mais recente', + selectedDescription: 'Sempre atualize para a versão mais recente', + name: 'Último', + }, + }, + upgradeMode: { + all: 'Atualizar tudo', + exclude: 'Excluir selecionados', + partial: 'Somente selecionado', + }, + upgradeModePlaceholder: { + exclude: 'Plugins selecionados não serão atualizados automaticamente', + partial: 'Apenas plugins selecionados serão atualizados automaticamente. Nenhum plugin está atualmente selecionado, então nenhum plugin será atualizado automaticamente.', + }, + operation: { + select: 'Selecionar plugins', + clearAll: 'Limpar tudo', + }, + pluginDowngradeWarning: { + downgrade: 'Descer de nível de qualquer forma', + exclude: 'Excluir da atualização automática', + title: 'Rebaixamento do Plugin', + description: 'A atualização automática está atualmente habilitada para este plugin. Reverter a versão pode causar a sobrescrição de suas alterações durante a próxima atualização automática.', + }, + noPluginPlaceholder: { + noFound: 'Nenhum plugin foi encontrado.', + noInstalled: 'Nenhum plugin instalado', + }, + updateTime: 'Atualizar hora', + automaticUpdates: 'Atualizações automáticas', + excludeUpdate: 'Os seguintes {{num}} plugins não serão atualizados automaticamente', + updateTimeTitle: 'Atualizar hora', + specifyPluginsToUpdate: 'Especifique os plugins a serem atualizados', + changeTimezone: 'Para mudar o fuso horário, vá para <setTimezone>Configurações</setTimezone>', + nextUpdateTime: 'Próxima atualização automática: {{time}}', + partialUPdate: 'Apenas os seguintes {{num}} plugins serão atualizados automaticamente', + updateSettings: 'Atualizar Configurações', + }, } export default translation diff --git a/web/i18n/pt-BR/share-app.ts b/web/i18n/pt-BR/share.ts similarity index 100% rename from web/i18n/pt-BR/share-app.ts rename to web/i18n/pt-BR/share.ts diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index a32b8c3c0f..791bbcbc7e 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -84,8 +84,8 @@ const translation = { advancedShortDescription: 'Flux de lucru îmbunătățit pentru conversații multi-tur', advancedUserDescription: 'Flux de lucru cu funcții suplimentare de memorie și interfață de chatbot.', noTemplateFoundTip: 'Încercați să căutați folosind cuvinte cheie diferite.', - foundResults: '{{număr}} Rezultatele', - foundResult: '{{număr}} Rezultat', + foundResults: '{{count}} Rezultatele', + foundResult: '{{count}} Rezultat', noIdeaTip: 'Nicio idee? Consultați șabloanele noastre', noAppsFound: 'Nu s-au găsit aplicații', workflowShortDescription: 'Flux agentic pentru automatizări inteligente', diff --git a/web/i18n/ro-RO/plugin.ts b/web/i18n/ro-RO/plugin.ts index b866f1de01..8c3ba06bbc 100644 --- a/web/i18n/ro-RO/plugin.ts +++ b/web/i18n/ro-RO/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointModalDesc: 'Odată configurate, pot fi utilizate funcțiile furnizate de plugin prin intermediul punctelor finale API.', modelNum: '{{num}} MODELE INCLUSE', configureModel: 'Configurarea modelului', + deprecation: { + reason: { + businessAdjustments: 'ajustări de afaceri', + noMaintainer: 'fără întreținător', + ownershipTransferred: 'proprietatea transferată', + }, + noReason: 'Acest plugin a fost declarat învechit și nu va mai fi actualizat.', + onlyReason: 'Acest plugin a fost depreciat din cauza {{deprecatedReason}} și nu va mai fi actualizat.', + fullMessage: 'Acest plugin a fost declarat învechit din cauza {{deprecatedReason}}, și nu va mai fi actualizat. Vă rugăm să folosiți în schimb <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>.', + }, }, debugInfo: { viewDocs: 'Vizualizați documentele', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: 'După configurarea acreditivelor, toți membrii din spațiul de lucru pot folosi acest instrument atunci când orchestran aplicații.', clientInfo: 'Deoarece nu s-au găsit secretele clientului sistemului pentru acest furnizor de instrumente, este necesară configurarea manuală; pentru redirect_uri, vă rugăm să folosiți', }, + deprecated: 'Încetat de a mai fi utilizat', + autoUpdate: { + strategy: { + disabled: { + description: 'Pluginurile nu se vor actualiza automat', + name: 'Dezactivat', + }, + fixOnly: { + selectedDescription: 'Actualizare automată doar pentru versiuni patch', + name: 'Fix doar', + }, + latest: { + name: 'Ultimul', + selectedDescription: 'Actualizați întotdeauna la cea mai recentă versiune', + description: 'Actualizați întotdeauna la cea mai recentă versiune', + }, + }, + upgradeMode: { + exclude: 'Excluzi selecția', + all: 'Actualizează tot', + partial: 'Numai selectat', + }, + upgradeModePlaceholder: { + exclude: 'Pluginurile selectate nu se vor actualiza automat.', + partial: 'Numai pluginurile selectate se vor actualiza automat. Nu există pluginuri selectate în prezent, așa că niciun plugin nu se va actualiza automat.', + }, + operation: { + select: 'Selectați plugin-uri', + clearAll: 'Șterge tot', + }, + pluginDowngradeWarning: { + title: 'Scădere a pluginului', + exclude: 'Exclude de la actualizarea automată', + downgrade: 'Oricum, downgradează', + description: 'Actualizarea automată este în prezent activată pentru acest plugin. Revenirea la o versiune anterioară poate provoca suprascrierea modificărilor tale în timpul următoarei actualizări automate.', + }, + noPluginPlaceholder: { + noFound: 'Nu au fost găsite plugin-uri', + noInstalled: 'Niciun plugin instalat', + }, + excludeUpdate: 'Următoarele {{num}} pluginuri nu se vor actualiza automat', + updateTimeTitle: 'Timp de actualizare', + updateSettings: 'Actualizează setările', + changeTimezone: 'Pentru a schimba fusul orar, mergi la <setTimezone>Setări</setTimezone>', + automaticUpdates: 'Actualizări automate', + specifyPluginsToUpdate: 'Specificați plugin-urile de actualizat', + partialUPdate: 'Numai următoarele {{num}} pluginuri se vor actualiza automat', + updateTime: 'Timp de actualizare', + nextUpdateTime: 'Următoarea actualizare automată: {{time}}', + }, } export default translation diff --git a/web/i18n/ro-RO/share-app.ts b/web/i18n/ro-RO/share.ts similarity index 100% rename from web/i18n/ro-RO/share-app.ts rename to web/i18n/ro-RO/share.ts diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index 16bdfd9b4a..d12f25ed57 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -78,11 +78,11 @@ const translation = { appCreateDSLErrorPart1: 'Обнаружена существенная разница в версиях DSL. Принудительный импорт может привести к сбою в работе приложения.', learnMore: 'Подробнее', forAdvanced: 'ДЛЯ ПРОДВИНУТЫХ ПОЛЬЗОВАТЕЛЕЙ', - foundResults: '{{Количество}} Результаты', + foundResults: '{{count}} Результаты', optional: 'Необязательный', chatbotShortDescription: 'Чат-бот на основе LLM с простой настройкой', advancedShortDescription: 'Рабочий процесс, улучшенный для многоходовых чатов', - foundResult: '{{Количество}} Результат', + foundResult: '{{count}} Результат', workflowShortDescription: 'Агентный поток для интеллектуальных автоматизаций', advancedUserDescription: 'Рабочий процесс с дополнительными функциями памяти и интерфейсом чат-бота.', noAppsFound: 'Приложения не найдены', diff --git a/web/i18n/ru-RU/plugin.ts b/web/i18n/ru-RU/plugin.ts index 5c96520ed9..f39139aa05 100644 --- a/web/i18n/ru-RU/plugin.ts +++ b/web/i18n/ru-RU/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointsEmpty: 'Нажмите кнопку «+», чтобы добавить конечную точку', switchVersion: 'Версия для переключателя', endpointsDocLink: 'Посмотреть документ', + deprecation: { + reason: { + businessAdjustments: 'бизнес-правки', + ownershipTransferred: 'передача права собственности', + noMaintainer: 'нет сопровождающего', + }, + noReason: 'Этот плагин был устаревшим и больше не будет обновляться.', + onlyReason: 'Этот плагин был устаревшим из-за {{deprecatedReason}} и больше не будет обновляться.', + fullMessage: 'Этот плагин больше не поддерживается по причине {{deprecatedReason}} и больше не будет обновляться. Пожалуйста, используйте <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> вместо этого.', + }, }, debugInfo: { title: 'Отладка', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: 'После настройки учетных данных все члены рабочей области могут использовать этот инструмент при оркестрации приложений.', clientInfo: 'Поскольку не найдены секреты клиентской системы для этого поставщика инструментов, необходимо настроить его вручную, для redirect_uri, пожалуйста, используйте', }, + deprecated: 'Устаревший', + autoUpdate: { + strategy: { + disabled: { + name: 'Отключен', + description: 'Плагины не будут автоматически обновляться', + }, + fixOnly: { + name: 'Только исправить', + selectedDescription: 'Автообновление только для версий патчей', + }, + latest: { + name: 'Новости', + selectedDescription: 'Всегда обновляйте до последней версии', + description: 'Всегда обновляйте до последней версии', + }, + }, + upgradeMode: { + partial: 'Только выбрано', + all: 'Обновить все', + exclude: 'Исключить выбранное', + }, + upgradeModePlaceholder: { + partial: 'Только выбранные плагины будут автоматически обновляться. В данный момент плагины не выбраны, поэтому никакие плагины не будут автоматически обновляться.', + exclude: 'Выбранные плагины не будут обновляться автоматически', + }, + operation: { + select: 'Выберите плагины', + clearAll: 'Очистить все', + }, + pluginDowngradeWarning: { + exclude: 'Исключить из автообновления', + title: 'Понижение версии плагина', + downgrade: 'Все равно понизьте версию', + description: 'Автообновление в данный момент включено для этого плагина. Понижение версии может привести к тому, что ваши изменения будут перезаписаны во время следующего автоматического обновления.', + }, + noPluginPlaceholder: { + noFound: 'Плагины не найдены', + noInstalled: 'Нет установленных плагинов', + }, + updateTimeTitle: 'Время обновления', + updateTime: 'Время обновления', + automaticUpdates: 'Автоматические обновления', + updateSettings: 'Обновить настройки', + nextUpdateTime: 'Следующее автообновление: {{time}}', + specifyPluginsToUpdate: 'Укажите плагины для обновления', + excludeUpdate: 'Следующие {{num}} плагины не будут обновляться автоматически', + partialUPdate: 'Только следующие {{num}} плагины будут обновляться автоматически', + changeTimezone: 'Чтобы изменить часовой пояс, перейдите в <setTimezone>Настройки</setTimezone>', + }, } export default translation diff --git a/web/i18n/ru-RU/share-app.ts b/web/i18n/ru-RU/share.ts similarity index 100% rename from web/i18n/ru-RU/share-app.ts rename to web/i18n/ru-RU/share.ts diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index a68b4128e1..cd6d1169a4 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -79,8 +79,8 @@ const translation = { advancedShortDescription: 'Potek dela izboljšan za večkratne pogovore', noAppsFound: 'Ni bilo najdenih aplikacij', agentShortDescription: 'Inteligentni agent z razmišljanjem in avtonomno uporabo orodij', - foundResult: '{{štetje}} Rezultat', - foundResults: '{{štetje}} Rezultati', + foundResult: '{{count}} Rezultat', + foundResults: '{{count}} Rezultati', noTemplateFoundTip: 'Poskusite iskati z različnimi ključnimi besedami.', optional: 'Neobvezno', forBeginners: 'Bolj osnovne vrste aplikacij', diff --git a/web/i18n/sl-SI/plugin.ts b/web/i18n/sl-SI/plugin.ts index cecb8e9a62..049a80f859 100644 --- a/web/i18n/sl-SI/plugin.ts +++ b/web/i18n/sl-SI/plugin.ts @@ -87,6 +87,16 @@ const translation = { endpointsTip: 'Ta vtičnik zagotavlja specifične funkcionalnosti preko končnih točk, prav tako pa lahko konfigurirate več nizov končnih točk za trenutno delovno okolje.', endpointModalDesc: 'Ko je konfiguriran, se lahko uporabljajo funkcije, ki jih vtičnik zagotavlja prek API končnih točk.', endpointsEmpty: 'Kliknite gumb \' \' za dodajanje končne točke', + deprecation: { + reason: { + businessAdjustments: 'poslovne prilagoditve', + noMaintainer: 'brez vzdrževalca', + ownershipTransferred: 'lastništvo preneseno', + }, + onlyReason: 'Ta vtičnik je bil opuščen zaradi {{deprecatedReason}} in ne bo več posodobljen.', + noReason: 'Ta vtičnik je bil ukinjen in ne bo več posodabljan.', + fullMessage: 'Ta vtičnik je bil ukinjen zaradi {{deprecatedReason}}, in ne bo več posodobljen. Namesto tega uporabite <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>.', + }, }, debugInfo: { viewDocs: 'Oglejte si dokumente', @@ -237,6 +247,56 @@ const translation = { clientInfo: 'Ker za tega ponudnika orodij niso bili najdeni klientski skrivnosti sistema, je potrebna ročna nastavitev, za redirect_uri prosimo uporabite', useApiAuthDesc: 'Po konfiguraciji poverilnic lahko vsi člani v delovnem prostoru uporabljajo to orodje pri orkestraciji aplikacij.', }, + deprecated: 'Zastaran', + autoUpdate: { + strategy: { + disabled: { + name: 'Onemogočeno', + description: 'Vtičniki se ne bodo samodejno posodobili', + }, + fixOnly: { + name: 'Popravi samo', + selectedDescription: 'Samodejno posodabljanje samo za različice popravkov', + }, + latest: { + selectedDescription: 'Vedno posodobite na najnovejšo različico', + name: 'Najnovejši', + description: 'Vedno posodobite na najnovejšo različico', + }, + }, + upgradeMode: { + partial: 'Samo izbrano', + exclude: 'Izključi izbrano', + all: 'Posodobi vse', + }, + upgradeModePlaceholder: { + exclude: 'Izbrani vtičniki se ne bodo samodejno posodabljali.', + partial: 'Samo izbrani vtičniki se bodo samodejno posodabljali. Trenutno ni izbranih nobenih vtičnikov, zato se nobeni vtičniki ne bodo samodejno posodobili.', + }, + operation: { + select: 'Izberi vtičnike', + clearAll: 'Počisti vse', + }, + pluginDowngradeWarning: { + downgrade: 'Kljub temu narediti nižjo različico', + exclude: 'Izključi iz samodejnega posodabljanja', + title: 'Zmanjšanje različice vtičnika', + description: 'Samodejno posodabljanje je trenutno omogočeno za ta vtičnik. Zmanjšanje različice lahko povzroči, da bodo vaše spremembe prepisane med naslednjim samodejnim posodabljanjem.', + }, + noPluginPlaceholder: { + noFound: 'Nobeni vtičniki niso bili najdeni', + noInstalled: 'Nobenih vtičnikov ni nameščenih', + }, + updateTimeTitle: 'Čas posodobitve', + specifyPluginsToUpdate: 'Določite vtičnike za posodobitev', + updateTime: 'Čas posodobitve', + nextUpdateTime: 'Naslednje samodejno posodabljanje: {{time}}', + automaticUpdates: 'Samodejna posodobitev', + excludeUpdate: 'Naslednjih {{num}} razširitev ne bo samodejno posodobljenih', + changeTimezone: 'Za spremembo časovnega pasu pojdite v <setTimezone>Nastavitve</setTimezone>', + partialUPdate: 'Samo naslednjih {{num}} vtičnikov se bo samodejno posodabljalo.', + updateSettings: 'Posodobi nastavitve', + }, } export default translation diff --git a/web/i18n/sl-SI/share-app.ts b/web/i18n/sl-SI/share.ts similarity index 100% rename from web/i18n/sl-SI/share-app.ts rename to web/i18n/sl-SI/share.ts diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index d89193bded..af2f67bcc1 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -73,7 +73,7 @@ const translation = { appCreateDSLErrorPart4: 'เวอร์ชัน DSL ที่ระบบรองรับ:', appCreateFailed: 'สร้างโปรเจกต์ไม่สําเร็จ', learnMore: 'ศึกษาเพิ่มเติม', - foundResults: '{{นับ}} ผลลัพธ์', + foundResults: '{{count}} ผลลัพธ์', noTemplateFoundTip: 'ลองค้นหาโดยใช้คีย์เวิร์ดอื่น', chatbotShortDescription: 'แชทบอทที่ใช้ LLM พร้อมการตั้งค่าที่ง่ายดาย', optional: 'เสริม', @@ -83,7 +83,7 @@ const translation = { completionShortDescription: 'ผู้ช่วย AI สําหรับงานสร้างข้อความ', agentUserDescription: 'ตัวแทนอัจฉริยะที่สามารถให้เหตุผลซ้ําๆ และใช้เครื่องมืออัตโนมัติเพื่อให้บรรลุเป้าหมายของงาน', noIdeaTip: 'ไม่มีความคิด? ดูเทมเพลตของเรา', - foundResult: '{{นับ}} ผล', + foundResult: '{{count}} ผล', noAppsFound: 'ไม่พบแอป', workflowShortDescription: 'โฟลว์อัตโนมัติสำหรับระบบอัจฉริยะ', forAdvanced: 'สําหรับผู้ใช้ขั้นสูง', diff --git a/web/i18n/th-TH/plugin.ts b/web/i18n/th-TH/plugin.ts index de1201396d..6a53350cad 100644 --- a/web/i18n/th-TH/plugin.ts +++ b/web/i18n/th-TH/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointsTip: 'ปลั๊กอินนี้มีฟังก์ชันเฉพาะผ่านปลายทาง และคุณสามารถกําหนดค่าชุดปลายทางหลายชุดสําหรับพื้นที่ทํางานปัจจุบันได้', endpointsEmpty: 'คลิกปุ่ม \'+\' เพื่อเพิ่มปลายทาง', serviceOk: 'บริการตกลง', + deprecation: { + reason: { + ownershipTransferred: 'การโอนความเป็นเจ้าของ', + businessAdjustments: 'การปรับเปลี่ยนธุรกิจ', + noMaintainer: 'ไม่มีผู้ดูแล', + }, + onlyReason: 'ปลั๊กอินนี้ถูกเลิกใช้เนื่องจาก {{deprecatedReason}} และจะไม่มีการอัปเดตอีกต่อไป.', + noReason: 'ปลั๊กอินนี้ได้ถูกยกเลิกใช้งานและจะไม่มีการอัปเดตอีกต่อไป.', + fullMessage: 'ปลั๊กอินนี้ถูกยกเลิกการใช้งานเนื่องจาก {{เหตุผลที่ถูกยกเลิก}} และจะไม่มีการอัปเดตอีกต่อไป กรุณาใช้ <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> แทน.', + }, }, debugInfo: { viewDocs: 'ดูเอกสาร', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: 'หลังจากตั้งค่าข้อมูลประจำตัวแล้ว สมาชิกทุกคนภายในพื้นที่ทำงานสามารถใช้เครื่องมือนี้เมื่อจัดการแอปพลิเคชันได้', clientInfo: 'เนื่องจากไม่พบความลับของลูกค้าสำหรับผู้ให้บริการเครื่องมือนี้ จำเป็นต้องตั้งค่าแบบแมนนวล สำหรับ redirect_uri กรุณาใช้', }, + deprecated: 'เลิกใช้', + autoUpdate: { + strategy: { + disabled: { + name: 'ผู้พิการ', + description: 'ปลั๊กอินจะไม่อัปเดตอัตโนมัติ', + }, + fixOnly: { + name: 'ซ่อมเฉพาะ', + selectedDescription: 'อัปเดตอัตโนมัติเฉพาะเวอร์ชันแพตช์เท่านั้น', + }, + latest: { + name: 'ล่าสุด', + selectedDescription: 'อัปเดตเป็นเวอร์ชันล่าสุดเสมอ', + description: 'อัปเดตเป็นเวอร์ชันล่าสุดเสมอ', + }, + }, + upgradeMode: { + partial: 'เฉพาะที่เลือกไว้', + exclude: 'ยกเว้นที่เลือกไว้', + all: 'อัปเดตทั้งหมด', + }, + upgradeModePlaceholder: { + exclude: 'ปลั๊กอินที่เลือกจะไม่อัปเดตอัตโนมัติ', + partial: 'เฉพาะปลั๊กอินที่เลือกจะอัปเดตโดยอัตโนมัติ ขณะนี้ไม่มีปลั๊กอินใดที่ถูกเลือก ดังนั้นจะไม่มีปลั๊กอินใดที่อัปเดตโดยอัตโนมัติ', + }, + operation: { + clearAll: 'ล้างทั้งหมด', + select: 'เลือกปลั๊กอิน', + }, + pluginDowngradeWarning: { + title: 'การลดเวอร์ชันปลั๊กอิน', + downgrade: 'ลดระดับอยู่ดี', + exclude: 'ไม่รวมในการอัปเดตอัตโนมัติ', + description: 'ฟีเจอร์การอัปเดตอัตโนมัติเปิดใช้งานอยู่สำหรับปลั๊กอินนี้ การลดระดับเวอร์ชันอาจทำให้การเปลี่ยนแปลงของคุณหายไปในระหว่างการอัปเดตอัตโนมัติต่อไป', + }, + noPluginPlaceholder: { + noInstalled: 'ไม่มีปลั๊กอินติดตั้ง', + noFound: 'ไม่พบปลั๊กอิน', + }, + specifyPluginsToUpdate: 'ระบุปลั๊กอินที่จะแ atualizar', + updateTime: 'เวลาที่อัปเดต', + updateTimeTitle: 'เวลาที่อัปเดต', + updateSettings: 'อัปเดตการตั้งค่า', + nextUpdateTime: 'การอัปเดตอัตโนมัติครั้งถัดไป: {{time}}', + automaticUpdates: 'การอัปเดตอัตโนมัติ', + excludeUpdate: 'ปลั๊กอิน {{num}} ต่อไปนี้จะไม่อัพเดตอัตโนมัติ', + partialUPdate: 'ปลั๊กอิน {{num}} ตัวต่อไปนี้จะอัปเดตให้อัตโนมัติเท่านั้น', + changeTimezone: 'ในการเปลี่ยนเขตเวลา ให้ไปที่ <setTimezone>การตั้งค่า</setTimezone>', + }, } export default translation diff --git a/web/i18n/th-TH/share-app.ts b/web/i18n/th-TH/share.ts similarity index 100% rename from web/i18n/th-TH/share-app.ts rename to web/i18n/th-TH/share.ts diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 73fff0f217..1847af9cf4 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -72,11 +72,11 @@ const translation = { appCreateDSLErrorPart3: 'Geçerli uygulama DSL sürümü:', appCreateDSLErrorTitle: 'Sürüm Uyumsuzluğu', Confirm: 'Onaylamak', - foundResults: '{{sayı}} Sonuç -ları', + foundResults: '{{count}} Sonuç -ları', noAppsFound: 'Uygulama bulunamadı', chatbotUserDescription: 'Basit yapılandırmayla hızlı bir şekilde LLM tabanlı bir sohbet botu oluşturun. Daha sonra Chatflow\'a geçebilirsiniz.', optional: 'Opsiyonel', - foundResult: '{{sayı}} Sonuç', + foundResult: '{{count}} Sonuç', noTemplateFound: 'Şablon bulunamadı', workflowUserDescription: 'Sürükle-bırak kolaylığıyla görsel olarak otonom yapay zeka iş akışları oluşturun.', advancedUserDescription: 'Ek bellek özellikleri ve sohbet robotu arayüzü ile iş akışı.', diff --git a/web/i18n/tr-TR/plugin.ts b/web/i18n/tr-TR/plugin.ts index 80237131e7..4c2b5510d2 100644 --- a/web/i18n/tr-TR/plugin.ts +++ b/web/i18n/tr-TR/plugin.ts @@ -84,6 +84,16 @@ const translation = { modelNum: '{{sayı}} DAHİL OLAN MODELLER', endpointDisableTip: 'Uç Noktayı Devre Dışı Bırak', serviceOk: 'Servis Tamam', + deprecation: { + reason: { + noMaintainer: 'bakımcı yok', + ownershipTransferred: 'mülkiyet devredildi', + businessAdjustments: 'iş ayarlamaları', + }, + noReason: 'Bu eklenti kullanımdan kaldırıldı ve artık güncellenmeyecek.', + onlyReason: 'Bu eklenti {{deprecatedReason}} nedeniyle kullanımdan kaldırılmıştır ve artık güncellenmeyecektir.', + fullMessage: 'Bu eklenti {{deprecatedReason}} nedeniyle kullanım dışı bırakılmıştır ve artık güncellenmeyecek. Lütfen bunun yerine <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>\'i kullanın.', + }, }, debugInfo: { title: 'Hata ayıklama', @@ -237,6 +247,56 @@ const translation = { saveAndAuth: 'Kaydet ve Yetkilendir', clientInfo: 'Bu araç sağlayıcı için sistem istemci gizlilikleri bulunmadığından, manuel olarak ayar yapılması gerekmektedir. redirect_uri için lütfen şu adresi kullanın', }, + deprecated: 'Kaldırılmış', + autoUpdate: { + strategy: { + disabled: { + name: 'Engelli', + description: 'Eklentiler otomatik olarak güncellenmeyecek', + }, + fixOnly: { + selectedDescription: 'Sadece yamanın versiyonları için otomatik güncelleme', + name: 'Sadece Düzelt', + }, + latest: { + name: 'Son', + selectedDescription: 'Her zaman en son sürüme güncelle', + description: 'Her zaman en son sürüme güncelle', + }, + }, + upgradeMode: { + partial: 'Sadece seçilen', + all: 'Hepsini güncelle', + exclude: 'Seçilenleri hariç tut', + }, + upgradeModePlaceholder: { + exclude: 'Seçilen eklentiler otomatik olarak güncellenmeyecek.', + partial: 'Sadece seçilen eklentiler otomatik olarak güncellenecek. Şu anda hiçbir eklenti seçilmedi, bu yüzden hiçbir eklenti otomatik olarak güncellenmeyecek.', + }, + operation: { + select: 'Eklentileri seçin', + clearAll: 'Hepsini temizle', + }, + pluginDowngradeWarning: { + downgrade: 'Her durumda düşürme', + title: 'Eklenti Düşürme', + exclude: 'Otomatik güncellemeden hariç tut', + description: 'Bu eklenti için otomatik güncelleme şu anda etkin. Sürümün düşürülmesi, bir sonraki otomatik güncelleme sırasında değişikliklerinizin üzerine yazılmasına neden olabilir.', + }, + noPluginPlaceholder: { + noInstalled: 'Hiçbir eklenti yüklenmemiş', + noFound: 'Hiçbir eklenti bulunamadı', + }, + automaticUpdates: 'Otomatik güncellemeler', + updateTime: 'Güncelleme zamanı', + updateTimeTitle: 'Güncelleme zamanı', + updateSettings: 'Ayarları Güncelle', + nextUpdateTime: 'Sonraki otomatik güncelleme: {{time}}', + specifyPluginsToUpdate: 'Güncellemek için eklentileri belirtin', + excludeUpdate: 'Aşağıdaki {{num}} eklenti otomatik olarak güncellenmeyecek', + changeTimezone: 'Zaman dilimini değiştirmek için <setTimezone>Ayarlar</setTimezone> sekmesine gidin', + partialUPdate: 'Sadece aşağıdaki {{num}} eklenti otomatik olarak güncellenecek', + }, } export default translation diff --git a/web/i18n/tr-TR/share-app.ts b/web/i18n/tr-TR/share.ts similarity index 100% rename from web/i18n/tr-TR/share-app.ts rename to web/i18n/tr-TR/share.ts diff --git a/web/i18n/uk-UA/plugin.ts b/web/i18n/uk-UA/plugin.ts index 7273f1174e..877d7843ff 100644 --- a/web/i18n/uk-UA/plugin.ts +++ b/web/i18n/uk-UA/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointModalDesc: 'Після налаштування можна використовувати функції, що надаються плагіном через кінцеві точки API.', configureTool: 'Інструмент налаштування', serviceOk: 'Сервіс працює', + deprecation: { + reason: { + ownershipTransferred: 'право власності передано', + businessAdjustments: 'бізнесові корективи', + noMaintainer: 'немає супроводжувача', + }, + noReason: 'Цей плагін було застаріло, і він більше не буде оновлюватися.', + onlyReason: 'Цей плагін було знято з підтримки через {{deprecatedReason}} і більше не буде оновлюватися.', + fullMessage: 'Цей плагін був застарілий через {{deprecatedReason}}, і більше не буде оновлюватися. Будь ласка, використовуйте <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> замість цього.', + }, }, debugInfo: { title: 'Налагодження', @@ -237,6 +247,56 @@ const translation = { clientInfo: 'Оскільки не знайдено жодних секретів клієнта системи для цього постачальника інструментів, потрібно налаштувати його вручну; для redirect_uri, будь ласка, використовуйте', useApiAuthDesc: 'Після налаштування облікових даних усі учасники робочого простору можуть використовувати цей інструмент під час оркестрації додатків.', }, + deprecated: 'Застарілий', + autoUpdate: { + strategy: { + disabled: { + name: 'Вимкнено', + description: 'Плагіни не будуть автоматично оновлюватися', + }, + fixOnly: { + name: 'Виправити тільки', + selectedDescription: 'Автоматичне оновлення лише для версій патчів', + }, + latest: { + name: 'Останні', + selectedDescription: 'Завжди оновлюйте до останньої версії', + description: 'Завжди оновлюйте до останньої версії', + }, + }, + upgradeMode: { + all: 'Оновити все', + partial: 'Тільки вибрані', + exclude: 'Виключити вибране', + }, + upgradeModePlaceholder: { + exclude: 'Вибрані плагіни не будуть оновлюватися автоматично', + partial: 'Тільки вибрані плагіни будуть автоматично оновлюватись. Наразі жоден з плагінів не вибрано, тому жоден плагін не буде автоматично оновлений.', + }, + operation: { + clearAll: 'Очистити все', + select: 'Виберіть плагіни', + }, + pluginDowngradeWarning: { + downgrade: 'Все одно знизити версію', + title: 'Пониження плагіна', + exclude: 'Виключити з автоматичного оновлення', + description: 'Автоматичне оновлення наразі увімкнене для цього плагіна. Пониження версії може призвести до того, що ваші зміни будуть перезаписані під час наступного автоматичного оновлення.', + }, + noPluginPlaceholder: { + noFound: 'Плагіни не були знайдені', + noInstalled: 'Жодних плагінів не встановлено', + }, + updateTime: 'Час оновлення', + automaticUpdates: 'Автоматичні оновлення', + updateTimeTitle: 'Час оновлення', + nextUpdateTime: 'Наступне автоматичне оновлення: {{time}}', + specifyPluginsToUpdate: 'Вкажіть плагіни для оновлення', + excludeUpdate: 'Наступні {{num}} плагіни не будуть автоматично оновлюватися', + updateSettings: 'Оновити налаштування', + changeTimezone: 'Щоб змінити часовий пояс, перейдіть до <setTimezone>Налаштування</setTimezone>', + partialUPdate: 'Тільки наступні {{num}} плагіни будуть автоматично оновлюватися', + }, } export default translation diff --git a/web/i18n/uk-UA/share-app.ts b/web/i18n/uk-UA/share.ts similarity index 100% rename from web/i18n/uk-UA/share-app.ts rename to web/i18n/uk-UA/share.ts diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index c3b5ed96b8..4100b52b36 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -80,13 +80,13 @@ const translation = { optional: 'Tùy chọn', advancedShortDescription: 'Quy trình làm việc cho các cuộc đối thoại nhiều lượt phức tạp với bộ nhớ', workflowUserDescription: 'Xây dựng trực quan quy trình AI tự động bằng kéo thả đơn giản.', - foundResults: '{{đếm}} Kết quả', + foundResults: '{{count}} Kết quả', chatbotUserDescription: 'Nhanh chóng xây dựng chatbot dựa trên LLM với cấu hình đơn giản. Bạn có thể chuyển sang Chatflow sau.', agentUserDescription: 'Một tác nhân thông minh có khả năng suy luận lặp đi lặp lại và sử dụng công cụ tự động để đạt được mục tiêu nhiệm vụ.', noIdeaTip: 'Không có ý tưởng? Kiểm tra các mẫu của chúng tôi', advancedUserDescription: 'Quy trình với tính năng bộ nhớ bổ sung và giao diện chatbot.', forAdvanced: 'DÀNH CHO NGƯỜI DÙNG NÂNG CAO', - foundResult: '{{đếm}} Kết quả', + foundResult: '{{count}} Kết quả', agentShortDescription: 'Quy trình nâng cao cho hội thoại nhiều lượt', noTemplateFound: 'Không tìm thấy mẫu', noAppsFound: 'Không tìm thấy ứng dụng nào', diff --git a/web/i18n/vi-VN/plugin.ts b/web/i18n/vi-VN/plugin.ts index 143cd71a66..677d90e6a7 100644 --- a/web/i18n/vi-VN/plugin.ts +++ b/web/i18n/vi-VN/plugin.ts @@ -84,6 +84,16 @@ const translation = { endpointDeleteContent: 'Bạn có muốn xóa {{name}} không?', endpointModalTitle: 'Điểm cuối thiết lập', disabled: 'Tàn tật', + deprecation: { + reason: { + noMaintainer: 'không có người bảo trì', + ownershipTransferred: 'quyền sở hữu được chuyển nhượng', + businessAdjustments: 'điều chỉnh kinh doanh', + }, + noReason: 'Plugin này đã bị loại bỏ và sẽ không còn được cập nhật.', + onlyReason: 'Plugin này đã bị ngừng hỗ trợ do {{deprecatedReason}} và sẽ không còn được cập nhật nữa.', + fullMessage: 'Plugin này đã bị ngừng sử dụng do {{deprecatedReason}}, và sẽ không còn được cập nhật nữa. Vui lòng sử dụng <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink> thay thế.', + }, }, debugInfo: { title: 'Gỡ lỗi', @@ -237,6 +247,56 @@ const translation = { useApiAuthDesc: 'Sau khi cấu hình thông tin xác thực, tất cả các thành viên trong không gian làm việc có thể sử dụng công cụ này khi điều phối các ứng dụng.', clientInfo: 'Vì không tìm thấy bí mật khách hàng hệ thống cho nhà cung cấp công cụ này, cần thiết lập thủ công, đối với redirect_uri, vui lòng sử dụng', }, + deprecated: 'Đã bị ngưng sử dụng', + autoUpdate: { + strategy: { + disabled: { + name: 'Khuyết tật', + description: 'Các plugin sẽ không tự động cập nhật', + }, + fixOnly: { + name: 'Chỉ sửa chữa', + selectedDescription: 'Tự động cập nhật chỉ cho các phiên bản bản vá', + }, + latest: { + name: 'Mới nhất', + description: 'Luôn cập nhật lên phiên bản mới nhất', + selectedDescription: 'Luôn cập nhật lên phiên bản mới nhất', + }, + }, + upgradeMode: { + partial: 'Chỉ được chọn', + exclude: 'Loại trừ đã chọn', + all: 'Cập nhật tất cả', + }, + upgradeModePlaceholder: { + exclude: 'Các plugin được chọn sẽ không tự động cập nhật', + partial: 'Chỉ những plugin được chọn mới tự động cập nhật. Hiện tại không có plugin nào được chọn, vì vậy sẽ không có plugin nào tự động cập nhật.', + }, + operation: { + clearAll: 'Xóa tất cả', + select: 'Chọn plugin', + }, + pluginDowngradeWarning: { + exclude: 'Loại trừ khỏi cập nhật tự động', + downgrade: 'Giảm cấp vẫn vậy', + description: 'Chức năng tự động cập nhật hiện đang được bật cho plugin này. Việc hạ cấp phiên bản có thể khiến các thay đổi của bạn bị ghi đè trong lần cập nhật tự động tiếp theo.', + title: 'Hạ cấp Plugin', + }, + noPluginPlaceholder: { + noInstalled: 'Không có plugin nào được cài đặt', + noFound: 'Không tìm thấy plugin nào', + }, + updateTimeTitle: 'Thời gian cập nhật', + updateTime: 'Thời gian cập nhật', + automaticUpdates: 'Cập nhật tự động', + nextUpdateTime: 'Cập nhật tự động tiếp theo: {{time}}', + specifyPluginsToUpdate: 'Chỉ định các plugin để cập nhật', + excludeUpdate: 'Các plugin {{num}} sau đây sẽ không tự động cập nhật', + updateSettings: 'Cập nhật cài đặt', + partialUPdate: 'Chỉ có {{num}} plugin sau đây sẽ tự động cập nhật', + changeTimezone: 'Để thay đổi múi giờ, hãy vào <setTimezone>Cài đặt</setTimezone>', + }, } export default translation diff --git a/web/i18n/vi-VN/share-app.ts b/web/i18n/vi-VN/share.ts similarity index 100% rename from web/i18n/vi-VN/share-app.ts rename to web/i18n/vi-VN/share.ts diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index 5f8f641b72..a080a26a8c 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -29,6 +29,7 @@ const translation = { searchTools: '搜索工具...', installPlugin: '安装插件', installFrom: '安装源', + deprecated: '已弃用', list: { noInstalled: '无已安装的插件', notFound: '未找到插件', @@ -99,6 +100,16 @@ const translation = { configureApp: '应用设置', configureModel: '模型设置', configureTool: '工具设置', + deprecation: { + fullMessage: '由于{{deprecatedReason}},此插件已被弃用,将不再发布新版本。请使用<CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>替代。', + onlyReason: '由于{{deprecatedReason}},此插件已被弃用,将不再发布新版本。', + noReason: '此插件已被弃用,将不再发布新版本。', + reason: { + businessAdjustments: '业务调整', + ownershipTransferred: '所有权转移', + noMaintainer: '无人维护', + }, + }, }, install: '{{num}} 次安装', installAction: '安装', @@ -114,6 +125,56 @@ const translation = { admins: '管理员', noone: '无人', }, + autoUpdate: { + automaticUpdates: '自动更新', + updateTime: '更新时间', + specifyPluginsToUpdate: '指定要更新的插件', + strategy: { + disabled: { + name: '禁用', + description: '插件将不会自动更新', + }, + fixOnly: { + name: '仅修复', + description: '仅自动更新补丁版本(例如,1.0.1 → 1.0.2)。次要版本更改不会触发更新。', + selectedDescription: '仅自动更新补丁版本', + }, + latest: { + name: '最新', + description: '始终更新到最新版本', + selectedDescription: '始终更新到最新版本', + }, + }, + updateTimeTitle: '更新时间', + upgradeMode: { + all: '更新全部', + exclude: '排除选定', + partial: '仅选定', + }, + upgradeModePlaceholder: { + exclude: '选定的插件将不会自动更新', + partial: '仅选定的插件将自动更新。目前未选择任何插件,因此不会自动更新任何插件。', + }, + excludeUpdate: '以下 {{num}} 个插件将不会自动更新', + partialUPdate: '仅以下 {{num}} 个插件将自动更新', + operation: { + clearAll: '清除所有', + select: '选择插件', + }, + nextUpdateTime: '下次自动更新时间: {{time}}', + pluginDowngradeWarning: { + title: '插件降级', + description: '此插件目前已启用自动更新。降级版本可能会导致您的更改在下次自动更新时被覆盖。', + downgrade: '仍然降级', + exclude: '从自动更新中排除', + }, + noPluginPlaceholder: { + noFound: '未找到插件', + noInstalled: '未安装插件', + }, + updateSettings: '更新设置', + changeTimezone: '要更改时区,请前往<setTimezone>设置</setTimezone>', + }, pluginInfoModal: { title: '插件信息', repository: '仓库', diff --git a/web/i18n/zh-Hans/share-app.ts b/web/i18n/zh-Hans/share.ts similarity index 100% rename from web/i18n/zh-Hans/share-app.ts rename to web/i18n/zh-Hans/share.ts diff --git a/web/i18n/zh-Hant/plugin.ts b/web/i18n/zh-Hant/plugin.ts index 938c754394..0d0e1f8782 100644 --- a/web/i18n/zh-Hant/plugin.ts +++ b/web/i18n/zh-Hant/plugin.ts @@ -84,6 +84,16 @@ const translation = { configureModel: '配置模型', endpointModalTitle: '設置終端節點', endpointsDocLink: '查看文件', + deprecation: { + reason: { + businessAdjustments: '業務調整', + ownershipTransferred: '所有權轉移', + noMaintainer: '沒有維護者', + }, + noReason: '此插件已被廢棄,將不再進行更新。', + onlyReason: '此插件因為 {{deprecatedReason}} 而被棄用,將不再更新。', + fullMessage: '由於 {{deprecatedReason}},此插件已被棄用,將不再更新。請改用 <CustomLink href=\'https://example.com/\'>{{-alternativePluginId}}</CustomLink>。', + }, }, debugInfo: { viewDocs: '查看文件', @@ -237,6 +247,56 @@ const translation = { clientInfo: '由於未找到此工具提供者的系統客戶端秘密,因此需要手動設置,對於 redirect_uri,請使用', useApiAuthDesc: '配置完憑證後,工作區內的所有成員在協調應用程式時都可以使用此工具。', }, + deprecated: '不推薦使用的', + autoUpdate: { + strategy: { + disabled: { + description: '插件將不會自動更新', + name: '殘疾的', + }, + fixOnly: { + name: '僅修理', + selectedDescription: '僅限於修補版本的自動更新', + }, + latest: { + description: '始終更新至最新版本', + name: '最新', + selectedDescription: '始終更新至最新版本', + }, + }, + upgradeMode: { + all: '更新所有', + exclude: '排除選定的', + partial: '僅選擇', + }, + upgradeModePlaceholder: { + partial: '只有選定的插件會自動更新。目前未選定任何插件,因此不會自動更新任何插件。', + exclude: '選定的插件將不會自動更新', + }, + operation: { + select: '選擇插件', + clearAll: '清除所有', + }, + pluginDowngradeWarning: { + downgrade: '無論如何降級', + title: '插件降級', + exclude: '排除自動更新', + description: '這個插件目前已啟用自動更新。降級版本可能會導致您的更改在下一次自動更新時被覆蓋。', + }, + noPluginPlaceholder: { + noInstalled: '沒有安裝插件', + noFound: '未找到任何外掛', + }, + automaticUpdates: '自動更新', + updateTime: '更新時間', + updateTimeTitle: '更新時間', + updateSettings: '更新設定', + partialUPdate: '只有以下 {{num}} 個插件將自動更新', + excludeUpdate: '以下 {{num}} 個插件將不會自動更新', + nextUpdateTime: '下次自動更新:{{time}}', + specifyPluginsToUpdate: '指定要更新的插件', + changeTimezone: '要更改時區,請前往<setTimezone>設定</setTimezone>', + }, } export default translation diff --git a/web/i18n/zh-Hant/share-app.ts b/web/i18n/zh-Hant/share.ts similarity index 100% rename from web/i18n/zh-Hant/share-app.ts rename to web/i18n/zh-Hant/share.ts diff --git a/web/package.json b/web/package.json index ee47c3692b..4bd21e6d86 100644 --- a/web/package.json +++ b/web/package.json @@ -21,8 +21,8 @@ "dev": "cross-env NODE_OPTIONS='--inspect' next dev", "build": "next build", "start": "cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && cross-env PORT=$npm_config_port HOSTNAME=$npm_config_host node .next/standalone/server.js", - "lint": "pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache", - "lint-only-show-error": "pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet", + "lint": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache", + "lint-only-show-error": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet", "fix": "next lint --fix", "eslint-fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix", "eslint-fix-only-show-error": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix --quiet", @@ -198,6 +198,7 @@ "cross-env": "^7.0.3", "eslint": "^9.20.1", "eslint-config-next": "~15.3.5", + "eslint-plugin-oxlint": "^1.6.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-sonarjs": "^3.0.2", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index efc64a42c6..40825aec01 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -516,6 +516,9 @@ importers: eslint-config-next: specifier: ~15.3.5 version: 15.3.5(eslint@9.31.0(jiti@1.21.7))(typescript@5.8.3) + eslint-plugin-oxlint: + specifier: ^1.6.0 + version: 1.6.0 eslint-plugin-react-hooks: specifier: ^5.1.0 version: 5.2.0(eslint@9.31.0(jiti@1.21.7)) @@ -4775,6 +4778,9 @@ packages: resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} engines: {node: '>=5.0.0'} + eslint-plugin-oxlint@1.6.0: + resolution: {integrity: sha512-DH5p3sCf0nIAPscl3yGnBWXXraV0bdl66hpLxvfnabvg/GzpgXf+pOCWpGK3qDb0+AIUkh1R/7A8GkOXtlj0oA==} + eslint-plugin-perfectionist@4.15.0: resolution: {integrity: sha512-pC7PgoXyDnEXe14xvRUhBII8A3zRgggKqJFx2a82fjrItDs1BSI7zdZnQtM2yQvcyod6/ujmzb7ejKPx8lZTnw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -5832,6 +5838,9 @@ packages: resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -13169,6 +13178,10 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} + eslint-plugin-oxlint@1.6.0: + dependencies: + jsonc-parser: 3.3.1 + eslint-plugin-perfectionist@4.15.0(eslint@9.31.0(jiti@1.21.7))(typescript@5.8.3): dependencies: '@typescript-eslint/types': 8.37.0 @@ -14650,6 +14663,8 @@ snapshots: espree: 9.6.1 semver: 7.7.2 + jsonc-parser@3.3.1: {} + jsonfile@6.1.0: dependencies: universalify: 2.0.1 diff --git a/web/service/common.ts b/web/service/common.ts index 99eb58e2a0..d70315f5c6 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -88,7 +88,7 @@ export const logout: Fetcher<CommonResponse, { url: string; params: Record<strin return get<CommonResponse>(url, params) } -export const fetchLanggeniusVersion: Fetcher<LangGeniusVersionResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => { +export const fetchLangGeniusVersion: Fetcher<LangGeniusVersionResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => { return get<LangGeniusVersionResponse>(url, { params }) } diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index ff092bb037..86f8134a5d 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -13,7 +13,6 @@ import type { InstalledLatestVersionResponse, InstalledPluginListWithTotalResponse, PackageDependency, - Permissions, Plugin, PluginDeclaration, PluginDetail, @@ -22,6 +21,7 @@ import type { PluginType, PluginsFromMarketplaceByInfoResponse, PluginsFromMarketplaceResponse, + ReferenceSetting, VersionInfo, VersionListResponse, uploadGitHubResponse, @@ -40,7 +40,7 @@ import { useQueryClient, } from '@tanstack/react-query' import { useInvalidateAllBuiltInTools } from './use-tools' -import usePermission from '@/app/components/plugins/plugin-page/use-permission' +import useReferenceSetting from '@/app/components/plugins/plugin-page/use-reference-setting' import { uninstallPlugin } from '@/service/plugins' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { cloneDeep } from 'lodash-es' @@ -350,37 +350,45 @@ export const useDebugKey = () => { }) } -const usePermissionsKey = [NAME_SPACE, 'permissions'] -export const usePermissions = () => { +const useReferenceSettingKey = [NAME_SPACE, 'referenceSettings'] +export const useReferenceSettings = () => { return useQuery({ - queryKey: usePermissionsKey, - queryFn: () => get<Permissions>('/workspaces/current/plugin/permission/fetch'), + queryKey: useReferenceSettingKey, + queryFn: () => get<ReferenceSetting>('/workspaces/current/plugin/preferences/fetch'), }) } -export const useInvalidatePermissions = () => { +export const useInvalidateReferenceSettings = () => { const queryClient = useQueryClient() return () => { queryClient.invalidateQueries( { - queryKey: usePermissionsKey, + queryKey: useReferenceSettingKey, }) } } -export const useMutationPermissions = ({ +export const useMutationReferenceSettings = ({ onSuccess, }: { onSuccess?: () => void }) => { return useMutation({ - mutationFn: (payload: Permissions) => { - return post('/workspaces/current/plugin/permission/change', { body: payload }) + mutationFn: (payload: ReferenceSetting) => { + return post('/workspaces/current/plugin/preferences/change', { body: payload }) }, onSuccess, }) } +export const useRemoveAutoUpgrade = () => { + return useMutation({ + mutationFn: (payload: { plugin_id: string }) => { + return post('/workspaces/current/plugin/preferences/autoupgrade/exclude', { body: payload }) + }, + }) +} + export const useMutationPluginsFromMarketplace = () => { return useMutation({ mutationFn: (pluginsSearchParams: PluginsSearchParams) => { @@ -427,6 +435,39 @@ export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[], }) } +export const useFetchPluginListOrBundleList = (pluginsSearchParams: PluginsSearchParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'fetchPluginListOrBundleList', pluginsSearchParams], + queryFn: () => { + const { + query, + sortBy, + sortOrder, + category, + tags, + exclude, + type, + page = 1, + pageSize = 40, + } = pluginsSearchParams + const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins' + return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, { + body: { + page, + page_size: pageSize, + query, + sort_by: sortBy, + sort_order: sortOrder, + category: category !== 'all' ? category : '', + tags, + exclude, + type, + }, + }) + }, + }) +} + export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[]) => { return useQuery({ queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos], @@ -448,7 +489,7 @@ const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList'] export const usePluginTaskList = (category?: PluginType) => { const { canManagement, - } = usePermission() + } = useReferenceSetting() const { refreshPluginList } = useRefreshPluginList() const { data,