diff --git a/.github/workflows/deploy-rag-dev.yml b/.github/workflows/deploy-rag-dev.yml new file mode 100644 index 0000000000..86265aad6d --- /dev/null +++ b/.github/workflows/deploy-rag-dev.yml @@ -0,0 +1,28 @@ +name: Deploy RAG Dev + +permissions: + contents: read + +on: + workflow_run: + workflows: ["Build and Push API & Web"] + branches: + - "deploy/rag-dev" + types: + - completed + +jobs: + deploy: + runs-on: ubuntu-latest + if: | + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'deploy/rag-dev' + steps: + - name: Deploy to server + uses: appleboy/ssh-action@v0.1.8 + with: + host: ${{ secrets.RAG_SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + ${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }} diff --git a/.github/workflows/expose_service_ports.sh b/.github/workflows/expose_service_ports.sh index 10d95cb736..01772ccf9f 100755 --- a/.github/workflows/expose_service_ports.sh +++ b/.github/workflows/expose_service_ports.sh @@ -10,6 +10,7 @@ yq eval '.services["elasticsearch"].ports += ["9200:9200"]' -i docker/docker-com yq eval '.services.couchbase-server.ports += ["8091-8096:8091-8096"]' -i docker/docker-compose.yaml yq eval '.services.couchbase-server.ports += ["11210:11210"]' -i docker/docker-compose.yaml yq eval '.services.tidb.ports += ["4000:4000"]' -i docker/tidb/docker-compose.yaml +yq eval '.services.oceanbase.ports += ["2881:2881"]' -i docker/docker-compose.yaml yq eval '.services.opengauss.ports += ["6600:6600"]' -i docker/docker-compose.yaml echo "Ports exposed for sandbox, weaviate, tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase, opengauss" diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml index c784817e72..512d14b2ee 100644 --- a/.github/workflows/vdb-tests.yml +++ b/.github/workflows/vdb-tests.yml @@ -31,6 +31,13 @@ jobs: with: persist-credentials: false + - name: Free Disk Space + uses: endersonmenezes/free-disk-space@v2 + with: + remove_dotnet: true + remove_haskell: true + remove_tool_cache: true + - name: Setup UV and Python uses: ./.github/actions/setup-uv with: @@ -59,7 +66,7 @@ jobs: tidb tiflash - - name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase) + - name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase) uses: hoverkraft-tech/compose-action@v2.0.2 with: compose-file: | @@ -75,9 +82,12 @@ jobs: pgvector chroma elasticsearch + oceanbase - - name: Check TiDB Ready - run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py + - name: Check VDB Ready (TiDB, Oceanbase) + run: | + uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py + uv run --project api python api/tests/integration_tests/vdb/oceanbase/check_oceanbase_ready.py - name: Test Vector Stores run: uv run --project api bash dev/pytest/pytest_vdb.sh diff --git a/.gitignore b/.gitignore index 74a9ef63ef..4c938b7682 100644 --- a/.gitignore +++ b/.gitignore @@ -210,3 +210,6 @@ mise.toml # Next.js build output .next/ + +# AI Assistant +.roo/ diff --git a/api/commands.py b/api/commands.py index 6262bfde6b..0a6cc61a68 100644 --- a/api/commands.py +++ b/api/commands.py @@ -27,7 +27,7 @@ from models.dataset import Dataset, DatasetCollectionBinding, DatasetMetadata, D from models.dataset import Document as DatasetDocument from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation from models.provider import Provider, ProviderModel -from services.account_service import RegisterService, TenantService +from services.account_service import AccountService, RegisterService, TenantService from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs from services.plugin.data_migration import PluginDataMigration from services.plugin.plugin_migration import PluginMigration @@ -68,6 +68,7 @@ def reset_password(email, new_password, password_confirm): account.password = base64_password_hashed account.password_salt = base64_salt db.session.commit() + AccountService.reset_login_error_rate_limit(email) click.echo(click.style("Password reset successfully.", fg="green")) diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index 5f209736a0..0107df22c5 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings): CURRENT_VERSION: str = Field( description="Dify version", - default="1.4.2", + default="1.4.3", ) COMMIT_SHA: str = Field( diff --git a/api/controllers/service_api/app/app.py b/api/controllers/service_api/app/app.py index 2c03aba33d..89222d5e83 100644 --- a/api/controllers/service_api/app/app.py +++ b/api/controllers/service_api/app/app.py @@ -47,7 +47,13 @@ class AppInfoApi(Resource): def get(self, app_model: App): """Get app information""" tags = [tag.name for tag in app_model.tags] - return {"name": app_model.name, "description": app_model.description, "tags": tags, "mode": app_model.mode} + return { + "name": app_model.name, + "description": app_model.description, + "tags": tags, + "mode": app_model.mode, + "author_name": app_model.author_name, + } api.add_resource(AppParameterApi, "/parameters") diff --git a/api/controllers/web/passport.py b/api/controllers/web/passport.py index 9d229185f3..10c3cdcf0e 100644 --- a/api/controllers/web/passport.py +++ b/api/controllers/web/passport.py @@ -163,7 +163,7 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: ) db.session.add(end_user) db.session.commit() - exp_dt = datetime.now(UTC) + timedelta(hours=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES * 24) + exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES) exp = int(exp_dt.timestamp()) payload = { "iss": site.id, diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 8c85f91d7e..9e6adc4b08 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping from typing import Any, Literal, Optional, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from sqlalchemy.orm import sessionmaker @@ -31,6 +31,7 @@ from core.workflow.repositories.workflow_execution_repository import WorkflowExe from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, Conversation, EndUser, Message, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom from services.conversation_service import ConversationService @@ -399,20 +400,17 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - conversation_id=conversation.id, - message_id=message.id, - context=context, - ) - - worker_thread = threading.Thread(target=worker_with_context) + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "conversation_id": conversation.id, + "message_id": message.id, + "context": context, + }, + ) worker_thread.start() @@ -449,24 +447,9 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): :param message_id: message ID :return: """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # get conversation and message conversation = self._get_conversation(conversation_id) message = self._get_message(message_id) diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index 158196f24d..a448bf8a94 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping from typing import Any, Literal, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from configs import dify_config @@ -23,6 +23,7 @@ from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser from services.conversation_service import ConversationService from services.errors.message import MessageNotExistsError @@ -182,20 +183,17 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - context=context, - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - conversation_id=conversation.id, - message_id=message.id, - ) - - worker_thread = threading.Thread(target=worker_with_context) + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "context": context, + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "conversation_id": conversation.id, + "message_id": message.id, + }, + ) worker_thread.start() @@ -229,24 +227,9 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): :param message_id: message ID :return: """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # get conversation and message conversation = self._get_conversation(conversation_id) message = self._get_message(message_id) diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index f4aec3479b..7f4770fc97 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping, Sequence from typing import Any, Literal, Optional, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from sqlalchemy.orm import sessionmaker @@ -29,6 +29,7 @@ from core.workflow.repositories.workflow_execution_repository import WorkflowExe from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom @@ -209,19 +210,16 @@ class WorkflowAppGenerator(BaseAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - context=context, - workflow_thread_pool_id=workflow_thread_pool_id, - ) - - worker_thread = threading.Thread(target=worker_with_context) + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "context": context, + "workflow_thread_pool_id": workflow_thread_pool_id, + }, + ) worker_thread.start() @@ -408,24 +406,9 @@ class WorkflowAppGenerator(BaseAppGenerator): :param workflow_thread_pool_id: workflow thread pool id :return: """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # workflow app runner = WorkflowAppRunner( application_generate_entity=application_generate_entity, diff --git a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py index 2b47d179d2..dd196e1f09 100644 --- a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py +++ b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py @@ -80,6 +80,23 @@ class OceanBaseVector(BaseVector): self.delete() + vals = [] + params = self._client.perform_raw_text_sql("SHOW PARAMETERS LIKE '%ob_vector_memory_limit_percentage%'") + for row in params: + val = int(row[6]) + vals.append(val) + if len(vals) == 0: + raise ValueError("ob_vector_memory_limit_percentage not found in parameters.") + if any(val == 0 for val in vals): + try: + self._client.perform_raw_text_sql("ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30") + except Exception as e: + raise Exception( + "Failed to set ob_vector_memory_limit_percentage. " + + "Maybe the database user has insufficient privilege.", + e, + ) + cols = [ Column("id", String(36), primary_key=True, autoincrement=False), Column("vector", VECTOR(self._vec_dim)), @@ -110,22 +127,6 @@ class OceanBaseVector(BaseVector): + "to support fulltext index and vector index in the same table", e, ) - vals = [] - params = self._client.perform_raw_text_sql("SHOW PARAMETERS LIKE '%ob_vector_memory_limit_percentage%'") - for row in params: - val = int(row[6]) - vals.append(val) - if len(vals) == 0: - raise ValueError("ob_vector_memory_limit_percentage not found in parameters.") - if any(val == 0 for val in vals): - try: - self._client.perform_raw_text_sql("ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30") - except Exception as e: - raise Exception( - "Failed to set ob_vector_memory_limit_percentage. " - + "Maybe the database user has insufficient privilege.", - e, - ) redis_client.set(collection_exist_cache_key, 1, ex=3600) def _check_hybrid_search_support(self) -> bool: diff --git a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py index 8fe6199517..c6cf0d2b27 100644 --- a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py +++ b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py @@ -41,6 +41,12 @@ class WeaviateVector(BaseVector): weaviate.connect.connection.has_grpc = False + # Fix to minimize the performance impact of the deprecation check in weaviate-client 3.24.0, + # by changing the connection timeout to pypi.org from 1 second to 0.001 seconds. + # TODO: This can be removed once weaviate-client is updated to 3.26.7 or higher, + # which does not contain the deprecation check. + weaviate.connect.connection.PYPI_TIMEOUT = 0.001 + try: client = weaviate.Client( url=config.endpoint, auth_client_secret=auth_config, timeout_config=(5, 60), startup_period=None diff --git a/api/core/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 363b2ee920..875cee17e6 100644 --- a/api/core/workflow/graph_engine/graph_engine.py +++ b/api/core/workflow/graph_engine/graph_engine.py @@ -9,7 +9,7 @@ from copy import copy, deepcopy from datetime import UTC, datetime from typing import Any, Optional, cast -from flask import Flask, current_app, has_request_context +from flask import Flask, current_app from configs import dify_config from core.app.apps.base_app_queue_manager import GenerateTaskStoppedError @@ -53,6 +53,7 @@ from core.workflow.nodes.end.end_stream_processor import EndStreamProcessor from core.workflow.nodes.enums import ErrorStrategy, FailBranchSourceHandle from core.workflow.nodes.event import RunCompletedEvent, RunRetrieverResourceEvent, RunStreamChunkEvent from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING +from libs.flask_utils import preserve_flask_contexts from models.enums import UserFrom from models.workflow import WorkflowType @@ -537,24 +538,9 @@ class GraphEngine: """ Run parallel nodes """ - for var, val in context.items(): - var.set(val) - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - q.put( ParallelBranchRunStartedEvent( parallel_id=parallel_id, diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index faa8f90bea..22c564c1fc 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -214,7 +214,7 @@ class AgentNode(ToolNode): ) if tool_runtime.entity.description: tool_runtime.entity.description.llm = ( - extra.get("descrption", "") or tool_runtime.entity.description.llm + extra.get("description", "") or tool_runtime.entity.description.llm ) for tool_runtime_params in tool_runtime.entity.parameters: tool_runtime_params.form = ( diff --git a/api/core/workflow/nodes/event/event.py b/api/core/workflow/nodes/event/event.py index b72d111f49..3ebe80f245 100644 --- a/api/core/workflow/nodes/event/event.py +++ b/api/core/workflow/nodes/event/event.py @@ -6,7 +6,6 @@ from pydantic import BaseModel, Field from core.model_runtime.entities.llm_entities import LLMUsage from core.rag.entities.citation_metadata import RetrievalSourceMetadata from core.workflow.entities.node_entities import NodeRunResult -from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus class RunCompletedEvent(BaseModel): @@ -39,11 +38,3 @@ class RunRetryEvent(BaseModel): error: str = Field(..., description="error") retry_index: int = Field(..., description="Retry attempt number") start_at: datetime = Field(..., description="Retry start time") - - -class SingleStepRetryEvent(NodeRunResult): - """Single step retry event""" - - status: WorkflowNodeExecutionStatus = WorkflowNodeExecutionStatus.RETRY - - elapsed_time: float = Field(..., description="elapsed time") diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index 2592823540..42b6795fb0 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -7,7 +7,7 @@ from datetime import UTC, datetime from queue import Empty, Queue from typing import TYPE_CHECKING, Any, Optional, cast -from flask import Flask, current_app, has_request_context +from flask import Flask, current_app from configs import dify_config from core.variables import ArrayVariable, IntegerVariable, NoneVariable @@ -37,6 +37,7 @@ from core.workflow.nodes.base import BaseNode from core.workflow.nodes.enums import NodeType from core.workflow.nodes.event import NodeEvent, RunCompletedEvent from core.workflow.nodes.iteration.entities import ErrorHandleMode, IterationNodeData +from libs.flask_utils import preserve_flask_contexts from .exc import ( InvalidIteratorValueError, @@ -583,23 +584,8 @@ class IterationNode(BaseNode[IterationNodeData]): """ run single iteration in parallel mode """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user + with preserve_flask_contexts(flask_app, context_vars=context): parallel_mode_run_id = uuid.uuid4().hex graph_engine_copy = graph_engine.create_copy() variable_pool_copy = graph_engine_copy.graph_runtime_state.variable_pool diff --git a/api/factories/variable_factory.py b/api/factories/variable_factory.py index fa8a90e79f..a41ef4ae4e 100644 --- a/api/factories/variable_factory.py +++ b/api/factories/variable_factory.py @@ -39,10 +39,6 @@ from core.variables.variables import ( from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID -class InvalidSelectorError(ValueError): - pass - - class UnsupportedSegmentTypeError(Exception): pass diff --git a/api/libs/flask_utils.py b/api/libs/flask_utils.py new file mode 100644 index 0000000000..4ea2779584 --- /dev/null +++ b/api/libs/flask_utils.py @@ -0,0 +1,65 @@ +import contextvars +from collections.abc import Iterator +from contextlib import contextmanager +from typing import TypeVar + +from flask import Flask, g, has_request_context + +T = TypeVar("T") + + +@contextmanager +def preserve_flask_contexts( + flask_app: Flask, + context_vars: contextvars.Context, +) -> Iterator[None]: + """ + A context manager that handles: + 1. flask-login's UserProxy copy + 2. ContextVars copy + 3. flask_app.app_context() + + This context manager ensures that the Flask application context is properly set up, + the current user is preserved across context boundaries, and any provided context variables + are set within the new context. + + Note: + This manager aims to allow use current_user cross thread and app context, + but it's not the recommend use, it's better to pass user directly in parameters. + + Args: + flask_app: The Flask application instance + context_vars: contextvars.Context object containing context variables to be set in the new context + + Yields: + None + + Example: + ```python + with preserve_flask_contexts(flask_app, context_vars=context_vars): + # Code that needs Flask app context and context variables + # Current user will be preserved if available + ``` + """ + # Set context variables if provided + if context_vars: + for var, val in context_vars.items(): + var.set(val) + + # Save current user before entering new app context + saved_user = None + if has_request_context() and hasattr(g, "_login_user"): + saved_user = g._login_user + + # Enter Flask app context + with flask_app.app_context(): + try: + # Restore user in new app context if it was saved + if saved_user is not None: + g._login_user = saved_user + + # Yield control back to the caller + yield + finally: + # Any cleanup can be added here if needed + pass diff --git a/api/services/errors/__init__.py b/api/services/errors/__init__.py index eb1f055708..697e691224 100644 --- a/api/services/errors/__init__.py +++ b/api/services/errors/__init__.py @@ -4,7 +4,6 @@ from . import ( app_model_config, audio, base, - completion, conversation, dataset, document, @@ -19,7 +18,6 @@ __all__ = [ "app_model_config", "audio", "base", - "completion", "conversation", "dataset", "document", diff --git a/api/services/errors/account.py b/api/services/errors/account.py index 5aca12ffeb..4d3d150e07 100644 --- a/api/services/errors/account.py +++ b/api/services/errors/account.py @@ -55,7 +55,3 @@ class MemberNotInTenantError(BaseServiceError): class RoleAlreadyAssignedError(BaseServiceError): pass - - -class RateLimitExceededError(BaseServiceError): - pass diff --git a/api/services/errors/completion.py b/api/services/errors/completion.py deleted file mode 100644 index 7fc50a588e..0000000000 --- a/api/services/errors/completion.py +++ /dev/null @@ -1,5 +0,0 @@ -from services.errors.base import BaseServiceError - - -class CompletionStoppedError(BaseServiceError): - pass diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 3ccd14415d..58a4b2f179 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from configs import dify_config from core.helper.position_helper import is_filtered from core.model_runtime.utils.encoders import jsonable_encoder -from core.plugin.entities.plugin import GenericProviderID, ToolProviderID +from core.plugin.entities.plugin import ToolProviderID from core.plugin.impl.exc import PluginDaemonClientSideError from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity @@ -290,7 +290,7 @@ class BuiltinToolManageService: def _fetch_builtin_provider(provider_name: str, tenant_id: str) -> BuiltinToolProvider | None: try: full_provider_name = provider_name - provider_id_entity = GenericProviderID(provider_name) + provider_id_entity = ToolProviderID(provider_name) provider_name = provider_id_entity.provider_name if provider_id_entity.organization != "langgenius": provider_obj = ( @@ -315,7 +315,7 @@ class BuiltinToolManageService: if provider_obj is None: return None - provider_obj.provider = GenericProviderID(provider_obj.provider).to_string() + provider_obj.provider = ToolProviderID(provider_obj.provider).to_string() return provider_obj except Exception: # it's an old provider without organization diff --git a/api/tasks/retry_document_indexing_task.py b/api/tasks/retry_document_indexing_task.py index a6e7092216..8f8c3f9d81 100644 --- a/api/tasks/retry_document_indexing_task.py +++ b/api/tasks/retry_document_indexing_task.py @@ -30,11 +30,11 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str]): logging.info(click.style("Dataset not found: {}".format(dataset_id), fg="red")) db.session.close() return - + tenant_id = dataset.tenant_id for document_id in document_ids: retry_indexing_cache_key = "document_{}_is_retried".format(document_id) # check document limit - features = FeatureService.get_features(dataset.tenant_id) + features = FeatureService.get_features(tenant_id) try: if features.billing.enabled: vector_space = features.vector_space diff --git a/api/tests/integration_tests/vdb/oceanbase/check_oceanbase_ready.py b/api/tests/integration_tests/vdb/oceanbase/check_oceanbase_ready.py new file mode 100644 index 0000000000..94a51292ff --- /dev/null +++ b/api/tests/integration_tests/vdb/oceanbase/check_oceanbase_ready.py @@ -0,0 +1,49 @@ +import time + +import pymysql + + +def check_oceanbase_ready() -> bool: + try: + connection = pymysql.connect( + host="localhost", + port=2881, + user="root", + password="difyai123456", + ) + affected_rows = connection.query("SELECT 1") + return affected_rows == 1 + except Exception as e: + print(f"Oceanbase is not ready. Exception: {e}") + return False + finally: + if connection: + connection.close() + + +def main(): + max_attempts = 50 + retry_interval_seconds = 2 + is_oceanbase_ready = False + for attempt in range(max_attempts): + try: + is_oceanbase_ready = check_oceanbase_ready() + except Exception as e: + print(f"Oceanbase is not ready. Exception: {e}") + is_oceanbase_ready = False + + if is_oceanbase_ready: + break + else: + print(f"Attempt {attempt + 1} failed, retry in {retry_interval_seconds} seconds...") + time.sleep(retry_interval_seconds) + + if is_oceanbase_ready: + print("Oceanbase is ready.") + else: + print(f"Oceanbase is not ready after {max_attempts} attempting checks.") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/api/tests/integration_tests/vdb/oceanbase/test_oceanbase.py b/api/tests/integration_tests/vdb/oceanbase/test_oceanbase.py index ebcb134168..8fbbbe61b8 100644 --- a/api/tests/integration_tests/vdb/oceanbase/test_oceanbase.py +++ b/api/tests/integration_tests/vdb/oceanbase/test_oceanbase.py @@ -1,15 +1,11 @@ -from unittest.mock import MagicMock, patch - import pytest from core.rag.datasource.vdb.oceanbase.oceanbase_vector import ( OceanBaseVector, OceanBaseVectorConfig, ) -from tests.integration_tests.vdb.__mock.tcvectordb import setup_tcvectordb_mock from tests.integration_tests.vdb.test_vector_store import ( AbstractVectorTest, - get_example_text, setup_mock_redis, ) @@ -20,10 +16,11 @@ def oceanbase_vector(): "dify_test_collection", config=OceanBaseVectorConfig( host="127.0.0.1", - port="2881", - user="root@test", + port=2881, + user="root", database="test", - password="test", + password="difyai123456", + enable_hybrid_search=True, ), ) @@ -33,39 +30,13 @@ class OceanBaseVectorTest(AbstractVectorTest): super().__init__() self.vector = vector - def search_by_vector(self): - hits_by_vector = self.vector.search_by_vector(query_vector=self.example_embedding) - assert len(hits_by_vector) == 0 - - def search_by_full_text(self): - hits_by_full_text = self.vector.search_by_full_text(query=get_example_text()) - assert len(hits_by_full_text) == 0 - - def text_exists(self): - exist = self.vector.text_exists(self.example_doc_id) - assert exist == True - def get_ids_by_metadata_field(self): ids = self.vector.get_ids_by_metadata_field(key="document_id", value=self.example_doc_id) - assert len(ids) == 0 - - -@pytest.fixture -def setup_mock_oceanbase_client(): - with patch("core.rag.datasource.vdb.oceanbase.oceanbase_vector.ObVecClient", new_callable=MagicMock) as mock_client: - yield mock_client - - -@pytest.fixture -def setup_mock_oceanbase_vector(oceanbase_vector): - with patch.object(oceanbase_vector, "_client"): - yield oceanbase_vector + assert len(ids) == 1 def test_oceanbase_vector( setup_mock_redis, - setup_mock_oceanbase_client, - setup_mock_oceanbase_vector, oceanbase_vector, ): OceanBaseVectorTest(oceanbase_vector).run_all_tests() diff --git a/api/tests/unit_tests/libs/test_flask_utils.py b/api/tests/unit_tests/libs/test_flask_utils.py new file mode 100644 index 0000000000..fb46ba50f3 --- /dev/null +++ b/api/tests/unit_tests/libs/test_flask_utils.py @@ -0,0 +1,124 @@ +import contextvars +import threading +from typing import Optional + +import pytest +from flask import Flask +from flask_login import LoginManager, UserMixin, current_user, login_user + +from libs.flask_utils import preserve_flask_contexts + + +class User(UserMixin): + """Simple User class for testing.""" + + def __init__(self, id: str): + self.id = id + + def get_id(self) -> str: + return self.id + + +@pytest.fixture +def login_app(app: Flask) -> Flask: + """Set up a Flask app with flask-login.""" + # Set a secret key for the app + app.config["SECRET_KEY"] = "test-secret-key" + + login_manager = LoginManager() + login_manager.init_app(app) + + @login_manager.user_loader + def load_user(user_id: str) -> Optional[User]: + if user_id == "test_user": + return User("test_user") + return None + + return app + + +@pytest.fixture +def test_user() -> User: + """Create a test user.""" + return User("test_user") + + +def test_current_user_not_accessible_across_threads(login_app: Flask, test_user: User): + """ + Test that current_user is not accessible in a different thread without preserve_flask_contexts. + + This test demonstrates that without the preserve_flask_contexts, we cannot access + current_user in a different thread, even with app_context. + """ + # Log in the user in the main thread + with login_app.test_request_context(): + login_user(test_user) + assert current_user.is_authenticated + assert current_user.id == "test_user" + + # Store the result of the thread execution + result = {"user_accessible": True, "error": None} + + # Define a function to run in a separate thread + def check_user_in_thread(): + try: + # Try to access current_user in a different thread with app_context + with login_app.app_context(): + # This should fail because current_user is not accessible across threads + # without preserve_flask_contexts + result["user_accessible"] = current_user.is_authenticated + except Exception as e: + result["error"] = str(e) # type: ignore + + # Run the function in a separate thread + thread = threading.Thread(target=check_user_in_thread) + thread.start() + thread.join() + + # Verify that we got an error or current_user is not authenticated + assert result["error"] is not None or (result["user_accessible"] is not None and not result["user_accessible"]) + + +def test_current_user_accessible_with_preserve_flask_contexts(login_app: Flask, test_user: User): + """ + Test that current_user is accessible in a different thread with preserve_flask_contexts. + + This test demonstrates that with the preserve_flask_contexts, we can access + current_user in a different thread. + """ + # Log in the user in the main thread + with login_app.test_request_context(): + login_user(test_user) + assert current_user.is_authenticated + assert current_user.id == "test_user" + + # Save the context variables + context_vars = contextvars.copy_context() + + # Store the result of the thread execution + result = {"user_accessible": False, "user_id": None, "error": None} + + # Define a function to run in a separate thread + def check_user_in_thread_with_manager(): + try: + # Use preserve_flask_contexts to access current_user in a different thread + with preserve_flask_contexts(login_app, context_vars): + from flask_login import current_user + + if current_user: + result["user_accessible"] = True + result["user_id"] = current_user.id + else: + result["user_accessible"] = False + except Exception as e: + result["error"] = str(e) # type: ignore + + # Run the function in a separate thread + thread = threading.Thread(target=check_user_in_thread_with_manager) + thread.start() + thread.join() + + # Verify that current_user is accessible and has the correct ID + assert result["error"] is None + assert result["user_accessible"] is True + assert result["user_id"] == "test_user" diff --git a/docker/.env.example b/docker/.env.example index 4cf5e202d0..d4d59936eb 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1067,6 +1067,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets # Plugin oss bucket PLUGIN_STORAGE_OSS_BUCKET= # Plugin oss s3 credentials +PLUGIN_S3_USE_AWS= PLUGIN_S3_USE_AWS_MANAGED_IAM=false PLUGIN_S3_ENDPOINT= PLUGIN_S3_USE_PATH_STYLE=false diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index a409a729ce..55e1b55599 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.2 + image: langgenius/dify-api:1.4.3 restart: always environment: # Use the shared environment variables. @@ -31,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.2 + image: langgenius/dify-api:1.4.3 restart: always environment: # Use the shared environment variables. @@ -57,7 +57,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.2 + image: langgenius/dify-web:1.4.3 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -168,6 +168,7 @@ services: PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} @@ -434,7 +435,7 @@ services: # OceanBase vector database oceanbase: - image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818 + image: oceanbase/oceanbase-ce:4.3.5-lts container_name: oceanbase profiles: - oceanbase @@ -449,9 +450,7 @@ services: OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456} OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} OB_SERVER_IP: 127.0.0.1 - MODE: MINI - ports: - - "${OCEANBASE_VECTOR_PORT:-2881}:2881" + MODE: mini # Oracle vector database oracle: diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index dceee484ca..4081bfd818 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -104,6 +104,7 @@ services: PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages} PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index d927334118..dddce106b9 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -467,6 +467,7 @@ x-shared-env: &shared-api-worker-env PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages} PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} + PLUGIN_S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-} PLUGIN_S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} PLUGIN_S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} PLUGIN_S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} @@ -508,7 +509,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.2 + image: langgenius/dify-api:1.4.3 restart: always environment: # Use the shared environment variables. @@ -537,7 +538,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.2 + image: langgenius/dify-api:1.4.3 restart: always environment: # Use the shared environment variables. @@ -563,7 +564,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.2 + image: langgenius/dify-web:1.4.3 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -674,6 +675,7 @@ services: PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} @@ -940,7 +942,7 @@ services: # OceanBase vector database oceanbase: - image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818 + image: oceanbase/oceanbase-ce:4.3.5-lts container_name: oceanbase profiles: - oceanbase @@ -955,9 +957,7 @@ services: OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456} OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} OB_SERVER_IP: 127.0.0.1 - MODE: MINI - ports: - - "${OCEANBASE_VECTOR_PORT:-2881}:2881" + MODE: mini # Oracle vector database oracle: diff --git a/docker/middleware.env.example b/docker/middleware.env.example index 66037f281c..338b057ae8 100644 --- a/docker/middleware.env.example +++ b/docker/middleware.env.example @@ -133,6 +133,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets PLUGIN_STORAGE_OSS_BUCKET= # Plugin oss s3 credentials PLUGIN_S3_USE_AWS_MANAGED_IAM=false +PLUGIN_S3_USE_AWS= PLUGIN_S3_ENDPOINT= PLUGIN_S3_USE_PATH_STYLE=false PLUGIN_AWS_ACCESS_KEY= diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx index fc97f5e669..e0c09e739e 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx @@ -15,7 +15,7 @@ const Overview = async (props: IDevelopProps) => { } = params return ( -
+
{ - const locale = getLocaleOnClient() const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile) const { t } = useTranslation() + const docLink = useDocLink() const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0 const relatedAppsTotal = relatedApps?.data?.length || 0 @@ -97,11 +96,7 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
{t('common.datasetMenus.emptyTip')}
diff --git a/web/app/(commonLayout)/datasets/layout.tsx b/web/app/(commonLayout)/datasets/layout.tsx index aecb537aa6..b3fc5147b5 100644 --- a/web/app/(commonLayout)/datasets/layout.tsx +++ b/web/app/(commonLayout)/datasets/layout.tsx @@ -1,9 +1,23 @@ 'use client' +import Loading from '@/app/components/base/loading' +import { useAppContext } from '@/context/app-context' import { ExternalApiPanelProvider } from '@/context/external-api-panel-context' import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context' +import { useRouter } from 'next/navigation' +import { useEffect } from 'react' export default function DatasetsLayout({ children }: { children: React.ReactNode }) { + const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() + const router = useRouter() + + useEffect(() => { + if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator) + router.replace('/apps') + }, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, router]) + + if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator) + return return ( diff --git a/web/app/(commonLayout)/datasets/template/template.en.mdx b/web/app/(commonLayout)/datasets/template/template.en.mdx index 4d00b7b2b5..e1ff827c96 100644 --- a/web/app/(commonLayout)/datasets/template/template.en.mdx +++ b/web/app/(commonLayout)/datasets/template/template.en.mdx @@ -54,7 +54,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi Index mode - - high_quality High quality: embedding using embedding model, built as vector database index + - high_quality High quality: Embedding using embedding model, built as vector database index - economy Economy: Build using inverted index of keyword table index diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx index d121a93df2..3994356b51 100644 --- a/web/app/(commonLayout)/datasets/template/template.zh.mdx +++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx @@ -55,7 +55,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi 索引方式 - high_quality 高质量:使用 - ding 模型进行嵌入,构建为向量数据库索引 + Embedding 模型进行嵌入,构建为向量数据库索引 - economy 经济:使用 keyword table index 的倒排索引进行构建 diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx index 7de5d51edb..78b8835009 100644 --- a/web/app/(shareLayout)/layout.tsx +++ b/web/app/(shareLayout)/layout.tsx @@ -19,7 +19,7 @@ const Layout: FC<{ const [isLoading, setIsLoading] = useState(true) useEffect(() => { (async () => { - if (!systemFeatures.webapp_auth.enabled) { + if (!isGlobalPending && !systemFeatures.webapp_auth.enabled) { setIsLoading(false) return } @@ -37,7 +37,7 @@ const Layout: FC<{ setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC) setIsLoading(false) })() - }, [pathname, redirectUrl, setWebAppAccessMode]) + }, [pathname, redirectUrl, setWebAppAccessMode, isGlobalPending, systemFeatures.webapp_auth.enabled]) if (isLoading || isGlobalPending) { return
diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx index 592c95261c..94a02945bb 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx @@ -1,13 +1,11 @@ 'use client' import type { FC } from 'react' import React from 'react' -import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' import OperationBtn from '@/app/components/app/configuration/base/operation-btn' import Panel from '@/app/components/app/configuration/base/feature-panel' import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { showWarning: boolean @@ -19,7 +17,7 @@ const HistoryPanel: FC = ({ onShowEditModal, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() return ( = ({ {showWarning && (
{t('appDebug.feature.conversationHistory.tip')} - {t('appDebug.feature.conversationHistory.learnMore')} diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 3170d33a82..9835481ae0 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -31,6 +31,7 @@ import { import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { fetchMembers } from '@/service/common' import type { Member } from '@/models/common' +import { useDocLink } from '@/context/i18n' type SettingsModalProps = { currentDataset: DataSet @@ -58,6 +59,7 @@ const SettingsModal: FC = ({ currentModel: isRerankDefaultModelValid, } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank) const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const ref = useRef(null) const isExternal = currentDataset.provider === 'external' @@ -328,7 +330,7 @@ const SettingsModal: FC = ({
{t('datasetSettings.form.retrievalSetting.title')}
diff --git a/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx b/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx index cca775c86e..f207cddd16 100644 --- a/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx +++ b/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx @@ -2,9 +2,7 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { onReturnToSimpleMode: () => void } @@ -13,7 +11,7 @@ const AdvancedModeWarning: FC = ({ onReturnToSimpleMode, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const [show, setShow] = React.useState(true) if (!show) return null @@ -25,7 +23,7 @@ const AdvancedModeWarning: FC = ({ {t('appDebug.promptMode.advancedWarning.description')} {t('appDebug.promptMode.advancedWarning.learnMore')} diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index ee4bd57325..3fd020f60f 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -20,6 +20,7 @@ import type { import { useToastContext } from '@/app/components/base/toast' import AppIcon from '@/app/components/base/app-icon' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const systemTypes = ['api'] type ExternalDataToolModalProps = { @@ -40,6 +41,7 @@ const ExternalDataToolModal: FC = ({ onValidateBeforeSave, }) => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const { locale } = useContext(I18n) const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' }) @@ -243,7 +245,7 @@ const ExternalDataToolModal: FC = ({
{t('common.apiBasedExtension.selector.title')} diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 6e5547d08a..f0a0da41a5 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -29,6 +29,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { getRedirection } from '@/utils/app-redirection' import FullScreenModal from '@/app/components/base/fullscreen-modal' import useTheme from '@/hooks/use-theme' +import { useDocLink } from '@/context/i18n' type CreateAppProps = { onSuccess: () => void @@ -303,31 +304,41 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP function AppPreview({ mode }: { mode: AppMode }) { const { t } = useTranslation() + const docLink = useDocLink() const modeToPreviewInfoMap = { 'chat': { title: t('app.types.chatbot'), description: t('app.newApp.chatbotUserDescription'), - link: 'https://docs.dify.ai/guides/application-orchestrate/readme', + link: docLink('/guides/application-orchestrate/chatbot-application'), }, 'advanced-chat': { title: t('app.types.advanced'), description: t('app.newApp.advancedUserDescription'), - link: 'https://docs.dify.ai/en/guides/workflow/README', + link: docLink('/guides/workflow/README', { + 'zh-Hans': '/guides/workflow/readme', + 'ja-JP': '/guides/workflow/concepts', + }), }, 'agent-chat': { title: t('app.types.agent'), description: t('app.newApp.agentUserDescription'), - link: 'https://docs.dify.ai/en/guides/application-orchestrate/agent', + link: docLink('/guides/application-orchestrate/agent'), }, 'completion': { title: t('app.newApp.completeApp'), description: t('app.newApp.completionUserDescription'), - link: null, + link: docLink('/guides/application-orchestrate/text-generator', { + 'zh-Hans': '/guides/application-orchestrate/readme', + 'ja-JP': '/guides/application-orchestrate/README', + }), }, 'workflow': { title: t('app.types.workflow'), description: t('app.newApp.workflowUserDescription'), - link: 'https://docs.dify.ai/en/guides/workflow/README', + link: docLink('/guides/workflow/README', { + 'zh-Hans': '/guides/workflow/readme', + 'ja-JP': '/guides/workflow/concepts', + }), }, } const previewInfo = modeToPreviewInfoMap[mode] diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 3062e3a911..208fddecd1 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -354,7 +354,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { } useEffect(() => { - adjustModalWidth() + const raf = requestAnimationFrame(adjustModalWidth) + return () => cancelAnimationFrame(raf) }, []) return ( diff --git a/web/app/components/app/overview/customize/index.tsx b/web/app/components/app/overview/customize/index.tsx index 4e84dd8b1f..0fedd76f89 100644 --- a/web/app/components/app/overview/customize/index.tsx +++ b/web/app/components/app/overview/customize/index.tsx @@ -3,13 +3,11 @@ import type { FC } from 'react' import React from 'react' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' +import { useDocLink } from '@/context/i18n' import type { AppMode } from '@/types/app' -import I18n from '@/context/i18n' import Button from '@/app/components/base/button' import Modal from '@/app/components/base/modal' import Tag from '@/app/components/base/tag' -import { LanguagesSupported } from '@/i18n/language' type IShareLinkProps = { isShow: boolean @@ -43,7 +41,7 @@ const CustomizeModal: FC = ({ mode, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const isChatApp = mode === 'chat' || mode === 'advanced-chat' return = ({ className='mt-2' onClick={() => window.open( - `https://docs.dify.ai/${locale !== LanguagesSupported[1] - ? 'user-guide/launching-dify-apps/developing-with-apis' - : `${locale.toLowerCase()}/guides/application-publishing/developing-with-apis` - }`, + docLink('/guides/application-publishing/developing-with-apis'), '_blank', ) } diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 98e2cab681..524c340a53 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from 'react' import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react' import Link from 'next/link' import { Trans, useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { SparklesSoft } from '@/app/components/base/icons/src/public/common' import Modal from '@/app/components/base/modal' import ActionButton from '@/app/components/base/action-button' @@ -19,14 +18,14 @@ import { SimpleSelect } from '@/app/components/base/select' import type { AppDetailResponse } from '@/models/app' import type { AppIconType, AppSSO, Language } from '@/types/app' import { useToastContext } from '@/app/components/base/toast' -import { LanguagesSupported, languages } from '@/i18n/language' +import { languages } from '@/i18n/language' import Tooltip from '@/app/components/base/tooltip' import { useProviderContext } from '@/context/provider-context' import { useModalContext } from '@/context/modal-context' import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker' -import I18n from '@/context/i18n' import cn from '@/utils/classnames' +import { useDocLink } from '@/context/i18n' export type ISettingsModalProps = { isChat: boolean @@ -98,7 +97,7 @@ const SettingsModal: FC = ({ const [language, setLanguage] = useState(default_language) const [saveLoading, setSaveLoading] = useState(false) const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [appIcon, setAppIcon] = useState( @@ -238,7 +237,10 @@ const SettingsModal: FC = ({
{t(`${prefixSettings}.modalTip`)} - {t('common.operation.learnMore')} + {t('common.operation.learnMore')}
{/* form body */} diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index eee680596b..d00cd9038a 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -1,6 +1,5 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { RiCloseLine, RiInformation2Fill } from '@remixicon/react' import DialogWrapper from '@/app/components/base/features/new-feature-panel/dialog-wrapper' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -19,8 +18,7 @@ import Moderation from '@/app/components/base/features/new-feature-panel/moderat import AnnotationReply from '@/app/components/base/features/new-feature-panel/annotation-reply' import type { PromptVariable } from '@/models/debug' import type { InputVar } from '@/app/components/workflow/types' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { show: boolean @@ -48,7 +46,7 @@ const NewFeaturePanel = ({ onAutoAddPromptVariable, }: Props) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts) @@ -80,7 +78,7 @@ const NewFeaturePanel = ({ {isChatMode ? t('workflow.common.fileUploadTip') : t('workflow.common.ImageUploadLegacyTip')} {t('workflow.common.featuresDocLink')}
diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index ab5200f38f..3ede2d7c6b 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -25,6 +25,7 @@ import { useModalContext } from '@/context/modal-context' import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import cn from '@/utils/classnames' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const systemTypes = ['openai_moderation', 'keywords', 'api'] @@ -46,6 +47,7 @@ const ModerationSettingModal: FC = ({ onSave, }) => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const { locale } = useContext(I18n) const { data: modelProviders, isLoading, mutate } = useSWR('/workspaces/current/model-providers', fetchModelProviders) @@ -316,7 +318,7 @@ const ModerationSettingModal: FC = ({ ) diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index 52c5195bbf..c931addd1a 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -63,6 +63,7 @@ import CustomDialog from '@/app/components/base/dialog' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const TextLabel: FC = (props) => { return @@ -146,6 +147,7 @@ const StepTwo = ({ updateRetrievalMethodCache, }: StepTwoProps) => { const { t } = useTranslation() + const docLink = useDocLink() const { locale } = useContext(I18n) const media = useBreakpoints() const isMobile = media === MediaType.mobile @@ -962,7 +964,9 @@ const StepTwo = ({ diff --git a/web/app/components/datasets/create/website/base/url-input.tsx b/web/app/components/datasets/create/website/base/url-input.tsx index b7dc9bfca5..ab965bebc3 100644 --- a/web/app/components/datasets/create/website/base/url-input.tsx +++ b/web/app/components/datasets/create/website/base/url-input.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from './input' import Button from '@/app/components/base/button' +import { useDocLink } from '@/context/i18n' const I18N_PREFIX = 'datasetCreation.stepOne.website' @@ -17,6 +18,7 @@ const UrlInput: FC = ({ onRun, }) => { const { t } = useTranslation() + const docLink = useDocLink() const [url, setUrl] = useState('') const handleUrlChange = useCallback((url: string | number) => { setUrl(url as string) @@ -32,7 +34,7 @@ const UrlInput: FC = ({ +
{t('tools.test.testResult')}
diff --git a/web/app/components/tools/provider/custom-create-card.tsx b/web/app/components/tools/provider/custom-create-card.tsx index 6dd268cb3a..f64daba8cd 100644 --- a/web/app/components/tools/provider/custom-create-card.tsx +++ b/web/app/components/tools/provider/custom-create-card.tsx @@ -14,6 +14,7 @@ import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-m import { createCustomCollection } from '@/service/tools' import Toast from '@/app/components/base/toast' import { useAppContext } from '@/context/app-context' +import { useDocLink } from '@/context/i18n' type Props = { onRefreshData: () => void @@ -25,10 +26,11 @@ const Contribute = ({ onRefreshData }: Props) => { const language = getLanguage(locale) const { isCurrentWorkspaceManager } = useAppContext() + const docLink = useDocLink() const linkUrl = useMemo(() => { - if (language.startsWith('zh_')) - return 'https://docs.dify.ai/zh-hans/guides/tools#ru-he-chuang-jian-zi-ding-yi-gong-ju' - return 'https://docs.dify.ai/en/guides/tools#how-to-create-custom-tools' + return docLink('/guides/tools#how-to-create-custom-tools', { + 'zh-Hans': '/guides/tools#ru-he-chuang-jian-zi-ding-yi-gong-ju', + }) }, [language]) const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false) diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 8b1003e89c..0d0ba80fe1 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -61,7 +61,7 @@ export const useShortcuts = (): void => { return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement) }, [workflowStore]) - useKeyPress(['delete', 'backspace'], (e) => { + useKeyPress(['delete'], (e) => { if (shouldHandleShortcut(e)) { e.preventDefault() handleNodesDelete() diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index de23602e34..674c768aa5 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -19,9 +19,7 @@ import { useWorkflowStore } from '../../../store' import { useRenderI18nObject } from '@/hooks/use-i18n' import type { NodeOutPutVar } from '../../../types' import type { Node } from 'reactflow' -import { useContext } from 'use-context-selector' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' export type Strategy = { agent_strategy_provider_name: string @@ -52,7 +50,7 @@ type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema export const AgentStrategy = memo((props: AgentStrategyProps) => { const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration) const renderI18nObject = useRenderI18nObject() const workflowStore = useWorkflowStore() @@ -223,11 +221,11 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { title={t('workflow.nodes.agent.strategy.configureTip')} description={
{t('workflow.nodes.agent.strategy.configureTipDesc')}
- + {t('workflow.nodes.agent.learnMore')}
} diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index 1e195c5d40..3540c60a39 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -140,6 +140,7 @@ const CodeEditor: FC = ({ language={languageMap[language] || 'javascript'} theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme value={outPutValue} + loading={Loading...} onChange={handleEditorChange} // https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html options={{ diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx index 51969f8510..f9292be477 100644 --- a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx +++ b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx @@ -5,6 +5,7 @@ import Input from '@/app/components/base/input' import { VarType } from '@/app/components/workflow/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { useDocLink } from '@/context/i18n' type DefaultValueProps = { forms: DefaultValueForm[] @@ -15,6 +16,7 @@ const DefaultValue = ({ onFormChange, }: DefaultValueProps) => { const { t } = useTranslation() + const docLink = useDocLink() const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => { return (payload: any) => { let value @@ -34,7 +36,9 @@ const DefaultValue = ({ {t('workflow.nodes.common.errorHandle.defaultValue.desc')}  
diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx index 05a6cb96af..fa9cff3dc8 100644 --- a/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx +++ b/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx @@ -1,8 +1,10 @@ import { RiMindMap } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import { useDocLink } from '@/context/i18n' const FailBranchCard = () => { const { t } = useTranslation() + const docLink = useDocLink() return (
@@ -17,7 +19,7 @@ const FailBranchCard = () => { {t('workflow.nodes.common.errorHandle.failBranch.customizeTip')}   diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index 72e9384d5f..181b278051 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -8,6 +8,8 @@ import VarReferencePicker from './var-reference-picker' import Input from '@/app/components/base/input' import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' +import { checkKeys } from '@/utils/var' +import Toast from '@/app/components/base/toast' type Props = { nodeId: string @@ -36,9 +38,27 @@ const VarList: FC = ({ const handleVarNameChange = useCallback((index: number) => { return (e: React.ChangeEvent) => { - onVarNameChange?.(list[index].variable, e.target.value) + const newKey = e.target.value + const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true) + if (!isValid) { + Toast.notify({ + type: 'error', + message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + }) + return + } + + if (list.map(item => item.variable?.trim()).includes(newKey.trim())) { + Toast.notify({ + type: 'error', + message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), + }) + return + } + + onVarNameChange?.(list[index].variable, newKey) const newList = produce(list, (draft) => { - draft[index].variable = e.target.value + draft[index].variable = newKey }) onChange(newList) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx index e35977ae31..9398ae7361 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx @@ -2,12 +2,10 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import VarReferenceVars from './var-reference-vars' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import ListEmpty from '@/app/components/base/list-empty' -import { LanguagesSupported } from '@/i18n/language' -import I18n from '@/context/i18n' +import { useDocLink } from '@/context/i18n' type Props = { vars: NodeOutPutVar[] @@ -24,7 +22,7 @@ const VarReferencePopup: FC = ({ isSupportFileVar = true, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() // max-h-[300px] overflow-y-auto todo: use portal to handle long list return (
= ({ {t('workflow.variableReference.assignedVarsDescription')} {t('workflow.variableReference.conversationVars')} + href={docLink('/guides/workflow/variables#conversation-variables', { + 'zh-Hans': '/guides/workflow/variables#会话变量', + 'ja-JP': '/guides/workflow/variables#会話変数', + })}> + {t('workflow.variableReference.conversationVars')} +
} /> )) diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 9acbdf4ff7..27063a2ba3 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -260,6 +260,7 @@ type Props = { maxHeightClass?: string onClose?: () => void onBlur?: () => void + autoFocus?: boolean } const VarReferenceVars: FC = ({ hideSearch, @@ -271,6 +272,7 @@ const VarReferenceVars: FC = ({ maxHeightClass, onClose, onBlur, + autoFocus = true, }) => { const { t } = useTranslation() const [searchText, setSearchText] = useState('') @@ -323,7 +325,7 @@ const VarReferenceVars: FC = ({ onKeyDown={handleKeyDown} onClear={() => setSearchText('')} onBlur={onBlur} - autoFocus + autoFocus={autoFocus} />
{ const language = useGetLanguage() + const docLink = useDocLink() const prefixLink = useMemo(() => { - if (language === 'zh_Hans') - return 'https://docs.dify.ai/zh-hans/guides/workflow/node/' - - return 'https://docs.dify.ai/en/guides/workflow/node/' + return docLink('/guides/workflow/node/') }, [language]) const linkMap = useMemo(() => { if (language === 'zh_Hans') { diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx index 2b8574b285..fecd1093d9 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx @@ -21,8 +21,8 @@ import { MittProvider, VisualEditorContextProvider, useMittContext } from './vis import ErrorMessage from './error-message' import { useVisualEditorStore } from './visual-editor/store' import Toast from '@/app/components/base/toast' -import { useGetDocLanguage } from '@/context/i18n' import { JSON_SCHEMA_MAX_DEPTH } from '@/config' +import { useDocLink } from '@/context/i18n' type JsonSchemaConfigProps = { defaultSchema?: SchemaRoot @@ -53,7 +53,7 @@ const JsonSchemaConfig: FC = ({ onClose, }) => { const { t } = useTranslation() - const docLanguage = useGetDocLanguage() + const docLink = useDocLink() const [currentTab, setCurrentTab] = useState(SchemaView.VisualEditor) const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA) const [json, setJson] = useState(JSON.stringify(jsonSchema, null, 2)) @@ -252,7 +252,7 @@ const JsonSchemaConfig: FC = ({
diff --git a/web/app/components/workflow/panel/chat-variable-panel/index.tsx b/web/app/components/workflow/panel/chat-variable-panel/index.tsx index ad00bddd0c..bbf39489dd 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/index.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/index.tsx @@ -3,7 +3,6 @@ import { useCallback, useState, } from 'react' -import { useContext } from 'use-context-selector' import { useStoreApi, } from 'reactflow' @@ -22,13 +21,12 @@ import type { import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft' import { BlockEnum } from '@/app/components/workflow/types' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' import cn from '@/utils/classnames' const ChatVariablePanel = () => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const store = useStoreApi() const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel) const varList = useStore(s => s.conversationVariables) as ConversationVariable[] @@ -139,7 +137,13 @@ const ChatVariablePanel = () => {
TIPS
@@ -166,7 +170,7 @@ const ChatVariablePanel = () => {
-
+
)} diff --git a/web/app/components/workflow/panel/env-panel/variable-modal.tsx b/web/app/components/workflow/panel/env-panel/variable-modal.tsx index 4546aabae6..c842554948 100644 --- a/web/app/components/workflow/panel/env-panel/variable-modal.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-modal.tsx @@ -139,7 +139,7 @@ const VariableModal = ({
{ type !== 'number' ?