Merge branch 'main' into refactor/remove-dissolve-tenant-method

# Conflicts:
#	api/services/account_service.py
pull/22690/head
zhangx1n 10 months ago
commit 8f3d2367ed

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
npm add -g pnpm@10.11.1 npm add -g pnpm@10.13.1
cd web && pnpm install cd web && pnpm install
pipx install uv 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 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 source /home/vscode/.bashrc

@ -28,7 +28,7 @@ jobs:
- name: Check changed files - name: Check changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v45 uses: tj-actions/changed-files@v46
with: with:
files: | files: |
api/** api/**
@ -75,7 +75,7 @@ jobs:
- name: Check changed files - name: Check changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v45 uses: tj-actions/changed-files@v46
with: with:
files: web/** files: web/**
@ -113,7 +113,7 @@ jobs:
- name: Check changed files - name: Check changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v45 uses: tj-actions/changed-files@v46
with: with:
files: | files: |
docker/generate_docker_compose docker/generate_docker_compose
@ -144,7 +144,7 @@ jobs:
- name: Check changed files - name: Check changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v45 uses: tj-actions/changed-files@v46
with: with:
files: | files: |
**.sh **.sh
@ -152,13 +152,15 @@ jobs:
**.yml **.yml
**Dockerfile **Dockerfile
dev/** dev/**
.editorconfig
- name: Super-linter - 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' if: steps.changed-files.outputs.any_changed == 'true'
env: env:
BASH_SEVERITY: warning BASH_SEVERITY: warning
DEFAULT_BRANCH: main DEFAULT_BRANCH: origin/main
EDITORCONFIG_FILE_NAME: editorconfig-checker.json
FILTER_REGEX_INCLUDE: pnpm-lock.yaml FILTER_REGEX_INCLUDE: pnpm-lock.yaml
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IGNORE_GENERATED_FILES: true IGNORE_GENERATED_FILES: true
@ -168,16 +170,6 @@ jobs:
# FIXME: temporarily disabled until api-docker.yaml's run script is fixed for shellcheck # FIXME: temporarily disabled until api-docker.yaml's run script is fixed for shellcheck
# VALIDATE_GITHUB_ACTIONS: true # VALIDATE_GITHUB_ACTIONS: true
VALIDATE_DOCKERFILE_HADOLINT: true VALIDATE_DOCKERFILE_HADOLINT: true
VALIDATE_EDITORCONFIG: true
VALIDATE_XML: true VALIDATE_XML: true
VALIDATE_YAML: 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

@ -27,7 +27,7 @@ jobs:
- name: Check changed files - name: Check changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v45 uses: tj-actions/changed-files@v46
with: with:
files: web/** files: web/**

@ -144,6 +144,8 @@ CONSOLE_CORS_ALLOW_ORIGINS=http://localhost:3000,*
# Vector database configuration # 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`. # 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 VECTOR_STORE=weaviate
# Prefix used to create collection name in vector database
VECTOR_INDEX_NAME_PREFIX=Vector_index
# Weaviate configuration # Weaviate configuration
WEAVIATE_ENDPOINT=http://localhost:8080 WEAVIATE_ENDPOINT=http://localhost:8080

@ -85,6 +85,11 @@ class VectorStoreConfig(BaseSettings):
default=False, 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): class KeywordStoreConfig(BaseSettings):
KEYWORD_STORE: str = Field( KEYWORD_STORE: str = Field(

@ -1,4 +1,4 @@
from datetime import UTC, datetime from datetime import datetime
import pytz # pip install pytz import pytz # pip install pytz
from flask_login import current_user from flask_login import current_user
@ -19,6 +19,7 @@ from fields.conversation_fields import (
conversation_pagination_fields, conversation_pagination_fields,
conversation_with_summary_pagination_fields, conversation_with_summary_pagination_fields,
) )
from libs.datetime_utils import naive_utc_now
from libs.helper import DatetimeString from libs.helper import DatetimeString
from libs.login import login_required from libs.login import login_required
from models import Conversation, EndUser, Message, MessageAnnotation from models import Conversation, EndUser, Message, MessageAnnotation
@ -315,7 +316,7 @@ def _get_conversation(app_model, conversation_id):
raise NotFound("Conversation Not Exists.") raise NotFound("Conversation Not Exists.")
if not conversation.read_at: 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 conversation.read_account_id = current_user.id
db.session.commit() db.session.commit()

@ -1,5 +1,3 @@
from datetime import UTC, datetime
from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, NotFound 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 controllers.console.wraps import account_initialization_required, setup_required
from extensions.ext_database import db from extensions.ext_database import db
from fields.app_fields import app_site_fields from fields.app_fields import app_site_fields
from libs.datetime_utils import naive_utc_now
from libs.login import login_required from libs.login import login_required
from models import Site from models import Site
@ -77,7 +76,7 @@ class AppSite(Resource):
setattr(site, attr_name, value) setattr(site, attr_name, value)
site.updated_by = current_user.id site.updated_by = current_user.id
site.updated_at = datetime.now(UTC).replace(tzinfo=None) site.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return site return site
@ -101,7 +100,7 @@ class AppSiteAccessTokenReset(Resource):
site.code = Site.generate_code(16) site.code = Site.generate_code(16)
site.updated_by = current_user.id site.updated_by = current_user.id
site.updated_at = datetime.now(UTC).replace(tzinfo=None) site.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return site return site

@ -1,5 +1,3 @@
import datetime
from flask import request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
@ -7,6 +5,7 @@ from constants.languages import supported_language
from controllers.console import api from controllers.console import api
from controllers.console.error import AlreadyActivateError from controllers.console.error import AlreadyActivateError
from extensions.ext_database import db 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 libs.helper import StrLen, email, extract_remote_ip, timezone
from models.account import AccountStatus from models.account import AccountStatus
from services.account_service import AccountService, RegisterService from services.account_service import AccountService, RegisterService
@ -65,7 +64,7 @@ class ActivateApi(Resource):
account.timezone = args["timezone"] account.timezone = args["timezone"]
account.interface_theme = "light" account.interface_theme = "light"
account.status = AccountStatus.ACTIVE.value 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() db.session.commit()
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))

@ -1,5 +1,4 @@
import logging import logging
from datetime import UTC, datetime
from typing import Optional from typing import Optional
import requests import requests
@ -13,6 +12,7 @@ from configs import dify_config
from constants.languages import languages from constants.languages import languages
from events.tenant_event import tenant_was_created from events.tenant_event import tenant_was_created
from extensions.ext_database import db from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from libs.helper import extract_remote_ip from libs.helper import extract_remote_ip
from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo
from models import Account from models import Account
@ -110,7 +110,7 @@ class OAuthCallback(Resource):
if account.status == AccountStatus.PENDING.value: if account.status == AccountStatus.PENDING.value:
account.status = AccountStatus.ACTIVE.value account.status = AccountStatus.ACTIVE.value
account.initialized_at = datetime.now(UTC).replace(tzinfo=None) account.initialized_at = naive_utc_now()
db.session.commit() db.session.commit()
try: try:

@ -1,4 +1,3 @@
import datetime
import json import json
from flask import request 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 core.rag.extractor.notion_extractor import NotionExtractor
from extensions.ext_database import db from extensions.ext_database import db
from fields.data_source_fields import integrate_list_fields, integrate_notion_info_list_fields 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 libs.login import login_required
from models import DataSourceOauthBinding, Document from models import DataSourceOauthBinding, Document
from services.dataset_service import DatasetService, DocumentService from services.dataset_service import DatasetService, DocumentService
@ -88,7 +88,7 @@ class DataSourceApi(Resource):
if action == "enable": if action == "enable":
if data_source_binding.disabled: if data_source_binding.disabled:
data_source_binding.disabled = False 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.add(data_source_binding)
db.session.commit() db.session.commit()
else: else:
@ -97,7 +97,7 @@ class DataSourceApi(Resource):
if action == "disable": if action == "disable":
if not data_source_binding.disabled: if not data_source_binding.disabled:
data_source_binding.disabled = True 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.add(data_source_binding)
db.session.commit() db.session.commit()
else: else:

@ -1,6 +1,5 @@
import logging import logging
from argparse import ArgumentTypeError from argparse import ArgumentTypeError
from datetime import UTC, datetime
from typing import cast from typing import cast
from flask import request from flask import request
@ -49,6 +48,7 @@ from fields.document_fields import (
document_status_fields, document_status_fields,
document_with_segments_fields, document_with_segments_fields,
) )
from libs.datetime_utils import naive_utc_now
from libs.login import login_required from libs.login import login_required
from models import Dataset, DatasetProcessRule, Document, DocumentSegment, UploadFile from models import Dataset, DatasetProcessRule, Document, DocumentSegment, UploadFile
from services.dataset_service import DatasetService, DocumentService from services.dataset_service import DatasetService, DocumentService
@ -750,7 +750,7 @@ class DocumentProcessingApi(DocumentResource):
raise InvalidActionError("Document not in indexing state.") raise InvalidActionError("Document not in indexing state.")
document.paused_by = current_user.id 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 document.is_paused = True
db.session.commit() db.session.commit()
@ -830,7 +830,7 @@ class DocumentMetadataApi(DocumentResource):
document.doc_metadata[key] = value document.doc_metadata[key] = value
document.doc_type = doc_type document.doc_type = doc_type
document.updated_at = datetime.now(UTC).replace(tzinfo=None) document.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return {"result": "success", "message": "Document metadata updated."}, 200 return {"result": "success", "message": "Document metadata updated."}, 200

@ -1,5 +1,4 @@
import logging import logging
from datetime import UTC, datetime
from flask_login import current_user from flask_login import current_user
from flask_restful import reqparse from flask_restful import reqparse
@ -27,6 +26,7 @@ from core.errors.error import (
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from extensions.ext_database import db from extensions.ext_database import db
from libs import helper from libs import helper
from libs.datetime_utils import naive_utc_now
from libs.helper import uuid_value from libs.helper import uuid_value
from models.model import AppMode from models.model import AppMode
from services.app_generate_service import AppGenerateService from services.app_generate_service import AppGenerateService
@ -51,7 +51,7 @@ class CompletionApi(InstalledAppResource):
streaming = args["response_mode"] == "streaming" streaming = args["response_mode"] == "streaming"
args["auto_generate_name"] = False 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() db.session.commit()
try: try:
@ -111,7 +111,7 @@ class ChatApi(InstalledAppResource):
args["auto_generate_name"] = False 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() db.session.commit()
try: try:

@ -1,5 +1,4 @@
import logging import logging
from datetime import UTC, datetime
from typing import Any from typing import Any
from flask import request 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 controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
from extensions.ext_database import db from extensions.ext_database import db
from fields.installed_app_fields import installed_app_list_fields 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 libs.login import login_required
from models import App, InstalledApp, RecommendedApp from models import App, InstalledApp, RecommendedApp
from services.account_service import TenantService from services.account_service import TenantService
@ -122,7 +122,7 @@ class InstalledAppsListApi(Resource):
tenant_id=current_tenant_id, tenant_id=current_tenant_id,
app_owner_tenant_id=app.tenant_id, app_owner_tenant_id=app.tenant_id,
is_pinned=False, 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.add(new_installed_app)
db.session.commit() db.session.commit()

@ -1,5 +1,3 @@
import datetime
import pytz import pytz
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
@ -35,6 +33,7 @@ from controllers.console.wraps import (
) )
from extensions.ext_database import db from extensions.ext_database import db
from fields.member_fields import account_fields 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.helper import TimestampField, email, extract_remote_ip, timezone
from libs.login import login_required from libs.login import login_required
from models import AccountIntegrate, InvitationCode from models import AccountIntegrate, InvitationCode
@ -80,7 +79,7 @@ class AccountInitApi(Resource):
raise InvalidInvitationCodeError() raise InvalidInvitationCodeError()
invitation_code.status = "used" 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_tenant_id = account.current_tenant_id
invitation_code.used_by_account_id = account.id invitation_code.used_by_account_id = account.id
@ -88,7 +87,7 @@ class AccountInitApi(Resource):
account.timezone = args["timezone"] account.timezone = args["timezone"]
account.interface_theme = "light" account.interface_theme = "light"
account.status = "active" account.status = "active"
account.initialized_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) account.initialized_at = naive_utc_now()
db.session.commit() db.session.commit()
return {"result": "success"} return {"result": "success"}

@ -29,7 +29,7 @@ from libs.login import login_required
from services.plugin.oauth_service import OAuthProxyService from services.plugin.oauth_service import OAuthProxyService
from services.tools.api_tools_manage_service import ApiToolManageService from services.tools.api_tools_manage_service import ApiToolManageService
from services.tools.builtin_tools_manage_service import BuiltinToolManageService 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.tool_labels_service import ToolLabelsService
from services.tools.tools_manage_service import ToolCommonService from services.tools.tools_manage_service import ToolCommonService
from services.tools.tools_transform_service import ToolTransformService from services.tools.tools_transform_service import ToolTransformService

@ -1,6 +1,6 @@
import time import time
from collections.abc import Callable from collections.abc import Callable
from datetime import UTC, datetime, timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from functools import wraps from functools import wraps
from typing import Optional from typing import Optional
@ -15,6 +15,7 @@ from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
from extensions.ext_database import db from extensions.ext_database import db
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
from libs.datetime_utils import naive_utc_now
from libs.login import _get_user from libs.login import _get_user
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
from models.dataset import Dataset, RateLimitLog from models.dataset import Dataset, RateLimitLog
@ -256,7 +257,7 @@ def validate_and_get_api_token(scope: str | None = None):
if auth_scheme != "bearer": if auth_scheme != "bearer":
raise Unauthorized("Authorization scheme must be '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) cutoff_time = current_time - timedelta(minutes=1)
with Session(db.engine, expire_on_commit=False) as session: with Session(db.engine, expire_on_commit=False) as session:
update_stmt = ( update_stmt = (

@ -1,7 +1,6 @@
import json import json
import logging import logging
from collections.abc import Generator from collections.abc import Generator
from datetime import UTC, datetime
from typing import Optional, Union, cast from typing import Optional, Union, cast
from core.app.app_config.entities import EasyUIBasedAppConfig, EasyUIBasedAppModelConfigFrom 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.app.task_pipeline.easy_ui_based_generate_task_pipeline import EasyUIBasedGenerateTaskPipeline
from core.prompt.utils.prompt_template_parser import PromptTemplateParser from core.prompt.utils.prompt_template_parser import PromptTemplateParser
from extensions.ext_database import db from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from models import Account from models import Account
from models.enums import CreatorUserRole from models.enums import CreatorUserRole
from models.model import App, AppMode, AppModelConfig, Conversation, EndUser, Message, MessageFile from models.model import App, AppMode, AppModelConfig, Conversation, EndUser, Message, MessageFile
@ -184,7 +184,7 @@ class MessageBasedAppGenerator(BaseAppGenerator):
db.session.commit() db.session.commit()
db.session.refresh(conversation) db.session.refresh(conversation)
else: else:
conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) conversation.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
message = Message( message = Message(

@ -8,7 +8,7 @@ from core.mcp.types import (
OAuthTokens, OAuthTokens,
) )
from models.tools import MCPToolProvider 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" LATEST_PROTOCOL_VERSION = "1.0"

@ -68,15 +68,17 @@ class MCPClient:
} }
parsed_url = urlparse(self.server_url) parsed_url = urlparse(self.server_url)
path = parsed_url.path path = parsed_url.path or ""
method_name = path.rstrip("/").split("/")[-1] if path else "" method_name = path.removesuffix("/").lower()
try: if method_name in connection_methods:
client_factory = connection_methods[method_name] client_factory = connection_methods[method_name]
self.connect_server(client_factory, method_name) self.connect_server(client_factory, method_name)
except KeyError: else:
try: try:
logger.debug(f"Not supported method {method_name} found in URL path, trying default 'mcp' method.")
self.connect_server(sse_client, "sse") self.connect_server(sse_client, "sse")
except MCPConnectionError: except MCPConnectionError:
logger.debug("MCP connection failed with 'sse', falling back to 'mcp' method.")
self.connect_server(streamablehttp_client, "mcp") self.connect_server(streamablehttp_client, "mcp")
def connect_server( def connect_server(
@ -91,7 +93,7 @@ class MCPClient:
else {} else {}
) )
self._streams_context = client_factory(url=self.server_url, headers=headers) 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") raise MCPConnectionError("Failed to create connection context")
# Use exit_stack to manage context managers properly # Use exit_stack to manage context managers properly
@ -141,10 +143,11 @@ class MCPClient:
try: try:
# ExitStack will handle proper cleanup of all managed context managers # ExitStack will handle proper cleanup of all managed context managers
self.exit_stack.close() 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 = None
self._session_context = None self._session_context = None
self._streams_context = None self._streams_context = None
self._initialized = False self._initialized = False
except Exception as e:
logging.exception("Error during cleanup")
raise ValueError(f"Error during cleanup: {e}")

@ -21,7 +21,7 @@ from core.tools.plugin_tool.tool import PluginTool
from core.tools.utils.uuid_utils import is_valid_uuid from core.tools.utils.uuid_utils import is_valid_uuid
from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from core.tools.workflow_as_tool.provider import WorkflowToolProviderController
from core.workflow.entities.variable_pool import VariablePool 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: if TYPE_CHECKING:
from core.workflow.nodes.tool.entities import ToolEntity from core.workflow.nodes.tool.entities import ToolEntity

@ -270,7 +270,14 @@ class AgentNode(BaseNode):
) )
extra = tool.get("extra", {}) 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( tool_runtime = ToolManager.get_agent_tool_runtime(
self.tenant_id, self.app_id, entity, self.invoke_from, runtime_variable_pool self.tenant_id, self.app_id, entity, self.invoke_from, runtime_variable_pool
) )

@ -13,6 +13,10 @@ class AgentNodeData(BaseNodeData):
agent_strategy_name: str agent_strategy_name: str
agent_strategy_label: str # redundancy agent_strategy_label: str # redundancy
memory: MemoryConfig | None = None 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): class AgentInput(BaseModel):
value: Union[list[str], list[ToolSelector], Any] value: Union[list[str], list[ToolSelector], Any]

@ -118,7 +118,7 @@ class KnowledgeRetrievalNodeData(BaseNodeData):
multiple_retrieval_config: Optional[MultipleRetrievalConfig] = None multiple_retrieval_config: Optional[MultipleRetrievalConfig] = None
single_retrieval_config: Optional[SingleRetrievalConfig] = None single_retrieval_config: Optional[SingleRetrievalConfig] = None
metadata_filtering_mode: Optional[Literal["disabled", "automatic", "manual"]] = "disabled" 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 metadata_filtering_conditions: Optional[MetadataFilteringCondition] = None
vision: VisionConfig = Field(default_factory=VisionConfig) vision: VisionConfig = Field(default_factory=VisionConfig)

@ -509,6 +509,8 @@ class KnowledgeRetrievalNode(BaseNode):
# get all metadata field # get all metadata field
metadata_fields = db.session.query(DatasetMetadata).filter(DatasetMetadata.dataset_id.in_(dataset_ids)).all() 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] 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 # get metadata model instance and fetch model config
model_instance, model_config = self.get_model_config(node_data.metadata_model_config) model_instance, model_config = self.get_model_config(node_data.metadata_model_config)
# fetch prompt messages # fetch prompt messages
@ -701,7 +703,7 @@ class KnowledgeRetrievalNode(BaseNode):
) )
def _get_prompt_template(self, node_data: KnowledgeRetrievalNodeData, metadata_fields: list, query: str): 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 input_text = query
prompt_messages: list[LLMNodeChatModelMessage] = [] prompt_messages: list[LLMNodeChatModelMessage] = []

@ -73,6 +73,9 @@ NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = {
}, },
NodeType.TOOL: { NodeType.TOOL: {
LATEST_VERSION: ToolNode, 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, "2": ToolNode,
"1": ToolNode, "1": ToolNode,
}, },
@ -123,6 +126,9 @@ NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = {
}, },
NodeType.AGENT: { NodeType.AGENT: {
LATEST_VERSION: AgentNode, 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, "2": AgentNode,
"1": AgentNode, "1": AgentNode,
}, },

@ -59,6 +59,10 @@ class ToolNodeData(BaseNodeData, ToolEntity):
return typ return typ
tool_parameters: dict[str, ToolInput] 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") @field_validator("tool_parameters", mode="before")
@classmethod @classmethod

@ -70,7 +70,13 @@ class ToolNode(BaseNode):
try: try:
from core.tools.tool_manager import ToolManager 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( tool_runtime = ToolManager.get_workflow_tool_runtime(
self.tenant_id, self.app_id, self.node_id, self._node_data, self.invoke_from, variable_pool self.tenant_id, self.app_id, self.node_id, self._node_data, self.invoke_from, variable_pool
) )

@ -1,6 +1,6 @@
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime from datetime import datetime
from typing import Any, Optional, Union from typing import Any, Optional, Union
from uuid import uuid4 from uuid import uuid4
@ -71,7 +71,7 @@ class WorkflowCycleManager:
workflow_version=self._workflow_info.version, workflow_version=self._workflow_info.version,
graph=self._workflow_info.graph_data, graph=self._workflow_info.graph_data,
inputs=inputs, inputs=inputs,
started_at=datetime.now(UTC).replace(tzinfo=None), started_at=naive_utc_now(),
) )
return self._save_and_cache_workflow_execution(execution) return self._save_and_cache_workflow_execution(execution)
@ -356,7 +356,7 @@ class WorkflowCycleManager:
created_at: Optional[datetime] = None, created_at: Optional[datetime] = None,
) -> WorkflowNodeExecution: ) -> WorkflowNodeExecution:
"""Create a node execution from an event.""" """Create a node execution from an event."""
now = datetime.now(UTC).replace(tzinfo=None) now = naive_utc_now()
created_at = created_at or now created_at = created_at or now
metadata = { metadata = {
@ -403,7 +403,7 @@ class WorkflowCycleManager:
handle_special_values: bool = False, handle_special_values: bool = False,
) -> None: ) -> None:
"""Update node execution with completion data.""" """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() elapsed_time = (finished_at - event.start_at).total_seconds()
# Process data # Process data

@ -1,4 +1,3 @@
import datetime
import logging import logging
import time import time
@ -8,6 +7,7 @@ from werkzeug.exceptions import NotFound
from core.indexing_runner import DocumentIsPausedError, IndexingRunner from core.indexing_runner import DocumentIsPausedError, IndexingRunner
from events.event_handlers.document_index_event import document_index_created from events.event_handlers.document_index_event import document_index_created
from extensions.ext_database import db from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from models.dataset import Document from models.dataset import Document
@ -33,7 +33,7 @@ def handle(sender, **kwargs):
raise NotFound("Document not found") raise NotFound("Document not found")
document.indexing_status = "parsing" 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) documents.append(document)
db.session.add(document) db.session.add(document)
db.session.commit() db.session.commit()

@ -1,5 +1,5 @@
from collections.abc import Generator from collections.abc import Generator
from datetime import UTC, datetime, timedelta from datetime import timedelta
from typing import Optional from typing import Optional
from azure.identity import ChainedTokenCredential, DefaultAzureCredential from azure.identity import ChainedTokenCredential, DefaultAzureCredential
@ -8,6 +8,7 @@ from azure.storage.blob import AccountSasPermissions, BlobServiceClient, Resourc
from configs import dify_config from configs import dify_config
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
from extensions.storage.base_storage import BaseStorage from extensions.storage.base_storage import BaseStorage
from libs.datetime_utils import naive_utc_now
class AzureBlobStorage(BaseStorage): class AzureBlobStorage(BaseStorage):
@ -78,7 +79,7 @@ class AzureBlobStorage(BaseStorage):
account_key=self.account_key or "", account_key=self.account_key or "",
resource_types=ResourceTypes(service=True, container=True, object=True), resource_types=ResourceTypes(service=True, container=True, object=True),
permission=AccountSasPermissions(read=True, write=True, delete=True, list=True, add=True, create=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) redis_client.set(cache_key, sas_token, ex=3000)
return BlobServiceClient(account_url=self.account_url or "", credential=sas_token) return BlobServiceClient(account_url=self.account_url or "", credential=sas_token)

@ -1,4 +1,3 @@
import datetime
import urllib.parse import urllib.parse
from typing import Any from typing import Any
@ -6,6 +5,7 @@ import requests
from flask_login import current_user from flask_login import current_user
from extensions.ext_database import db from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from models.source import DataSourceOauthBinding from models.source import DataSourceOauthBinding
@ -75,7 +75,7 @@ class NotionOAuth(OAuthDataSource):
if data_source_binding: if data_source_binding:
data_source_binding.source_info = source_info data_source_binding.source_info = source_info
data_source_binding.disabled = False 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() db.session.commit()
else: else:
new_data_source_binding = DataSourceOauthBinding( new_data_source_binding = DataSourceOauthBinding(
@ -115,7 +115,7 @@ class NotionOAuth(OAuthDataSource):
if data_source_binding: if data_source_binding:
data_source_binding.source_info = source_info data_source_binding.source_info = source_info
data_source_binding.disabled = False 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() db.session.commit()
else: else:
new_data_source_binding = DataSourceOauthBinding( new_data_source_binding = DataSourceOauthBinding(
@ -154,7 +154,7 @@ class NotionOAuth(OAuthDataSource):
} }
data_source_binding.source_info = new_source_info data_source_binding.source_info = new_source_info
data_source_binding.disabled = False 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() db.session.commit()
else: else:
raise ValueError("Data source binding not found") raise ValueError("Data source binding not found")

@ -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 ###

@ -255,7 +255,7 @@ class Dataset(Base):
@staticmethod @staticmethod
def gen_collection_name_by_id(dataset_id: str) -> str: def gen_collection_name_by_id(dataset_id: str) -> str:
normalized_dataset_id = dataset_id.replace("-", "_") 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): class DatasetProcessRule(Base):

@ -1,7 +1,6 @@
from datetime import UTC, datetime
from celery import states # type: ignore from celery import states # type: ignore
from libs.datetime_utils import naive_utc_now
from models.base import Base from models.base import Base
from .engine import db from .engine import db
@ -18,8 +17,8 @@ class CeleryTask(Base):
result = db.Column(db.PickleType, nullable=True) result = db.Column(db.PickleType, nullable=True)
date_done = db.Column( date_done = db.Column(
db.DateTime, db.DateTime,
default=lambda: datetime.now(UTC).replace(tzinfo=None), default=lambda: naive_utc_now(),
onupdate=lambda: datetime.now(UTC).replace(tzinfo=None), onupdate=lambda: naive_utc_now(),
nullable=True, nullable=True,
) )
traceback = db.Column(db.Text, nullable=True) traceback = db.Column(db.Text, nullable=True)
@ -39,4 +38,4 @@ class CeleryTaskSet(Base):
id = db.Column(db.Integer, db.Sequence("taskset_id_sequence"), autoincrement=True, primary_key=True) id = db.Column(db.Integer, db.Sequence("taskset_id_sequence"), autoincrement=True, primary_key=True)
taskset_id = db.Column(db.String(155), unique=True) taskset_id = db.Column(db.String(155), unique=True)
result = db.Column(db.PickleType, nullable=True) result = db.Column(db.PickleType, nullable=True)
date_done = db.Column(db.DateTime, default=lambda: datetime.now(UTC).replace(tzinfo=None), nullable=True) date_done = db.Column(db.DateTime, default=lambda: naive_utc_now(), nullable=True)

@ -254,7 +254,7 @@ class MCPToolProvider(Base):
# name of the mcp provider # name of the mcp provider
name: Mapped[str] = mapped_column(db.String(40), nullable=False) name: Mapped[str] = mapped_column(db.String(40), nullable=False)
# server identifier of the mcp provider # 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 # encrypted url of the mcp provider
server_url: Mapped[str] = mapped_column(db.Text, nullable=False) server_url: Mapped[str] = mapped_column(db.Text, nullable=False)
# hash of server_url for uniqueness check # hash of server_url for uniqueness check
@ -358,7 +358,7 @@ class ToolModelInvoke(Base):
# type # type
tool_type = db.Column(db.String(40), nullable=False) tool_type = db.Column(db.String(40), nullable=False)
# tool name # tool name
tool_name = db.Column(db.String(40), nullable=False) tool_name = db.Column(db.String(128), nullable=False)
# invoke parameters # invoke parameters
model_parameters = db.Column(db.Text, nullable=False) model_parameters = db.Column(db.Text, nullable=False)
# prompt messages # prompt messages

@ -1,7 +1,7 @@
import json import json
import logging import logging
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from datetime import UTC, datetime from datetime import datetime
from enum import Enum, StrEnum from enum import Enum, StrEnum
from typing import TYPE_CHECKING, Any, Optional, Union from typing import TYPE_CHECKING, Any, Optional, Union
from uuid import uuid4 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.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
from core.workflow.nodes.enums import NodeType from core.workflow.nodes.enums import NodeType
from factories.variable_factory import TypeMismatchError, build_segment_with_type 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 libs.helper import extract_tenant_id
from ._workflow_exc import NodeNotFoundError, WorkflowDataError from ._workflow_exc import NodeNotFoundError, WorkflowDataError
@ -138,7 +139,7 @@ class Workflow(Base):
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(
db.DateTime, db.DateTime,
nullable=False, nullable=False,
default=datetime.now(UTC).replace(tzinfo=None), default=naive_utc_now(),
server_onupdate=func.current_timestamp(), server_onupdate=func.current_timestamp(),
) )
_environment_variables: Mapped[str] = mapped_column( _environment_variables: Mapped[str] = mapped_column(
@ -179,7 +180,7 @@ class Workflow(Base):
workflow.conversation_variables = conversation_variables or [] workflow.conversation_variables = conversation_variables or []
workflow.marked_name = marked_name workflow.marked_name = marked_name
workflow.marked_comment = marked_comment 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 workflow.updated_at = workflow.created_at
return workflow return workflow
@ -907,7 +908,7 @@ _EDITABLE_SYSTEM_VARIABLE = frozenset(["query", "files"])
def _naive_utc_datetime(): def _naive_utc_datetime():
return datetime.now(UTC).replace(tzinfo=None) return naive_utc_now()
class WorkflowDraftVariable(Base): class WorkflowDraftVariable(Base):

@ -17,6 +17,7 @@ from constants.languages import language_timezone_mapping, languages
from events.tenant_event import tenant_was_created from events.tenant_event import tenant_was_created
from extensions.ext_database import db from extensions.ext_database import db
from extensions.ext_redis import redis_client, redis_fallback 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.helper import RateLimiter, TokenManager
from libs.passport import PassportService from libs.passport import PassportService
from libs.password import compare_password, hash_password, valid_password from libs.password import compare_password, hash_password, valid_password
@ -135,8 +136,8 @@ class AccountService:
available_ta.current = True available_ta.current = True
db.session.commit() db.session.commit()
if datetime.now(UTC).replace(tzinfo=None) - account.last_active_at > timedelta(minutes=10): if naive_utc_now() - account.last_active_at > timedelta(minutes=10):
account.last_active_at = datetime.now(UTC).replace(tzinfo=None) account.last_active_at = naive_utc_now()
db.session.commit() db.session.commit()
return cast(Account, account) return cast(Account, account)
@ -180,7 +181,7 @@ class AccountService:
if account.status == AccountStatus.PENDING.value: if account.status == AccountStatus.PENDING.value:
account.status = AccountStatus.ACTIVE.value account.status = AccountStatus.ACTIVE.value
account.initialized_at = datetime.now(UTC).replace(tzinfo=None) account.initialized_at = naive_utc_now()
db.session.commit() db.session.commit()
@ -318,7 +319,7 @@ class AccountService:
# If it exists, update the record # If it exists, update the record
account_integrate.open_id = open_id account_integrate.open_id = open_id
account_integrate.encrypted_token = "" # todo account_integrate.encrypted_token = "" # todo
account_integrate.updated_at = datetime.now(UTC).replace(tzinfo=None) account_integrate.updated_at = naive_utc_now()
else: else:
# If it does not exist, create a new record # If it does not exist, create a new record
account_integrate = AccountIntegrate( account_integrate = AccountIntegrate(
@ -353,7 +354,7 @@ class AccountService:
@staticmethod @staticmethod
def update_login_info(account: Account, *, ip_address: str) -> None: def update_login_info(account: Account, *, ip_address: str) -> None:
"""Update last login time and ip""" """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 account.last_login_ip = ip_address
db.session.add(account) db.session.add(account)
db.session.commit() db.session.commit()
@ -1066,6 +1067,15 @@ class TenantService:
target_member_join.role = new_role target_member_join.role = new_role
db.session.commit() db.session.commit()
@staticmethod
def dissolve_tenant(tenant: Tenant, operator: Account) -> None:
"""Dissolve tenant"""
TenantService.check_member_permission(tenant, operator, None, "remove")
db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id).delete()
db.session.delete(tenant)
db.session.commit()
@staticmethod @staticmethod
def get_custom_config(tenant_id: str) -> dict: def get_custom_config(tenant_id: str) -> dict:
tenant = db.get_or_404(Tenant, tenant_id) tenant = db.get_or_404(Tenant, tenant_id)
@ -1108,7 +1118,7 @@ class RegisterService:
) )
account.last_login_ip = ip_address 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) TenantService.create_owner_tenant_if_not_exist(account=account, is_setup=True)
@ -1149,7 +1159,7 @@ class RegisterService:
is_setup=is_setup, is_setup=is_setup,
) )
account.status = AccountStatus.ACTIVE.value if not status else status.value 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: if open_id is not None and provider is not None:
AccountService.link_account_integrate(provider, open_id, account) AccountService.link_account_integrate(provider, open_id, account)

@ -1,6 +1,5 @@
import json import json
import logging import logging
from datetime import UTC, datetime
from typing import Optional, cast from typing import Optional, cast
from flask_login import current_user from flask_login import current_user
@ -17,6 +16,7 @@ from core.tools.tool_manager import ToolManager
from core.tools.utils.configuration import ToolParameterConfigurationManager from core.tools.utils.configuration import ToolParameterConfigurationManager
from events.app_event import app_was_created from events.app_event import app_was_created
from extensions.ext_database import db from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from models.account import Account from models.account import Account
from models.model import App, AppMode, AppModelConfig, Site from models.model import App, AppMode, AppModelConfig, Site
from models.tools import ApiToolProvider from models.tools import ApiToolProvider
@ -235,7 +235,7 @@ class AppService:
app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False) app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False)
app.max_active_requests = args.get("max_active_requests") app.max_active_requests = args.get("max_active_requests")
app.updated_by = current_user.id app.updated_by = current_user.id
app.updated_at = datetime.now(UTC).replace(tzinfo=None) app.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return app return app
@ -249,7 +249,7 @@ class AppService:
""" """
app.name = name app.name = name
app.updated_by = current_user.id app.updated_by = current_user.id
app.updated_at = datetime.now(UTC).replace(tzinfo=None) app.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return app return app
@ -265,7 +265,7 @@ class AppService:
app.icon = icon app.icon = icon
app.icon_background = icon_background app.icon_background = icon_background
app.updated_by = current_user.id app.updated_by = current_user.id
app.updated_at = datetime.now(UTC).replace(tzinfo=None) app.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return app return app
@ -282,7 +282,7 @@ class AppService:
app.enable_site = enable_site app.enable_site = enable_site
app.updated_by = current_user.id app.updated_by = current_user.id
app.updated_at = datetime.now(UTC).replace(tzinfo=None) app.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return app return app
@ -299,7 +299,7 @@ class AppService:
app.enable_api = enable_api app.enable_api = enable_api
app.updated_by = current_user.id app.updated_by = current_user.id
app.updated_at = datetime.now(UTC).replace(tzinfo=None) app.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return app return app

@ -1,5 +1,4 @@
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from datetime import UTC, datetime
from typing import Optional, Union from typing import Optional, Union
from sqlalchemy import asc, desc, func, or_, select 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.app.entities.app_invoke_entities import InvokeFrom
from core.llm_generator.llm_generator import LLMGenerator from core.llm_generator.llm_generator import LLMGenerator
from extensions.ext_database import db from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from libs.infinite_scroll_pagination import InfiniteScrollPagination from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models import ConversationVariable from models import ConversationVariable
from models.account import Account from models.account import Account
@ -113,7 +113,7 @@ class ConversationService:
return cls.auto_generate_name(app_model, conversation) return cls.auto_generate_name(app_model, conversation)
else: else:
conversation.name = name conversation.name = name
conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) conversation.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
return conversation return conversation
@ -169,7 +169,7 @@ class ConversationService:
conversation = cls.get_conversation(app_model, conversation_id, user) conversation = cls.get_conversation(app_model, conversation_id, user)
conversation.is_deleted = True conversation.is_deleted = True
conversation.updated_at = datetime.now(UTC).replace(tzinfo=None) conversation.updated_at = naive_utc_now()
db.session.commit() db.session.commit()
@classmethod @classmethod

@ -26,6 +26,7 @@ from events.document_event import document_was_deleted
from extensions.ext_database import db from extensions.ext_database import db
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
from libs import helper from libs import helper
from libs.datetime_utils import naive_utc_now
from models.account import Account, TenantAccountRole from models.account import Account, TenantAccountRole
from models.dataset import ( from models.dataset import (
AppDatasetJoin, AppDatasetJoin,
@ -428,7 +429,7 @@ class DatasetService:
# Add metadata fields # Add metadata fields
filtered_data["updated_by"] = user.id 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 # update Retrieval model
filtered_data["retrieval_model"] = data["retrieval_model"] filtered_data["retrieval_model"] = data["retrieval_model"]
@ -994,7 +995,7 @@ class DocumentService:
# update document to be paused # update document to be paused
document.is_paused = True document.is_paused = True
document.paused_by = current_user.id 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.add(document)
db.session.commit() db.session.commit()

@ -1,6 +1,5 @@
import json import json
from copy import deepcopy from copy import deepcopy
from datetime import UTC, datetime
from typing import Any, Optional, Union, cast from typing import Any, Optional, Union, cast
from urllib.parse import urlparse from urllib.parse import urlparse
@ -11,6 +10,7 @@ from constants import HIDDEN_VALUE
from core.helper import ssrf_proxy from core.helper import ssrf_proxy
from core.rag.entities.metadata_entities import MetadataCondition from core.rag.entities.metadata_entities import MetadataCondition
from extensions.ext_database import db from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from models.dataset import ( from models.dataset import (
Dataset, Dataset,
ExternalKnowledgeApis, ExternalKnowledgeApis,
@ -120,7 +120,7 @@ class ExternalDatasetService:
external_knowledge_api.description = args.get("description", "") external_knowledge_api.description = args.get("description", "")
external_knowledge_api.settings = json.dumps(args.get("settings"), ensure_ascii=False) external_knowledge_api.settings = json.dumps(args.get("settings"), ensure_ascii=False)
external_knowledge_api.updated_by = user_id 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() db.session.commit()
return external_knowledge_api return external_knowledge_api

@ -70,16 +70,15 @@ class MCPToolManageService:
MCPToolProvider.server_url_hash == server_url_hash, MCPToolProvider.server_url_hash == server_url_hash,
MCPToolProvider.server_identifier == server_identifier, MCPToolProvider.server_identifier == server_identifier,
), ),
MCPToolProvider.tenant_id == tenant_id,
) )
.first() .first()
) )
if existing_provider: if existing_provider:
if existing_provider.name == name: if existing_provider.name == name:
raise ValueError(f"MCP tool {name} already exists") 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") 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") raise ValueError(f"MCP tool {server_identifier} already exists")
encrypted_server_url = encrypter.encrypt_token(tenant_id, server_url) encrypted_server_url = encrypter.encrypt_token(tenant_id, server_url)
mcp_tool = MCPToolProvider( mcp_tool = MCPToolProvider(
@ -111,15 +110,14 @@ class MCPToolManageService:
] ]
@classmethod @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) mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id)
try: try:
with MCPClient( with MCPClient(
mcp_provider.decrypted_server_url, provider_id, tenant_id, authed=mcp_provider.authed, for_list=True mcp_provider.decrypted_server_url, provider_id, tenant_id, authed=mcp_provider.authed, for_list=True
) as mcp_client: ) as mcp_client:
tools = mcp_client.list_tools() tools = mcp_client.list_tools()
except MCPAuthError as e: except MCPAuthError:
raise ValueError("Please auth the tool first") raise ValueError("Please auth the tool first")
except MCPError as e: except MCPError as e:
raise ValueError(f"Failed to connect to MCP server: {e}") raise ValueError(f"Failed to connect to MCP server: {e}")
@ -184,12 +182,11 @@ class MCPToolManageService:
error_msg = str(e.orig) error_msg = str(e.orig)
if "unique_mcp_provider_name" in error_msg: if "unique_mcp_provider_name" in error_msg:
raise ValueError(f"MCP tool {name} already exists") 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") 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") raise ValueError(f"MCP tool {server_identifier} already exists")
else: raise
raise
@classmethod @classmethod
def update_mcp_provider_credentials( def update_mcp_provider_credentials(

@ -2,7 +2,6 @@ import json
import time import time
import uuid import uuid
from collections.abc import Callable, Generator, Mapping, Sequence from collections.abc import Callable, Generator, Mapping, Sequence
from datetime import UTC, datetime
from typing import Any, Optional, cast from typing import Any, Optional, cast
from uuid import uuid4 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 events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated
from extensions.ext_database import db from extensions.ext_database import db
from factories.file_factory import build_from_mapping, build_from_mappings 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.account import Account
from models.model import App, AppMode from models.model import App, AppMode
from models.tools import WorkflowToolProvider from models.tools import WorkflowToolProvider
@ -232,7 +232,7 @@ class WorkflowService:
workflow.graph = json.dumps(graph) workflow.graph = json.dumps(graph)
workflow.features = json.dumps(features) workflow.features = json.dumps(features)
workflow.updated_by = account.id 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.environment_variables = environment_variables
workflow.conversation_variables = conversation_variables workflow.conversation_variables = conversation_variables
@ -268,7 +268,7 @@ class WorkflowService:
tenant_id=app_model.tenant_id, tenant_id=app_model.tenant_id,
app_id=app_model.id, app_id=app_model.id,
type=draft_workflow.type, 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, graph=draft_workflow.graph,
features=draft_workflow.features, features=draft_workflow.features,
created_by=account.id, created_by=account.id,
@ -523,8 +523,8 @@ class WorkflowService:
node_type=node.type_, node_type=node.type_,
title=node.title, title=node.title,
elapsed_time=time.perf_counter() - start_at, elapsed_time=time.perf_counter() - start_at,
created_at=datetime.now(UTC).replace(tzinfo=None), created_at=naive_utc_now(),
finished_at=datetime.now(UTC).replace(tzinfo=None), finished_at=naive_utc_now(),
) )
if run_succeeded and node_run_result: if run_succeeded and node_run_result:
@ -621,7 +621,7 @@ class WorkflowService:
setattr(workflow, field, value) setattr(workflow, field, value)
workflow.updated_by = account_id workflow.updated_by = account_id
workflow.updated_at = datetime.now(UTC).replace(tzinfo=None) workflow.updated_at = naive_utc_now()
return workflow return workflow

@ -1,4 +1,3 @@
import datetime
import logging import logging
import time import time
@ -8,6 +7,7 @@ from celery import shared_task # type: ignore
from configs import dify_config from configs import dify_config
from core.indexing_runner import DocumentIsPausedError, IndexingRunner from core.indexing_runner import DocumentIsPausedError, IndexingRunner
from extensions.ext_database import db from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from models.dataset import Dataset, Document from models.dataset import Dataset, Document
from services.feature_service import FeatureService from services.feature_service import FeatureService
@ -53,7 +53,7 @@ def document_indexing_task(dataset_id: str, document_ids: list):
if document: if document:
document.indexing_status = "error" document.indexing_status = "error"
document.error = str(e) 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.add(document)
db.session.commit() db.session.commit()
db.session.close() db.session.close()
@ -68,7 +68,7 @@ def document_indexing_task(dataset_id: str, document_ids: list):
if document: if document:
document.indexing_status = "parsing" 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) documents.append(document)
db.session.add(document) db.session.add(document)
db.session.commit() db.session.commit()

@ -26,8 +26,15 @@ redis_mock.hgetall = MagicMock(return_value={})
redis_mock.hdel = MagicMock() redis_mock.hdel = MagicMock()
redis_mock.incr = MagicMock(return_value=1) 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 # 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() redis_patcher.start()

@ -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

@ -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"

@ -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)

@ -102,17 +102,16 @@ class TestDatasetServiceUpdateDataset:
patch("services.dataset_service.DatasetService.get_dataset") as mock_get_dataset, patch("services.dataset_service.DatasetService.get_dataset") as mock_get_dataset,
patch("services.dataset_service.DatasetService.check_dataset_permission") as mock_check_perm, patch("services.dataset_service.DatasetService.check_dataset_permission") as mock_check_perm,
patch("extensions.ext_database.db.session") as mock_db, 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) current_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
mock_datetime.datetime.now.return_value = current_time mock_naive_utc_now.return_value = current_time
mock_datetime.UTC = datetime.UTC
yield { yield {
"get_dataset": mock_get_dataset, "get_dataset": mock_get_dataset,
"check_permission": mock_check_perm, "check_permission": mock_check_perm,
"db_session": mock_db, "db_session": mock_db,
"datetime": mock_datetime, "naive_utc_now": mock_naive_utc_now,
"current_time": current_time, "current_time": current_time,
} }
@ -292,7 +291,7 @@ class TestDatasetServiceUpdateDataset:
"embedding_model_provider": "openai", "embedding_model_provider": "openai",
"embedding_model": "text-embedding-ada-002", "embedding_model": "text-embedding-ada-002",
"updated_by": user.id, "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( self._assert_database_update_called(
@ -327,7 +326,7 @@ class TestDatasetServiceUpdateDataset:
"indexing_technique": "high_quality", "indexing_technique": "high_quality",
"retrieval_model": "new_model", "retrieval_model": "new_model",
"updated_by": user.id, "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[ actual_call_args = mock_dataset_service_dependencies[
@ -365,7 +364,7 @@ class TestDatasetServiceUpdateDataset:
"collection_binding_id": None, "collection_binding_id": None,
"retrieval_model": "new_model", "retrieval_model": "new_model",
"updated_by": user.id, "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( self._assert_database_update_called(
@ -422,7 +421,7 @@ class TestDatasetServiceUpdateDataset:
"collection_binding_id": "binding-456", "collection_binding_id": "binding-456",
"retrieval_model": "new_model", "retrieval_model": "new_model",
"updated_by": user.id, "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( self._assert_database_update_called(
@ -463,7 +462,7 @@ class TestDatasetServiceUpdateDataset:
"collection_binding_id": "binding-123", "collection_binding_id": "binding-123",
"retrieval_model": "new_model", "retrieval_model": "new_model",
"updated_by": user.id, "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( self._assert_database_update_called(
@ -525,7 +524,7 @@ class TestDatasetServiceUpdateDataset:
"collection_binding_id": "binding-789", "collection_binding_id": "binding-789",
"retrieval_model": "new_model", "retrieval_model": "new_model",
"updated_by": user.id, "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( self._assert_database_update_called(
@ -568,7 +567,7 @@ class TestDatasetServiceUpdateDataset:
"collection_binding_id": "binding-123", "collection_binding_id": "binding-123",
"retrieval_model": "new_model", "retrieval_model": "new_model",
"updated_by": user.id, "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( self._assert_database_update_called(

@ -412,6 +412,8 @@ SUPABASE_URL=your-server-url
# The type of vector store to use. # 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`. # 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 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`. # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`.
WEAVIATE_ENDPOINT=http://weaviate:8080 WEAVIATE_ENDPOINT=http://weaviate:8080

@ -136,6 +136,7 @@ x-shared-env: &shared-api-worker-env
SUPABASE_API_KEY: ${SUPABASE_API_KEY:-your-access-key} SUPABASE_API_KEY: ${SUPABASE_API_KEY:-your-access-key}
SUPABASE_URL: ${SUPABASE_URL:-your-server-url} SUPABASE_URL: ${SUPABASE_URL:-your-server-url}
VECTOR_STORE: ${VECTOR_STORE:-weaviate} VECTOR_STORE: ${VECTOR_STORE:-weaviate}
VECTOR_INDEX_NAME_PREFIX: ${VECTOR_INDEX_NAME_PREFIX:-Vector_index}
WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080} WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080}
WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih} WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih}
QDRANT_URL: ${QDRANT_URL:-http://qdrant:6333} QDRANT_URL: ${QDRANT_URL:-http://qdrant:6333}

@ -1 +1,7 @@
from dify_client.client import ChatClient, CompletionClient, WorkflowClient, KnowledgeBaseClient, DifyClient from dify_client.client import (
ChatClient,
CompletionClient,
WorkflowClient,
KnowledgeBaseClient,
DifyClient,
)

@ -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 sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache tzdata 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 PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"

@ -1,6 +1,5 @@
'use client' 'use client'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { basePath } from '@/utils/var'
import { import {
RiAddLine, RiAddLine,
RiArrowRightLine, RiArrowRightLine,
@ -18,7 +17,7 @@ const CreateAppCard = ({ ref }: CreateAppCardProps) => {
<div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px] <div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px]
border-components-panel-border transition-all duration-200 ease-in-out' border-components-panel-border transition-all duration-200 ease-in-out'
> >
<Link ref={ref} className='group flex grow cursor-pointer items-start p-4' href={`${basePath}/datasets/create`}> <Link ref={ref} className='group flex grow cursor-pointer items-start p-4' href='/datasets/create'>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter
p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge' p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge'
@ -29,7 +28,7 @@ const CreateAppCard = ({ ref }: CreateAppCardProps) => {
</div> </div>
</Link> </Link>
<div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div> <div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div>
<Link className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href={`${basePath}/datasets/connect`}> <Link className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href='/datasets/connect'>
<div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div> <div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div>
<RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' /> <RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' />
</Link> </Link>

@ -83,7 +83,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>subchunk_segmentation</code> (object) 子チャンクルール - <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code> - <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
- <code>chunk_overlap</code> 隣接するチャンク間の重を定義 (オプション) - <code>chunk_overlap</code> 隣接するチャンク間の重なりを定義 (オプション)
</Property> </Property>
<PropertyInstruction>ナレッジベースにパラメータが設定されていない場合、最初のアップロードには以下のパラメータを提供する必要があります。提供されない場合、デフォルトパラメータが使用されます。</PropertyInstruction> <PropertyInstruction>ナレッジベースにパラメータが設定されていない場合、最初のアップロードには以下のパラメータを提供する必要があります。提供されない場合、デフォルトパラメータが使用されます。</PropertyInstruction>
<Property name='retrieval_model' type='object' key='retrieval_model'> <Property name='retrieval_model' type='object' key='retrieval_model'>
@ -218,7 +218,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>subchunk_segmentation</code> (object) 子チャンクルール - <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code> - <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
- <code>chunk_overlap</code> 隣接するチャンク間の重を定義 (オプション) - <code>chunk_overlap</code> 隣接するチャンク間の重なりを定義 (オプション)
</Property> </Property>
<Property name='file' type='multipart/form-data' key='file'> <Property name='file' type='multipart/form-data' key='file'>
アップロードする必要があるファイル。 アップロードする必要があるファイル。
@ -555,7 +555,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>subchunk_segmentation</code> (object) 子チャンクルール - <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code> - <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
- <code>chunk_overlap</code> 隣接するチャンク間の重を定義 (オプション) - <code>chunk_overlap</code> 隣接するチャンク間の重なりを定義 (オプション)
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -657,7 +657,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>subchunk_segmentation</code> (object) 子チャンクルール - <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code> - <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
- <code>chunk_overlap</code> 隣接するチャンク間の重を定義 (オプション) - <code>chunk_overlap</code> 隣接するチャンク間の重なりを定義 (オプション)
</Property> </Property>
</Properties> </Properties>
</Col> </Col>

@ -14,7 +14,6 @@ import Loading from '@/app/components/base/loading'
import Badge from '@/app/components/base/badge' import Badge from '@/app/components/base/badge'
import { useKnowledge } from '@/hooks/use-knowledge' import { useKnowledge } from '@/hooks/use-knowledge'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
export type ISelectDataSetProps = { export type ISelectDataSetProps = {
isShow: boolean isShow: boolean
@ -112,7 +111,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
}} }}
> >
<span className='text-text-tertiary'>{t('appDebug.feature.dataSet.noDataSet')}</span> <span className='text-text-tertiary'>{t('appDebug.feature.dataSet.noDataSet')}</span>
<Link href={`${basePath}/datasets/create`} className='font-normal text-text-accent'>{t('appDebug.feature.dataSet.toCreate')}</Link> <Link href='/datasets/create' className='font-normal text-text-accent'>{t('appDebug.feature.dataSet.toCreate')}</Link>
</div> </div>
)} )}

@ -117,7 +117,7 @@ const Question: FC<QuestionProps> = ({
</div> </div>
<div <div
ref={contentRef} ref={contentRef}
className='bg-background-gradient-bg-fill-chat-bubble-bg-3 w-full rounded-2xl px-4 py-3 text-sm text-text-primary' className='w-full rounded-2xl bg-background-gradient-bg-fill-chat-bubble-bg-3 px-4 py-3 text-sm text-text-primary'
style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}} style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}}
> >
{ {

@ -333,7 +333,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
<Col> <Col>
根据 workflow 执行 ID 获取 workflow 任务当前执行结果 根据 workflow 执行 ID 获取 workflow 任务当前执行结果
### Path ### Path
- `workflow_run_id` (string) workflow_run_id,可在流式返回 Chunk 中获取 - `workflow_run_id` (string) workflow 执行 ID,可在流式返回 Chunk 中获取
### Response ### Response
- `id` (string) workflow 执行 ID - `id` (string) workflow 执行 ID
- `workflow_id` (string) 关联的 Workflow ID - `workflow_id` (string) 关联的 Workflow ID

@ -14,7 +14,6 @@ import Nav from '../nav'
import type { NavItem } from '../nav/nav-selector' import type { NavItem } from '../nav/nav-selector'
import { fetchDatasetDetail, fetchDatasets } from '@/service/datasets' import { fetchDatasetDetail, fetchDatasets } from '@/service/datasets'
import type { DataSetListResponse } from '@/models/datasets' import type { DataSetListResponse } from '@/models/datasets'
import { basePath } from '@/utils/var'
const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
if (!pageIndex || previousPageData.has_more) if (!pageIndex || previousPageData.has_more)
@ -57,7 +56,7 @@ const DatasetNav = () => {
icon_background: dataset.icon_background, icon_background: dataset.icon_background,
})) as NavItem[]} })) as NavItem[]}
createText={t('common.menus.newDataset')} createText={t('common.menus.newDataset')}
onCreate={() => router.push(`${basePath}/datasets/create`)} onCreate={() => router.push('/datasets/create')}
onLoadmore={handleLoadmore} onLoadmore={handleLoadmore}
isApp={false} isApp={false}
/> />

@ -7,7 +7,7 @@ import { renderI18nObject } from '@/i18n'
const nodeDefault: NodeDefault<AgentNodeType> = { const nodeDefault: NodeDefault<AgentNodeType> = {
defaultValue: { defaultValue: {
version: '2', tool_node_version: '2',
}, },
getAvailablePrevNodes(isChatMode) { getAvailablePrevNodes(isChatMode) {
return isChatMode return isChatMode
@ -62,27 +62,29 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
const userSettings = toolValue.settings const userSettings = toolValue.settings
const reasoningConfig = toolValue.parameters const reasoningConfig = toolValue.parameters
const version = payload.version const version = payload.version
const toolNodeVersion = payload.tool_node_version
const mergeVersion = version || toolNodeVersion
schemas.forEach((schema: any) => { schemas.forEach((schema: any) => {
if (schema?.required) { if (schema?.required) {
if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) { if (schema.form === 'form' && !mergeVersion && !userSettings[schema.name]?.value) {
return { return {
isValid: false, isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), 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 { return {
isValid: false, isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), 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 { return {
isValid: false, isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), 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 { return {
isValid: false, isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),

@ -12,6 +12,7 @@ export type AgentNodeType = CommonNodeType & {
plugin_unique_identifier?: string plugin_unique_identifier?: string
memory?: Memory memory?: Memory
version?: string version?: string
tool_node_version?: string
} }
export enum AgentFeature { export enum AgentFeature {

@ -129,7 +129,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
} }
const formattingLegacyData = () => { const formattingLegacyData = () => {
if (inputs.version) if (inputs.version || inputs.tool_node_version)
return inputs return inputs
const newData = produce(inputs, (draft) => { const newData = produce(inputs, (draft) => {
const schemas = currentStrategy?.parameters || [] const schemas = currentStrategy?.parameters || []
@ -140,7 +140,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
if (targetSchema?.type === FormTypeEnum.multiToolSelector) if (targetSchema?.type === FormTypeEnum.multiToolSelector)
draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool)) 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 return newData
} }

@ -10,7 +10,7 @@ const nodeDefault: NodeDefault<ToolNodeType> = {
defaultValue: { defaultValue: {
tool_parameters: {}, tool_parameters: {},
tool_configurations: {}, tool_configurations: {},
version: '2', tool_node_version: '2',
}, },
getAvailablePrevNodes(isChatMode: boolean) { getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode const nodes = isChatMode

@ -23,4 +23,5 @@ export type ToolNodeType = CommonNodeType & {
output_schema: Record<string, any> output_schema: Record<string, any>
paramSchemas?: Record<string, any>[] paramSchemas?: Record<string, any>[]
version?: string version?: string
tool_node_version?: string
} }

@ -286,8 +286,8 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
} }
} }
if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version) { 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.version = '2' (node as Node<ToolNodeType>).data.tool_node_version = '2'
const toolConfigurations = (node as Node<ToolNodeType>).data.tool_configurations const toolConfigurations = (node as Node<ToolNodeType>).data.tool_configurations
if (toolConfigurations && Object.keys(toolConfigurations).length > 0) { if (toolConfigurations && Object.keys(toolConfigurations).length > 0) {

@ -8,6 +8,7 @@ import storybook from 'eslint-plugin-storybook'
import tailwind from 'eslint-plugin-tailwindcss' import tailwind from 'eslint-plugin-tailwindcss'
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from 'eslint-plugin-react-hooks'
import sonar from 'eslint-plugin-sonarjs' import sonar from 'eslint-plugin-sonarjs'
import oxlint from 'eslint-plugin-oxlint'
// import reactRefresh from 'eslint-plugin-react-refresh' // import reactRefresh from 'eslint-plugin-react-refresh'
@ -245,4 +246,5 @@ export default combine(
'tailwindcss/migration-from-tailwind-2': 'warn', 'tailwindcss/migration-from-tailwind-2': 'warn',
}, },
}, },
oxlint.configs['flat/recommended'],
) )

@ -78,7 +78,7 @@ const translation = {
optional: 'Wahlfrei', optional: 'Wahlfrei',
noTemplateFound: 'Keine Vorlagen gefunden', noTemplateFound: 'Keine Vorlagen gefunden',
workflowUserDescription: 'Autonome KI-Arbeitsabläufe visuell per Drag-and-Drop erstellen.', workflowUserDescription: 'Autonome KI-Arbeitsabläufe visuell per Drag-and-Drop erstellen.',
foundResults: '{{Anzahl}} Befund', foundResults: '{{count}} Befund',
chatbotShortDescription: 'LLM-basierter Chatbot mit einfacher Einrichtung', chatbotShortDescription: 'LLM-basierter Chatbot mit einfacher Einrichtung',
completionUserDescription: 'Erstellen Sie schnell einen KI-Assistenten für Textgenerierungsaufgaben mit einfacher Konfiguration.', completionUserDescription: 'Erstellen Sie schnell einen KI-Assistenten für Textgenerierungsaufgaben mit einfacher Konfiguration.',
noAppsFound: 'Keine Apps gefunden', noAppsFound: 'Keine Apps gefunden',
@ -92,7 +92,7 @@ const translation = {
noTemplateFoundTip: 'Versuchen Sie, mit verschiedenen Schlüsselwörtern zu suchen.', noTemplateFoundTip: 'Versuchen Sie, mit verschiedenen Schlüsselwörtern zu suchen.',
advancedUserDescription: 'Workflow mit Speicherfunktionen und Chatbot-Oberfläche.', 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.', 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.', 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', agentShortDescription: 'Intelligenter Agent mit logischem Denken und autonomer Werkzeugnutzung',
dropDSLToCreateApp: 'Ziehen Sie die DSL-Datei hierher, um die App zu erstellen', dropDSLToCreateApp: 'Ziehen Sie die DSL-Datei hierher, um die App zu erstellen',

@ -77,10 +77,10 @@ const translation = {
appCreateDSLErrorPart1: 'تفاوت قابل توجهی در نسخه های DSL مشاهده شده است. اجبار به واردات ممکن است باعث اختلال در عملکرد برنامه شود.', appCreateDSLErrorPart1: 'تفاوت قابل توجهی در نسخه های DSL مشاهده شده است. اجبار به واردات ممکن است باعث اختلال در عملکرد برنامه شود.',
appCreateDSLWarning: 'احتیاط: تفاوت نسخه DSL ممکن است بر ویژگی های خاصی تأثیر بگذارد', appCreateDSLWarning: 'احتیاط: تفاوت نسخه DSL ممکن است بر ویژگی های خاصی تأثیر بگذارد',
completionShortDescription: 'دستیار هوش مصنوعی برای تسک های تولید متن', completionShortDescription: 'دستیار هوش مصنوعی برای تسک های تولید متن',
foundResult: '{{تعداد}} نتیجه', foundResult: '{{count}} نتیجه',
chatbotUserDescription: 'به سرعت یک چت بات مبتنی بر LLM با پیکربندی ساده بسازید. بعدا می توانید به Chatflow بروید.', chatbotUserDescription: 'به سرعت یک چت بات مبتنی بر LLM با پیکربندی ساده بسازید. بعدا می توانید به Chatflow بروید.',
chooseAppType: 'انتخاب نوع برنامه', chooseAppType: 'انتخاب نوع برنامه',
foundResults: '{{تعداد}} نتیجه', foundResults: '{{count}} نتیجه',
noIdeaTip: 'ایده ای ندارید؟ قالب های ما را بررسی کنید', noIdeaTip: 'ایده ای ندارید؟ قالب های ما را بررسی کنید',
forBeginners: 'انواع برنامه‌های پایه‌تر', forBeginners: 'انواع برنامه‌های پایه‌تر',
noAppsFound: 'هیچ برنامه ای یافت نشد', noAppsFound: 'هیچ برنامه ای یافت نشد',

@ -74,12 +74,12 @@ const translation = {
appCreateDSLErrorPart2: 'क्या आप जारी रखना चाहते हैं?', appCreateDSLErrorPart2: 'क्या आप जारी रखना चाहते हैं?',
learnMore: 'और जानो', learnMore: 'और जानो',
forBeginners: 'नए उपयोगकर्ताओं के लिए बुनियादी ऐप प्रकार', forBeginners: 'नए उपयोगकर्ताओं के लिए बुनियादी ऐप प्रकार',
foundResults: '{{गिनती}} परिणाम', foundResults: '{{count}} परिणाम',
forAdvanced: 'उन्नत उपयोगकर्ताओं के लिए', forAdvanced: 'उन्नत उपयोगकर्ताओं के लिए',
agentUserDescription: 'पुनरावृत्त तर्क और स्वायत्त उपकरण में सक्षम एक बुद्धिमान एजेंट कार्य लक्ष्यों को प्राप्त करने के लिए उपयोग करता है।', agentUserDescription: 'पुनरावृत्त तर्क और स्वायत्त उपकरण में सक्षम एक बुद्धिमान एजेंट कार्य लक्ष्यों को प्राप्त करने के लिए उपयोग करता है।',
optional: 'वैकल्पिक', optional: 'वैकल्पिक',
chatbotShortDescription: 'सरल सेटअप के साथ एलएलएम-आधारित चैटबॉट', chatbotShortDescription: 'सरल सेटअप के साथ एलएलएम-आधारित चैटबॉट',
foundResult: '{{गिनती}} परिणाम', foundResult: '{{count}} परिणाम',
completionUserDescription: 'सरल कॉन्फ़िगरेशन के साथ पाठ निर्माण कार्यों के लिए त्वरित रूप से AI सहायक बनाएं।', completionUserDescription: 'सरल कॉन्फ़िगरेशन के साथ पाठ निर्माण कार्यों के लिए त्वरित रूप से AI सहायक बनाएं।',
noIdeaTip: 'कोई विचार नहीं? हमारे टेम्प्लेट देखें', noIdeaTip: 'कोई विचार नहीं? हमारे टेम्प्लेट देखें',
noTemplateFound: 'कोई टेम्पलेट नहीं मिला', noTemplateFound: 'कोई टेम्पलेट नहीं मिला',

@ -79,11 +79,11 @@ const translation = {
appCreateDSLErrorTitle: 'バージョンの非互換性', appCreateDSLErrorTitle: 'バージョンの非互換性',
appCreateDSLWarning: '注意:DSL のバージョンの違いは、特定の機能に影響を与える可能性があります', appCreateDSLWarning: '注意:DSL のバージョンの違いは、特定の機能に影響を与える可能性があります',
appCreateDSLErrorPart1: 'DSL バージョンに大きな違いが検出されました。インポートを強制すると、アプリケーションが誤動作する可能性があります。', appCreateDSLErrorPart1: 'DSL バージョンに大きな違いが検出されました。インポートを強制すると、アプリケーションが誤動作する可能性があります。',
optional: '意', optional: '意',
forBeginners: '初心者向けの基本的なアプリタイプ', forBeginners: '初心者向けの基本的なアプリタイプ',
noTemplateFoundTip: '別のキーワードを使用して検索してみてください。', noTemplateFoundTip: '別のキーワードを使用して検索してみてください。',
agentShortDescription: '推論と自律的なツールの使用を備えたインテリジェントエージェント', agentShortDescription: '推論と自律的なツールの使用を備えたインテリジェントエージェント',
foundResults: '{{カウント}}業績', foundResults: '{{count}}件の結果',
noTemplateFound: 'テンプレートが見つかりません', noTemplateFound: 'テンプレートが見つかりません',
noAppsFound: 'アプリが見つかりませんでした', noAppsFound: 'アプリが見つかりませんでした',
workflowShortDescription: 'インテリジェントな自動化のためのエージェントフロー', workflowShortDescription: 'インテリジェントな自動化のためのエージェントフロー',
@ -91,7 +91,7 @@ const translation = {
advancedUserDescription: '追加のメモリ機能とチャットボットインターフェースを備えたワークフロー', advancedUserDescription: '追加のメモリ機能とチャットボットインターフェースを備えたワークフロー',
advancedShortDescription: 'メモリを使用した複雑なマルチターン対話のワークフロー', advancedShortDescription: 'メモリを使用した複雑なマルチターン対話のワークフロー',
agentUserDescription: 'タスクの目標を達成するために反復的な推論と自律的なツールを使用できるインテリジェントエージェント。', agentUserDescription: 'タスクの目標を達成するために反復的な推論と自律的なツールを使用できるインテリジェントエージェント。',
foundResult: '{{カウント}}結果', foundResult: '{{count}}件の結果',
forAdvanced: '上級ユーザー向け', forAdvanced: '上級ユーザー向け',
chooseAppType: 'アプリタイプを選択', chooseAppType: 'アプリタイプを選択',
learnMore: '詳細情報', learnMore: '詳細情報',

@ -43,7 +43,7 @@ const translation = {
log: 'ログ', log: 'ログ',
learnMore: '詳細はこちら', learnMore: '詳細はこちら',
params: 'パラメータ', params: 'パラメータ',
duplicate: '複', duplicate: '',
rename: '名前の変更', rename: '名前の変更',
audioSourceUnavailable: 'AudioSource が利用できません', audioSourceUnavailable: 'AudioSource が利用できません',
zoomIn: 'ズームインする', zoomIn: 'ズームインする',
@ -229,7 +229,7 @@ const translation = {
permanentlyDeleteButton: 'アカウントを完全に削除', permanentlyDeleteButton: 'アカウントを完全に削除',
feedbackTitle: 'フィードバック', feedbackTitle: 'フィードバック',
feedbackLabel: 'アカウントを削除した理由を教えてください。', feedbackLabel: 'アカウントを削除した理由を教えてください。',
feedbackPlaceholder: '意', feedbackPlaceholder: '意',
sendVerificationButton: '確認コードの送信', sendVerificationButton: '確認コードの送信',
editWorkspaceInfo: 'ワークスペース情報を編集', editWorkspaceInfo: 'ワークスペース情報を編集',
workspaceName: 'ワークスペース名', workspaceName: 'ワークスペース名',

@ -2,7 +2,7 @@ const translation = {
category: { category: {
extensions: '拡張機能', extensions: '拡張機能',
all: 'すべて', all: 'すべて',
tools: '道具', tools: 'ツール',
bundles: 'バンドル', bundles: 'バンドル',
agents: 'エージェント戦略', agents: 'エージェント戦略',
models: 'モデル', models: 'モデル',
@ -11,7 +11,7 @@ const translation = {
agent: 'エージェント戦略', agent: 'エージェント戦略',
model: 'モデル', model: 'モデル',
bundle: 'バンドル', bundle: 'バンドル',
tool: '道具', tool: 'ツール',
extension: '拡張', extension: '拡張',
}, },
list: { list: {
@ -60,7 +60,7 @@ const translation = {
uninstalledTitle: 'ツールがインストールされていません', uninstalledTitle: 'ツールがインストールされていません',
empty: 'ツールを追加するには「+」ボタンをクリックしてください。複数のツールを追加できます。', empty: 'ツールを追加するには「+」ボタンをクリックしてください。複数のツールを追加できます。',
paramsTip1: 'LLM 推論パラメータを制御します。', paramsTip1: 'LLM 推論パラメータを制御します。',
toolLabel: '道具', toolLabel: 'ツール',
unsupportedTitle: 'サポートされていないアクション', unsupportedTitle: 'サポートされていないアクション',
toolSetting: 'ツール設定', toolSetting: 'ツール設定',
unsupportedMCPTool: '現在選択されているエージェント戦略プラグインのバージョンはMCPツールをサポートしていません。', unsupportedMCPTool: '現在選択されているエージェント戦略プラグインのバージョンはMCPツールをサポートしていません。',

@ -887,7 +887,7 @@ const translation = {
modelNotSelected: 'モデルが選択されていません', modelNotSelected: 'モデルが選択されていません',
toolNotAuthorizedTooltip: '{{tool}} 認可されていません', toolNotAuthorizedTooltip: '{{tool}} 認可されていません',
toolNotInstallTooltip: '{{tool}}はインストールされていません', toolNotInstallTooltip: '{{tool}}はインストールされていません',
tools: '道具', tools: 'ツール',
learnMore: 'もっと学ぶ', learnMore: 'もっと学ぶ',
configureModel: 'モデルを設定する', configureModel: 'モデルを設定する',
model: 'モデル', model: 'モデル',

@ -90,12 +90,12 @@ const translation = {
noTemplateFound: '템플릿을 찾을 수 없습니다.', noTemplateFound: '템플릿을 찾을 수 없습니다.',
completionShortDescription: '텍스트 생성 작업을 위한 AI 도우미', completionShortDescription: '텍스트 생성 작업을 위한 AI 도우미',
learnMore: '더 알아보세요', learnMore: '더 알아보세요',
foundResults: '{{개수}} 결과', foundResults: '{{count}} 결과',
agentShortDescription: agentShortDescription:
'추론 및 자율적인 도구 사용 기능이 있는 지능형 에이전트', '추론 및 자율적인 도구 사용 기능이 있는 지능형 에이전트',
advancedShortDescription: '다중 대화를 위해 강화된 워크플로우', advancedShortDescription: '다중 대화를 위해 강화된 워크플로우',
noAppsFound: '앱을 찾을 수 없습니다.', noAppsFound: '앱을 찾을 수 없습니다.',
foundResult: '{{개수}} 결과', foundResult: '{{count}} 결과',
completionUserDescription: completionUserDescription:
'간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.', '간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.',
chatbotUserDescription: chatbotUserDescription:

@ -80,7 +80,7 @@ const translation = {
appCreateDSLErrorPart1: 'Wykryto istotną różnicę w wersjach DSL. Wymuszenie importu może spowodować nieprawidłowe działanie aplikacji.', 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.', noTemplateFoundTip: 'Spróbuj wyszukać za pomocą różnych słów kluczowych.',
noAppsFound: 'Nie znaleziono aplikacji', noAppsFound: 'Nie znaleziono aplikacji',
foundResults: '{{liczba}} Wyniki', foundResults: '{{count}} Wyniki',
noTemplateFound: 'Nie znaleziono szablonów', noTemplateFound: 'Nie znaleziono szablonów',
chatbotUserDescription: 'Szybko zbuduj chatbota opartego na LLM z prostą konfiguracją. Możesz przełączyć się na Chatflow później.', chatbotUserDescription: 'Szybko zbuduj chatbota opartego na LLM z prostą konfiguracją. Możesz przełączyć się na Chatflow później.',
optional: 'Fakultatywny', optional: 'Fakultatywny',
@ -91,7 +91,7 @@ const translation = {
completionShortDescription: 'Asystent AI do zadań generowania tekstu', completionShortDescription: 'Asystent AI do zadań generowania tekstu',
noIdeaTip: 'Nie masz pomysłów? Sprawdź nasze szablony', noIdeaTip: 'Nie masz pomysłów? Sprawdź nasze szablony',
forAdvanced: 'DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW', forAdvanced: 'DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW',
foundResult: '{{liczba}} Wynik', foundResult: '{{count}} Wynik',
advancedShortDescription: 'Przepływ ulepszony dla wieloturowych czatów', advancedShortDescription: 'Przepływ ulepszony dla wieloturowych czatów',
learnMore: 'Dowiedz się więcej', learnMore: 'Dowiedz się więcej',
chatbotShortDescription: 'Chatbot oparty na LLM z prostą konfiguracją', chatbotShortDescription: 'Chatbot oparty na LLM z prostą konfiguracją',

@ -84,8 +84,8 @@ const translation = {
advancedShortDescription: 'Flux de lucru îmbunătățit pentru conversații multi-tur', 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.', 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.', noTemplateFoundTip: 'Încercați să căutați folosind cuvinte cheie diferite.',
foundResults: '{{număr}} Rezultatele', foundResults: '{{count}} Rezultatele',
foundResult: '{{număr}} Rezultat', foundResult: '{{count}} Rezultat',
noIdeaTip: 'Nicio idee? Consultați șabloanele noastre', noIdeaTip: 'Nicio idee? Consultați șabloanele noastre',
noAppsFound: 'Nu s-au găsit aplicații', noAppsFound: 'Nu s-au găsit aplicații',
workflowShortDescription: 'Flux agentic pentru automatizări inteligente', workflowShortDescription: 'Flux agentic pentru automatizări inteligente',

@ -78,11 +78,11 @@ const translation = {
appCreateDSLErrorPart1: 'Обнаружена существенная разница в версиях DSL. Принудительный импорт может привести к сбою в работе приложения.', appCreateDSLErrorPart1: 'Обнаружена существенная разница в версиях DSL. Принудительный импорт может привести к сбою в работе приложения.',
learnMore: 'Подробнее', learnMore: 'Подробнее',
forAdvanced: 'ДЛЯ ПРОДВИНУТЫХ ПОЛЬЗОВАТЕЛЕЙ', forAdvanced: 'ДЛЯ ПРОДВИНУТЫХ ПОЛЬЗОВАТЕЛЕЙ',
foundResults: '{{Количество}} Результаты', foundResults: '{{count}} Результаты',
optional: 'Необязательный', optional: 'Необязательный',
chatbotShortDescription: 'Чат-бот на основе LLM с простой настройкой', chatbotShortDescription: 'Чат-бот на основе LLM с простой настройкой',
advancedShortDescription: 'Рабочий процесс, улучшенный для многоходовых чатов', advancedShortDescription: 'Рабочий процесс, улучшенный для многоходовых чатов',
foundResult: '{{Количество}} Результат', foundResult: '{{count}} Результат',
workflowShortDescription: 'Агентный поток для интеллектуальных автоматизаций', workflowShortDescription: 'Агентный поток для интеллектуальных автоматизаций',
advancedUserDescription: 'Рабочий процесс с дополнительными функциями памяти и интерфейсом чат-бота.', advancedUserDescription: 'Рабочий процесс с дополнительными функциями памяти и интерфейсом чат-бота.',
noAppsFound: 'Приложения не найдены', noAppsFound: 'Приложения не найдены',

@ -79,8 +79,8 @@ const translation = {
advancedShortDescription: 'Potek dela izboljšan za večkratne pogovore', advancedShortDescription: 'Potek dela izboljšan za večkratne pogovore',
noAppsFound: 'Ni bilo najdenih aplikacij', noAppsFound: 'Ni bilo najdenih aplikacij',
agentShortDescription: 'Inteligentni agent z razmišljanjem in avtonomno uporabo orodij', agentShortDescription: 'Inteligentni agent z razmišljanjem in avtonomno uporabo orodij',
foundResult: '{{štetje}} Rezultat', foundResult: '{{count}} Rezultat',
foundResults: '{{štetje}} Rezultati', foundResults: '{{count}} Rezultati',
noTemplateFoundTip: 'Poskusite iskati z različnimi ključnimi besedami.', noTemplateFoundTip: 'Poskusite iskati z različnimi ključnimi besedami.',
optional: 'Neobvezno', optional: 'Neobvezno',
forBeginners: 'Bolj osnovne vrste aplikacij', forBeginners: 'Bolj osnovne vrste aplikacij',

@ -73,7 +73,7 @@ const translation = {
appCreateDSLErrorPart4: 'เวอร์ชัน DSL ที่ระบบรองรับ:', appCreateDSLErrorPart4: 'เวอร์ชัน DSL ที่ระบบรองรับ:',
appCreateFailed: 'สร้างโปรเจกต์ไม่สําเร็จ', appCreateFailed: 'สร้างโปรเจกต์ไม่สําเร็จ',
learnMore: 'ศึกษาเพิ่มเติม', learnMore: 'ศึกษาเพิ่มเติม',
foundResults: '{{นับ}} ผลลัพธ์', foundResults: '{{count}} ผลลัพธ์',
noTemplateFoundTip: 'ลองค้นหาโดยใช้คีย์เวิร์ดอื่น', noTemplateFoundTip: 'ลองค้นหาโดยใช้คีย์เวิร์ดอื่น',
chatbotShortDescription: 'แชทบอทที่ใช้ LLM พร้อมการตั้งค่าที่ง่ายดาย', chatbotShortDescription: 'แชทบอทที่ใช้ LLM พร้อมการตั้งค่าที่ง่ายดาย',
optional: 'เสริม', optional: 'เสริม',
@ -83,7 +83,7 @@ const translation = {
completionShortDescription: 'ผู้ช่วย AI สําหรับงานสร้างข้อความ', completionShortDescription: 'ผู้ช่วย AI สําหรับงานสร้างข้อความ',
agentUserDescription: 'ตัวแทนอัจฉริยะที่สามารถให้เหตุผลซ้ําๆ และใช้เครื่องมืออัตโนมัติเพื่อให้บรรลุเป้าหมายของงาน', agentUserDescription: 'ตัวแทนอัจฉริยะที่สามารถให้เหตุผลซ้ําๆ และใช้เครื่องมืออัตโนมัติเพื่อให้บรรลุเป้าหมายของงาน',
noIdeaTip: 'ไม่มีความคิด? ดูเทมเพลตของเรา', noIdeaTip: 'ไม่มีความคิด? ดูเทมเพลตของเรา',
foundResult: '{{นับ}} ผล', foundResult: '{{count}} ผล',
noAppsFound: 'ไม่พบแอป', noAppsFound: 'ไม่พบแอป',
workflowShortDescription: 'โฟลว์อัตโนมัติสำหรับระบบอัจฉริยะ', workflowShortDescription: 'โฟลว์อัตโนมัติสำหรับระบบอัจฉริยะ',
forAdvanced: 'สําหรับผู้ใช้ขั้นสูง', forAdvanced: 'สําหรับผู้ใช้ขั้นสูง',

@ -72,11 +72,11 @@ const translation = {
appCreateDSLErrorPart3: 'Geçerli uygulama DSL sürümü:', appCreateDSLErrorPart3: 'Geçerli uygulama DSL sürümü:',
appCreateDSLErrorTitle: 'Sürüm Uyumsuzluğu', appCreateDSLErrorTitle: 'Sürüm Uyumsuzluğu',
Confirm: 'Onaylamak', Confirm: 'Onaylamak',
foundResults: '{{sayı}} Sonuç -ları', foundResults: '{{count}} Sonuç -ları',
noAppsFound: 'Uygulama bulunamadı', 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.', chatbotUserDescription: 'Basit yapılandırmayla hızlı bir şekilde LLM tabanlı bir sohbet botu oluşturun. Daha sonra Chatflow\'a geçebilirsiniz.',
optional: 'Opsiyonel', optional: 'Opsiyonel',
foundResult: '{{sayı}} Sonuç', foundResult: '{{count}} Sonuç',
noTemplateFound: 'Şablon bulunamadı', noTemplateFound: 'Şablon bulunamadı',
workflowUserDescription: 'Sürükle-bırak kolaylığıyla görsel olarak otonom yapay zeka iş akışları oluşturun.', 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ışı.', advancedUserDescription: 'Ek bellek özellikleri ve sohbet robotu arayüzü ile iş akışı.',

@ -80,13 +80,13 @@ const translation = {
optional: 'Tùy chọn', 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ớ', 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.', 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.', 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ụ.', 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', 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.', 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', 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', 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', noTemplateFound: 'Không tìm thấy mẫu',
noAppsFound: 'Không tìm thấy ứng dụng nào', noAppsFound: 'Không tìm thấy ứng dụng nào',

@ -21,8 +21,8 @@
"dev": "cross-env NODE_OPTIONS='--inspect' next dev", "dev": "cross-env NODE_OPTIONS='--inspect' next dev",
"build": "next build", "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", "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": "pnpx oxlint && 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-only-show-error": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet",
"fix": "next lint --fix", "fix": "next lint --fix",
"eslint-fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --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", "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", "cross-env": "^7.0.3",
"eslint": "^9.20.1", "eslint": "^9.20.1",
"eslint-config-next": "~15.3.5", "eslint-config-next": "~15.3.5",
"eslint-plugin-oxlint": "^1.6.0",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-sonarjs": "^3.0.2", "eslint-plugin-sonarjs": "^3.0.2",

@ -516,6 +516,9 @@ importers:
eslint-config-next: eslint-config-next:
specifier: ~15.3.5 specifier: ~15.3.5
version: 15.3.5(eslint@9.31.0(jiti@1.21.7))(typescript@5.8.3) 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: eslint-plugin-react-hooks:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.2.0(eslint@9.31.0(jiti@1.21.7)) version: 5.2.0(eslint@9.31.0(jiti@1.21.7))
@ -4775,6 +4778,9 @@ packages:
resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==}
engines: {node: '>=5.0.0'} 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: eslint-plugin-perfectionist@4.15.0:
resolution: {integrity: sha512-pC7PgoXyDnEXe14xvRUhBII8A3zRgggKqJFx2a82fjrItDs1BSI7zdZnQtM2yQvcyod6/ujmzb7ejKPx8lZTnw==} resolution: {integrity: sha512-pC7PgoXyDnEXe14xvRUhBII8A3zRgggKqJFx2a82fjrItDs1BSI7zdZnQtM2yQvcyod6/ujmzb7ejKPx8lZTnw==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@ -5832,6 +5838,9 @@ packages:
resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 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: jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
@ -13169,6 +13178,10 @@ snapshots:
eslint-plugin-no-only-tests@3.3.0: {} 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): eslint-plugin-perfectionist@4.15.0(eslint@9.31.0(jiti@1.21.7))(typescript@5.8.3):
dependencies: dependencies:
'@typescript-eslint/types': 8.37.0 '@typescript-eslint/types': 8.37.0
@ -14650,6 +14663,8 @@ snapshots:
espree: 9.6.1 espree: 9.6.1
semver: 7.7.2 semver: 7.7.2
jsonc-parser@3.3.1: {}
jsonfile@6.1.0: jsonfile@6.1.0:
dependencies: dependencies:
universalify: 2.0.1 universalify: 2.0.1

Loading…
Cancel
Save