From 759a932bb7f2b4988730a673ccd9396ad11748f6 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 21 Oct 2025 15:17:17 +0800 Subject: [PATCH 01/33] Fix: release WorkflowTool database sessions promptly (#26893) --- api/.env.example | 3 + api/core/tools/tool_manager.py | 3 +- api/core/tools/workflow_as_tool/provider.py | 87 +++++++++++-------- api/core/tools/workflow_as_tool/tool.py | 25 +++--- api/models/tools.py | 2 +- .../tools/workflow_tools_manage_service.py | 33 +++---- .../tools/test_tools_transform_service.py | 3 + docker/.env.example | 12 +++ docker/docker-compose-template.yaml | 2 + docker/docker-compose.middleware.yaml | 2 + docker/docker-compose.yaml | 4 + docker/middleware.env.example | 12 +++ 12 files changed, 123 insertions(+), 65 deletions(-) diff --git a/api/.env.example b/api/.env.example index 1d8190ce5f..4df6adf348 100644 --- a/api/.env.example +++ b/api/.env.example @@ -434,6 +434,9 @@ CODE_EXECUTION_SSL_VERIFY=True CODE_EXECUTION_POOL_MAX_CONNECTIONS=100 CODE_EXECUTION_POOL_MAX_KEEPALIVE_CONNECTIONS=20 CODE_EXECUTION_POOL_KEEPALIVE_EXPIRY=5.0 +CODE_EXECUTION_CONNECT_TIMEOUT=10 +CODE_EXECUTION_READ_TIMEOUT=60 +CODE_EXECUTION_WRITE_TIMEOUT=10 CODE_MAX_NUMBER=9223372036854775807 CODE_MIN_NUMBER=-9223372036854775808 CODE_MAX_STRING_LENGTH=400000 diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 006cf856d5..5c414915f4 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -326,7 +326,8 @@ class ToolManager: workflow_provider_stmt = select(WorkflowToolProvider).where( WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == provider_id ) - workflow_provider = db.session.scalar(workflow_provider_stmt) + with Session(db.engine, expire_on_commit=False) as session, session.begin(): + workflow_provider = session.scalar(workflow_provider_stmt) if workflow_provider is None: raise ToolProviderNotFoundError(f"workflow provider {provider_id} not found") diff --git a/api/core/tools/workflow_as_tool/provider.py b/api/core/tools/workflow_as_tool/provider.py index e514c8c57b..d7afbc7389 100644 --- a/api/core/tools/workflow_as_tool/provider.py +++ b/api/core/tools/workflow_as_tool/provider.py @@ -1,6 +1,7 @@ from collections.abc import Mapping from pydantic import Field +from sqlalchemy.orm import Session from core.app.app_config.entities import VariableEntity, VariableEntityType from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager @@ -20,6 +21,7 @@ from core.tools.entities.tool_entities import ( from core.tools.utils.workflow_configuration_sync import WorkflowToolConfigurationUtils from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db +from models.account import Account from models.model import App, AppMode from models.tools import WorkflowToolProvider from models.workflow import Workflow @@ -44,29 +46,34 @@ class WorkflowToolProviderController(ToolProviderController): @classmethod def from_db(cls, db_provider: WorkflowToolProvider) -> "WorkflowToolProviderController": - app = db_provider.app + with Session(db.engine, expire_on_commit=False) as session, session.begin(): + provider = session.get(WorkflowToolProvider, db_provider.id) if db_provider.id else None + if not provider: + raise ValueError("workflow provider not found") + app = session.get(App, provider.app_id) + if not app: + raise ValueError("app not found") - if not app: - raise ValueError("app not found") + user = session.get(Account, provider.user_id) if provider.user_id else None - controller = WorkflowToolProviderController( - entity=ToolProviderEntity( - identity=ToolProviderIdentity( - author=db_provider.user.name if db_provider.user_id and db_provider.user else "", - name=db_provider.label, - label=I18nObject(en_US=db_provider.label, zh_Hans=db_provider.label), - description=I18nObject(en_US=db_provider.description, zh_Hans=db_provider.description), - icon=db_provider.icon, + controller = WorkflowToolProviderController( + entity=ToolProviderEntity( + identity=ToolProviderIdentity( + author=user.name if user else "", + name=provider.label, + label=I18nObject(en_US=provider.label, zh_Hans=provider.label), + description=I18nObject(en_US=provider.description, zh_Hans=provider.description), + icon=provider.icon, + ), + credentials_schema=[], + plugin_id=None, ), - credentials_schema=[], - plugin_id=None, - ), - provider_id=db_provider.id or "", - ) + provider_id=provider.id or "", + ) - # init tools - - controller.tools = [controller._get_db_provider_tool(db_provider, app)] + controller.tools = [ + controller._get_db_provider_tool(provider, app, session=session, user=user), + ] return controller @@ -74,7 +81,14 @@ class WorkflowToolProviderController(ToolProviderController): def provider_type(self) -> ToolProviderType: return ToolProviderType.WORKFLOW - def _get_db_provider_tool(self, db_provider: WorkflowToolProvider, app: App) -> WorkflowTool: + def _get_db_provider_tool( + self, + db_provider: WorkflowToolProvider, + app: App, + *, + session: Session, + user: Account | None = None, + ) -> WorkflowTool: """ get db provider tool :param db_provider: the db provider @@ -82,7 +96,7 @@ class WorkflowToolProviderController(ToolProviderController): :return: the tool """ workflow: Workflow | None = ( - db.session.query(Workflow) + session.query(Workflow) .where(Workflow.app_id == db_provider.app_id, Workflow.version == db_provider.version) .first() ) @@ -101,8 +115,6 @@ class WorkflowToolProviderController(ToolProviderController): def fetch_workflow_variable(variable_name: str) -> VariableEntity | None: return next(filter(lambda x: x.variable == variable_name, variables), None) - user = db_provider.user - workflow_tool_parameters = [] for parameter in parameters: variable = fetch_workflow_variable(parameter.name) @@ -187,22 +199,25 @@ class WorkflowToolProviderController(ToolProviderController): if self.tools is not None: return self.tools - db_providers: WorkflowToolProvider | None = ( - db.session.query(WorkflowToolProvider) - .where( - WorkflowToolProvider.tenant_id == tenant_id, - WorkflowToolProvider.app_id == self.provider_id, + with Session(db.engine, expire_on_commit=False) as session, session.begin(): + db_provider: WorkflowToolProvider | None = ( + session.query(WorkflowToolProvider) + .where( + WorkflowToolProvider.tenant_id == tenant_id, + WorkflowToolProvider.app_id == self.provider_id, + ) + .first() ) - .first() - ) - if not db_providers: - return [] - if not db_providers.app: - raise ValueError("app not found") + if not db_provider: + return [] - app = db_providers.app - self.tools = [self._get_db_provider_tool(db_providers, app)] + app = session.get(App, db_provider.app_id) + if not app: + raise ValueError("app not found") + + user = session.get(Account, db_provider.user_id) if db_provider.user_id else None + self.tools = [self._get_db_provider_tool(db_provider, app, session=session, user=user)] return self.tools diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index 50c2327004..bc5b12c102 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -5,6 +5,7 @@ from typing import Any from flask import has_request_context from sqlalchemy import select +from sqlalchemy.orm import Session from core.file import FILE_MODEL_IDENTITY, File, FileTransferMethod from core.tools.__base.tool import Tool @@ -179,16 +180,17 @@ class WorkflowTool(Tool): """ get the workflow by app id and version """ - if not version: - workflow = ( - db.session.query(Workflow) - .where(Workflow.app_id == app_id, Workflow.version != Workflow.VERSION_DRAFT) - .order_by(Workflow.created_at.desc()) - .first() - ) - else: - stmt = select(Workflow).where(Workflow.app_id == app_id, Workflow.version == version) - workflow = db.session.scalar(stmt) + with Session(db.engine, expire_on_commit=False) as session, session.begin(): + if not version: + stmt = ( + select(Workflow) + .where(Workflow.app_id == app_id, Workflow.version != Workflow.VERSION_DRAFT) + .order_by(Workflow.created_at.desc()) + ) + workflow = session.scalars(stmt).first() + else: + stmt = select(Workflow).where(Workflow.app_id == app_id, Workflow.version == version) + workflow = session.scalar(stmt) if not workflow: raise ValueError("workflow not found or not published") @@ -200,7 +202,8 @@ class WorkflowTool(Tool): get the app by app id """ stmt = select(App).where(App.id == app_id) - app = db.session.scalar(stmt) + with Session(db.engine, expire_on_commit=False) as session, session.begin(): + app = session.scalar(stmt) if not app: raise ValueError("app not found") diff --git a/api/models/tools.py b/api/models/tools.py index aec53da50c..4e2976ce1b 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -222,7 +222,7 @@ class WorkflowToolProvider(TypeBase): sa.UniqueConstraint("tenant_id", "app_id", name="unique_workflow_tool_provider_app_id"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"), init=False) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # name of the workflow provider name: Mapped[str] = mapped_column(String(255), nullable=False) # label of the workflow provider diff --git a/api/services/tools/workflow_tools_manage_service.py b/api/services/tools/workflow_tools_manage_service.py index 2449536d5c..b1cc963681 100644 --- a/api/services/tools/workflow_tools_manage_service.py +++ b/api/services/tools/workflow_tools_manage_service.py @@ -4,6 +4,7 @@ from datetime import datetime from typing import Any from sqlalchemy import or_, select +from sqlalchemy.orm import Session from core.model_runtime.utils.encoders import jsonable_encoder from core.tools.__base.tool_provider import ToolProviderController @@ -13,6 +14,7 @@ from core.tools.utils.workflow_configuration_sync import WorkflowToolConfigurati from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db +from libs.uuid_utils import uuidv7 from models.model import App from models.tools import WorkflowToolProvider from models.workflow import Workflow @@ -63,27 +65,27 @@ class WorkflowToolManageService: if workflow is None: raise ValueError(f"Workflow not found for app {workflow_app_id}") - workflow_tool_provider = WorkflowToolProvider( - tenant_id=tenant_id, - user_id=user_id, - app_id=workflow_app_id, - name=name, - label=label, - icon=json.dumps(icon), - description=description, - parameter_configuration=json.dumps(parameters), - privacy_policy=privacy_policy, - version=workflow.version, - ) + with Session(db.engine, expire_on_commit=False) as session, session.begin(): + workflow_tool_provider = WorkflowToolProvider( + id=str(uuidv7()), + tenant_id=tenant_id, + user_id=user_id, + app_id=workflow_app_id, + name=name, + label=label, + icon=json.dumps(icon), + description=description, + parameter_configuration=json.dumps(parameters), + privacy_policy=privacy_policy, + version=workflow.version, + ) + session.add(workflow_tool_provider) try: WorkflowToolProviderController.from_db(workflow_tool_provider) except Exception as e: raise ValueError(str(e)) - db.session.add(workflow_tool_provider) - db.session.commit() - if labels is not None: ToolLabelManager.update_tool_labels( ToolTransformService.workflow_provider_to_controller(workflow_tool_provider), labels @@ -168,7 +170,6 @@ class WorkflowToolManageService: except Exception as e: raise ValueError(str(e)) - db.session.add(workflow_tool_provider) db.session.commit() if labels is not None: diff --git a/api/tests/test_containers_integration_tests/services/tools/test_tools_transform_service.py b/api/tests/test_containers_integration_tests/services/tools/test_tools_transform_service.py index ae0c7b7a6b..e2c616420f 100644 --- a/api/tests/test_containers_integration_tests/services/tools/test_tools_transform_service.py +++ b/api/tests/test_containers_integration_tests/services/tools/test_tools_transform_service.py @@ -6,6 +6,7 @@ from faker import Faker from core.tools.entities.api_entities import ToolProviderApiEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderType +from libs.uuid_utils import uuidv7 from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider from services.tools.tools_transform_service import ToolTransformService @@ -66,6 +67,7 @@ class TestToolTransformService: ) elif provider_type == "workflow": provider = WorkflowToolProvider( + id=str(uuidv7()), name=fake.company(), description=fake.text(max_nb_chars=100), icon='{"background": "#FF6B6B", "content": "🔧"}', @@ -758,6 +760,7 @@ class TestToolTransformService: # Create workflow tool provider provider = WorkflowToolProvider( + id=str(uuidv7()), name=fake.company(), description=fake.text(max_nb_chars=100), icon='{"background": "#FF6B6B", "content": "🔧"}', diff --git a/docker/.env.example b/docker/.env.example index b0e8d020ba..ca580dcb79 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -259,6 +259,18 @@ POSTGRES_MAINTENANCE_WORK_MEM=64MB # Reference: https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB +# Sets the maximum allowed duration of any statement before termination. +# Default is 60000 milliseconds. +# +# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT +POSTGRES_STATEMENT_TIMEOUT=60000 + +# Sets the maximum allowed duration of any idle in-transaction session before termination. +# Default is 60000 milliseconds. +# +# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT +POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT=60000 + # ------------------------------ # Redis Configuration # This Redis configuration is used for caching and for pub/sub during conversation. diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 5a67c080cc..8e4b8b8d7c 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -129,6 +129,8 @@ services: -c 'work_mem=${POSTGRES_WORK_MEM:-4MB}' -c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}' -c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}' + -c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-60000}' + -c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-60000}' volumes: - ./volumes/db/data:/var/lib/postgresql/data healthcheck: diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index ebc619a50f..be0e321df5 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -15,6 +15,8 @@ services: -c 'work_mem=${POSTGRES_WORK_MEM:-4MB}' -c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}' -c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}' + -c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-60000}' + -c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-60000}' volumes: - ${PGDATA_HOST_VOLUME:-./volumes/db/data}:/var/lib/postgresql/data ports: diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 421b733e2b..d384b8735f 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -68,6 +68,8 @@ x-shared-env: &shared-api-worker-env POSTGRES_WORK_MEM: ${POSTGRES_WORK_MEM:-4MB} POSTGRES_MAINTENANCE_WORK_MEM: ${POSTGRES_MAINTENANCE_WORK_MEM:-64MB} POSTGRES_EFFECTIVE_CACHE_SIZE: ${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB} + POSTGRES_STATEMENT_TIMEOUT: ${POSTGRES_STATEMENT_TIMEOUT:-60000} + POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT: ${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-60000} REDIS_HOST: ${REDIS_HOST:-redis} REDIS_PORT: ${REDIS_PORT:-6379} REDIS_USERNAME: ${REDIS_USERNAME:-} @@ -736,6 +738,8 @@ services: -c 'work_mem=${POSTGRES_WORK_MEM:-4MB}' -c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}' -c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}' + -c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-60000}' + -c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-60000}' volumes: - ./volumes/db/data:/var/lib/postgresql/data healthcheck: diff --git a/docker/middleware.env.example b/docker/middleware.env.example index 2eba62f594..c9bb8c0528 100644 --- a/docker/middleware.env.example +++ b/docker/middleware.env.example @@ -40,6 +40,18 @@ POSTGRES_MAINTENANCE_WORK_MEM=64MB # Reference: https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB +# Sets the maximum allowed duration of any statement before termination. +# Default is 60000 milliseconds. +# +# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT +POSTGRES_STATEMENT_TIMEOUT=60000 + +# Sets the maximum allowed duration of any idle in-transaction session before termination. +# Default is 60000 milliseconds. +# +# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT +POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT=60000 + # ----------------------------- # Environment Variables for redis Service # ----------------------------- From ea8245a91b9a677ecd4d250aaff9a1473e247a78 Mon Sep 17 00:00:00 2001 From: Guangdong Liu Date: Tue, 21 Oct 2025 15:25:01 +0800 Subject: [PATCH 02/33] fix: handle exceptions during loop break condition evaluation (#26961) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Novice --- api/core/workflow/nodes/loop/loop_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/loop/loop_node.py b/api/core/workflow/nodes/loop/loop_node.py index b51790c0a2..c35195e931 100644 --- a/api/core/workflow/nodes/loop/loop_node.py +++ b/api/core/workflow/nodes/loop/loop_node.py @@ -108,7 +108,7 @@ class LoopNode(Node): raise ValueError(f"Invalid value for loop variable {loop_variable.label}") variable_selector = [self._node_id, loop_variable.label] variable = segment_to_variable(segment=processed_segment, selector=variable_selector) - self.graph_runtime_state.variable_pool.add(variable_selector, variable) + self.graph_runtime_state.variable_pool.add(variable_selector, variable.value) loop_variable_selectors[loop_variable.label] = variable_selector inputs[loop_variable.label] = processed_segment.value From 05f66fcf0d89343f8b23001b253482b729ec5323 Mon Sep 17 00:00:00 2001 From: Jyong <76649700+JohnJyong@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:30:58 +0800 Subject: [PATCH 03/33] remove built-in pipeline template user field (#27184) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- ...662b25d9bc_remove_builtin_template_user.py | 36 +++++++++++++++++++ api/models/dataset.py | 9 ----- .../database/database_retrieval.py | 1 - 3 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 api/migrations/versions/2025_10_21_1430-ae662b25d9bc_remove_builtin_template_user.py diff --git a/api/migrations/versions/2025_10_21_1430-ae662b25d9bc_remove_builtin_template_user.py b/api/migrations/versions/2025_10_21_1430-ae662b25d9bc_remove_builtin_template_user.py new file mode 100644 index 0000000000..086a02e7c3 --- /dev/null +++ b/api/migrations/versions/2025_10_21_1430-ae662b25d9bc_remove_builtin_template_user.py @@ -0,0 +1,36 @@ +"""remove-builtin-template-user + +Revision ID: ae662b25d9bc +Revises: d98acf217d43 +Create Date: 2025-10-21 14:30:28.566192 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ae662b25d9bc' +down_revision = 'd98acf217d43' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + with op.batch_alter_table('pipeline_built_in_templates', schema=None) as batch_op: + batch_op.drop_column('updated_by') + batch_op.drop_column('created_by') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('pipeline_built_in_templates', schema=None) as batch_op: + batch_op.add_column(sa.Column('created_by', sa.UUID(), autoincrement=False, nullable=False)) + batch_op.add_column(sa.Column('updated_by', sa.UUID(), autoincrement=False, nullable=True)) + + # ### end Alembic commands ### diff --git a/api/models/dataset.py b/api/models/dataset.py index 5653445f2b..4a9e2688b8 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -1239,15 +1239,6 @@ class PipelineBuiltInTemplate(Base): # type: ignore[name-defined] language = mapped_column(db.String(255), nullable=False) created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) - created_by = mapped_column(StringUUID, nullable=False) - updated_by = mapped_column(StringUUID, nullable=True) - - @property - def created_user_name(self): - account = db.session.query(Account).where(Account.id == self.created_by).first() - if account: - return account.name - return "" class PipelineCustomizedTemplate(Base): # type: ignore[name-defined] diff --git a/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py b/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py index ec91f79606..908f9a2684 100644 --- a/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py +++ b/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py @@ -74,5 +74,4 @@ class DatabasePipelineTemplateRetrieval(PipelineTemplateRetrievalBase): "chunk_structure": pipeline_template.chunk_structure, "export_data": pipeline_template.yaml_content, "graph": graph_data, - "created_by": pipeline_template.created_user_name, } From 9a9d6a4a2b3122957f85b48eaad4256db2b07e25 Mon Sep 17 00:00:00 2001 From: Nite Knite Date: Tue, 21 Oct 2025 15:48:02 +0800 Subject: [PATCH 04/33] chore: update support channels (#27188) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web/app/components/base/zendesk/index.tsx | 20 ++++++++--- web/app/components/base/zendesk/utils.ts | 10 ++++++ .../header/account-dropdown/index.tsx | 4 +-- .../header/account-dropdown/support.tsx | 36 ++++++++++--------- web/i18n/de-DE/common.ts | 1 + web/i18n/en-US/common.ts | 1 + web/i18n/es-ES/common.ts | 1 + web/i18n/fa-IR/common.ts | 1 + web/i18n/fr-FR/common.ts | 1 + web/i18n/hi-IN/common.ts | 1 + web/i18n/id-ID/common.ts | 1 + web/i18n/it-IT/common.ts | 1 + web/i18n/ja-JP/common.ts | 1 + web/i18n/ko-KR/common.ts | 1 + web/i18n/pl-PL/common.ts | 1 + web/i18n/pt-BR/common.ts | 1 + web/i18n/ro-RO/common.ts | 1 + web/i18n/ru-RU/common.ts | 1 + web/i18n/sl-SI/common.ts | 1 + web/i18n/th-TH/common.ts | 1 + web/i18n/tr-TR/common.ts | 1 + web/i18n/uk-UA/common.ts | 1 + web/i18n/vi-VN/common.ts | 1 + web/i18n/zh-Hans/common.ts | 1 + web/i18n/zh-Hant/common.ts | 1 + web/service/base.ts | 6 ++-- 26 files changed, 70 insertions(+), 27 deletions(-) diff --git a/web/app/components/base/zendesk/index.tsx b/web/app/components/base/zendesk/index.tsx index b3d67eb390..a6971fe1db 100644 --- a/web/app/components/base/zendesk/index.tsx +++ b/web/app/components/base/zendesk/index.tsx @@ -10,11 +10,21 @@ const Zendesk = () => { const nonce = process.env.NODE_ENV === 'production' ? (headers() as unknown as UnsafeUnwrappedHeaders).get('x-nonce') ?? '' : '' return ( - + ) } diff --git a/web/app/components/base/zendesk/utils.ts b/web/app/components/base/zendesk/utils.ts index 6f853343aa..216499efc4 100644 --- a/web/app/components/base/zendesk/utils.ts +++ b/web/app/components/base/zendesk/utils.ts @@ -21,3 +21,13 @@ export const setZendeskConversationFields = (fields: ConversationField[], callba if (!IS_CE_EDITION && window.zE) window.zE('messenger:set', 'conversationFields', fields, callback) } + +export const setZendeskWidgetVisibility = (visible: boolean) => { + if (!IS_CE_EDITION && window.zE) + window.zE('messenger', visible ? 'show' : 'hide') +} + +export const toggleZendeskWindow = (open: boolean) => { + if (!IS_CE_EDITION && window.zE) + window.zE('messenger', open ? 'open' : 'close') +} diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 178f347102..30b2bfdf6f 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -68,7 +68,7 @@ export default function AppSelector() {
{ - ({ open }) => ( + ({ open, close }) => ( <> @@ -142,7 +142,7 @@ export default function AppSelector() { - + {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && }
diff --git a/web/app/components/header/account-dropdown/support.tsx b/web/app/components/header/account-dropdown/support.tsx index 15c6d3e5cf..fda45f2db5 100644 --- a/web/app/components/header/account-dropdown/support.tsx +++ b/web/app/components/header/account-dropdown/support.tsx @@ -1,23 +1,25 @@ import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' -import { RiArrowRightSLine, RiArrowRightUpLine, RiDiscordLine, RiFeedbackLine, RiMailSendLine, RiQuestionLine } from '@remixicon/react' +import { RiArrowRightSLine, RiArrowRightUpLine, RiChatSmile2Line, RiDiscordLine, RiFeedbackLine, RiQuestionLine } from '@remixicon/react' import { Fragment } from 'react' import Link from 'next/link' import { useTranslation } from 'react-i18next' -import { mailToSupport } from '../utils/util' import cn from '@/utils/classnames' import { useProviderContext } from '@/context/provider-context' import { Plan } from '@/app/components/billing/type' -import { useAppContext } from '@/context/app-context' +import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils' -export default function Support() { +type SupportProps = { + closeAccountDropdown: () => void +} + +export default function Support({ closeAccountDropdown }: SupportProps) { const itemClassName = ` flex items-center w-full h-9 pl-3 pr-2 text-text-secondary system-md-regular rounded-lg hover:bg-state-base-hover cursor-pointer gap-1 ` const { t } = useTranslation() const { plan } = useProviderContext() - const { userProfile, langGeniusVersionInfo } = useAppContext() - const canEmailSupport = plan.type === Plan.professional || plan.type === Plan.team || plan.type === Plan.enterprise + const hasDedicatedChannel = plan.type !== Plan.sandbox return { @@ -48,17 +50,17 @@ export default function Support() { )} >
- {canEmailSupport && - - -
{t('common.userProfile.emailSupport')}
- -
+ {hasDedicatedChannel && + } Date: Tue, 21 Oct 2025 15:53:12 +0800 Subject: [PATCH 05/33] Feature:during account initialization, set the interface language to be consistent with the display language(#27029) (#27042) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> --- api/constants/languages.py | 6 ++++++ api/controllers/console/auth/login.py | 8 ++++++-- api/controllers/console/setup.py | 7 ++++++- api/services/account_service.py | 9 ++++----- api/tests/integration_tests/conftest.py | 1 + .../services/test_account_service.py | 2 ++ api/tests/unit_tests/services/test_account_service.py | 3 ++- web/app/install/installForm.tsx | 3 ++- web/app/signin/check-code/page.tsx | 5 +++-- web/service/common.ts | 2 +- 10 files changed, 33 insertions(+), 13 deletions(-) diff --git a/api/constants/languages.py b/api/constants/languages.py index a509ddcf5d..0312a558c9 100644 --- a/api/constants/languages.py +++ b/api/constants/languages.py @@ -31,3 +31,9 @@ def supported_language(lang): error = f"{lang} is not a valid language." raise ValueError(error) + + +def get_valid_language(lang: str | None) -> str: + if lang and lang in languages: + return lang + return languages[0] diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index f371613bee..c0a565b5da 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -4,7 +4,7 @@ from flask_restx import Resource, reqparse import services from configs import dify_config -from constants.languages import languages +from constants.languages import get_valid_language from controllers.console import console_ns from controllers.console.auth.error import ( AuthenticationFailedError, @@ -204,10 +204,12 @@ class EmailCodeLoginApi(Resource): .add_argument("email", type=str, required=True, location="json") .add_argument("code", type=str, required=True, location="json") .add_argument("token", type=str, required=True, location="json") + .add_argument("language", type=str, required=False, location="json") ) args = parser.parse_args() user_email = args["email"] + language = args["language"] token_data = AccountService.get_email_code_login_data(args["token"]) if token_data is None: @@ -241,7 +243,9 @@ class EmailCodeLoginApi(Resource): if account is None: try: account = AccountService.create_account_and_tenant( - email=user_email, name=user_email, interface_language=languages[0] + email=user_email, + name=user_email, + interface_language=get_valid_language(language), ) except WorkSpaceNotAllowedCreateError: raise NotAllowedCreateWorkspace() diff --git a/api/controllers/console/setup.py b/api/controllers/console/setup.py index 6d2b22bde3..1200349e2d 100644 --- a/api/controllers/console/setup.py +++ b/api/controllers/console/setup.py @@ -74,12 +74,17 @@ class SetupApi(Resource): .add_argument("email", type=email, required=True, location="json") .add_argument("name", type=StrLen(30), required=True, location="json") .add_argument("password", type=valid_password, required=True, location="json") + .add_argument("language", type=str, required=False, location="json") ) args = parser.parse_args() # setup RegisterService.setup( - email=args["email"], name=args["name"], password=args["password"], ip_address=extract_remote_ip(request) + email=args["email"], + name=args["name"], + password=args["password"], + ip_address=extract_remote_ip(request), + language=args["language"], ) return {"result": "success"}, 201 diff --git a/api/services/account_service.py b/api/services/account_service.py index cb0eb7a9dd..13c3993fb5 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import Session from werkzeug.exceptions import Unauthorized from configs import dify_config -from constants.languages import language_timezone_mapping, languages +from constants.languages import get_valid_language, language_timezone_mapping from events.tenant_event import tenant_was_created from extensions.ext_database import db from extensions.ext_redis import redis_client, redis_fallback @@ -1259,7 +1259,7 @@ class RegisterService: return f"member_invite:token:{token}" @classmethod - def setup(cls, email: str, name: str, password: str, ip_address: str): + def setup(cls, email: str, name: str, password: str, ip_address: str, language: str): """ Setup dify @@ -1269,11 +1269,10 @@ class RegisterService: :param ip_address: ip address """ try: - # Register account = AccountService.create_account( email=email, name=name, - interface_language=languages[0], + interface_language=get_valid_language(language), password=password, is_setup=True, ) @@ -1315,7 +1314,7 @@ class RegisterService: account = AccountService.create_account( email=email, name=name, - interface_language=language or languages[0], + interface_language=get_valid_language(language), password=password, is_setup=is_setup, ) diff --git a/api/tests/integration_tests/conftest.py b/api/tests/integration_tests/conftest.py index 9dc7b76e04..4395a9815a 100644 --- a/api/tests/integration_tests/conftest.py +++ b/api/tests/integration_tests/conftest.py @@ -58,6 +58,7 @@ def setup_account(request) -> Generator[Account, None, None]: name=name, password=secrets.token_hex(16), ip_address="localhost", + language="en-US", ) with _CACHED_APP.test_request_context(): diff --git a/api/tests/test_containers_integration_tests/services/test_account_service.py b/api/tests/test_containers_integration_tests/services/test_account_service.py index c59fc50f08..4d4e77a802 100644 --- a/api/tests/test_containers_integration_tests/services/test_account_service.py +++ b/api/tests/test_containers_integration_tests/services/test_account_service.py @@ -2299,6 +2299,7 @@ class TestRegisterService: name=admin_name, password=admin_password, ip_address=ip_address, + language="en-US", ) # Verify account was created @@ -2348,6 +2349,7 @@ class TestRegisterService: name=admin_name, password=admin_password, ip_address=ip_address, + language="en-US", ) # Verify no entities were created (rollback worked) diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index 737202f8de..627a04bcd0 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -893,7 +893,7 @@ class TestRegisterService: mock_dify_setup.return_value = mock_dify_setup_instance # Execute test - RegisterService.setup("admin@example.com", "Admin User", "password123", "192.168.1.1") + RegisterService.setup("admin@example.com", "Admin User", "password123", "192.168.1.1", "en-US") # Verify results mock_create_account.assert_called_once_with( @@ -925,6 +925,7 @@ class TestRegisterService: "Admin User", "password123", "192.168.1.1", + "en-US", ) # Verify rollback operations were called diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index 0a534b72fe..01bfd59b6d 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -35,7 +35,7 @@ type AccountFormValues = z.infer const InstallForm = () => { useDocumentTitle('') - const { t } = useTranslation() + const { t, i18n } = useTranslation() const docLink = useDocLink() const router = useRouter() const [showPassword, setShowPassword] = React.useState(false) @@ -58,6 +58,7 @@ const InstallForm = () => { await setup({ body: { ...data, + language: i18n.language, }, }) diff --git a/web/app/signin/check-code/page.tsx b/web/app/signin/check-code/page.tsx index da6bd426af..adbde377a1 100644 --- a/web/app/signin/check-code/page.tsx +++ b/web/app/signin/check-code/page.tsx @@ -13,12 +13,13 @@ import I18NContext from '@/context/i18n' import { resolvePostLoginRedirect } from '../utils/post-login-redirect' export default function CheckCode() { - const { t } = useTranslation() + const { t, i18n } = useTranslation() const router = useRouter() const searchParams = useSearchParams() const email = decodeURIComponent(searchParams.get('email') as string) const token = decodeURIComponent(searchParams.get('token') as string) const invite_token = decodeURIComponent(searchParams.get('invite_token') || '') + const language = i18n.language const [code, setVerifyCode] = useState('') const [loading, setIsLoading] = useState(false) const { locale } = useContext(I18NContext) @@ -40,7 +41,7 @@ export default function CheckCode() { return } setIsLoading(true) - const ret = await emailLoginWithCode({ email, code, token }) + const ret = await emailLoginWithCode({ email, code, token, language }) if (ret.result === 'success') { if (invite_token) { router.replace(`/signin/invite-settings?${searchParams.toString()}`) diff --git a/web/service/common.ts b/web/service/common.ts index 8f2adc329e..55dec33cb5 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -345,7 +345,7 @@ export const uploadRemoteFileInfo = (url: string, isPublic?: boolean, silent?: b export const sendEMailLoginCode = (email: string, language = 'en-US') => post('/email-code-login', { body: { email, language } }) -export const emailLoginWithCode = (data: { email: string; code: string; token: string }) => +export const emailLoginWithCode = (data: { email: string; code: string; token: string; language: string }) => post('/email-code-login/validity', { body: data }) export const sendResetPasswordCode = (email: string, language = 'en-US') => From 4a6398fc1fb18d9ffc37f7634e1028a210562a7b Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 21 Oct 2025 16:05:26 +0800 Subject: [PATCH 06/33] Fix: surface workflow container LLM usage (#27021) --- api/core/rag/retrieval/dataset_retrieval.py | 23 ++++++- .../multi_dataset_function_call_router.py | 15 ++-- .../router/multi_dataset_react_route.py | 16 ++--- api/core/tools/workflow_as_tool/tool.py | 68 ++++++++++++++++++- api/core/workflow/nodes/base/__init__.py | 2 + .../nodes/base/usage_tracking_mixin.py | 28 ++++++++ .../nodes/iteration/iteration_node.py | 64 +++++++++++++++-- .../knowledge_retrieval_node.py | 66 ++++++++++++------ api/core/workflow/nodes/loop/loop_node.py | 28 ++++++-- api/core/workflow/nodes/tool/tool_node.py | 32 +++++++-- 10 files changed, 283 insertions(+), 59 deletions(-) create mode 100644 api/core/workflow/nodes/base/usage_tracking_mixin.py diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py index 99bbe615fb..45b19f25a0 100644 --- a/api/core/rag/retrieval/dataset_retrieval.py +++ b/api/core/rag/retrieval/dataset_retrieval.py @@ -72,6 +72,19 @@ default_retrieval_model: dict[str, Any] = { class DatasetRetrieval: def __init__(self, application_generate_entity=None): self.application_generate_entity = application_generate_entity + self._llm_usage = LLMUsage.empty_usage() + + @property + def llm_usage(self) -> LLMUsage: + return self._llm_usage.model_copy() + + def _record_usage(self, usage: LLMUsage | None) -> None: + if usage is None or usage.total_tokens <= 0: + return + if self._llm_usage.total_tokens == 0: + self._llm_usage = usage + else: + self._llm_usage = self._llm_usage.plus(usage) def retrieve( self, @@ -312,15 +325,18 @@ class DatasetRetrieval: ) tools.append(message_tool) dataset_id = None + router_usage = LLMUsage.empty_usage() if planning_strategy == PlanningStrategy.REACT_ROUTER: react_multi_dataset_router = ReactMultiDatasetRouter() - dataset_id = react_multi_dataset_router.invoke( + dataset_id, router_usage = react_multi_dataset_router.invoke( query, tools, model_config, model_instance, user_id, tenant_id ) elif planning_strategy == PlanningStrategy.ROUTER: function_call_router = FunctionCallMultiDatasetRouter() - dataset_id = function_call_router.invoke(query, tools, model_config, model_instance) + dataset_id, router_usage = function_call_router.invoke(query, tools, model_config, model_instance) + + self._record_usage(router_usage) if dataset_id: # get retrieval model config @@ -983,7 +999,8 @@ class DatasetRetrieval: ) # handle invoke result - result_text, _ = self._handle_invoke_result(invoke_result=invoke_result) + result_text, usage = self._handle_invoke_result(invoke_result=invoke_result) + self._record_usage(usage) result_text_json = parse_and_check_json_markdown(result_text, []) automatic_metadata_filters = [] diff --git a/api/core/rag/retrieval/router/multi_dataset_function_call_router.py b/api/core/rag/retrieval/router/multi_dataset_function_call_router.py index de59c6380e..5f3e1a8cae 100644 --- a/api/core/rag/retrieval/router/multi_dataset_function_call_router.py +++ b/api/core/rag/retrieval/router/multi_dataset_function_call_router.py @@ -2,7 +2,7 @@ from typing import Union from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.model_manager import ModelInstance -from core.model_runtime.entities.llm_entities import LLMResult +from core.model_runtime.entities.llm_entities import LLMResult, LLMUsage from core.model_runtime.entities.message_entities import PromptMessageTool, SystemPromptMessage, UserPromptMessage @@ -13,15 +13,15 @@ class FunctionCallMultiDatasetRouter: dataset_tools: list[PromptMessageTool], model_config: ModelConfigWithCredentialsEntity, model_instance: ModelInstance, - ) -> Union[str, None]: + ) -> tuple[Union[str, None], LLMUsage]: """Given input, decided what to do. Returns: Action specifying what tool to use. """ if len(dataset_tools) == 0: - return None + return None, LLMUsage.empty_usage() elif len(dataset_tools) == 1: - return dataset_tools[0].name + return dataset_tools[0].name, LLMUsage.empty_usage() try: prompt_messages = [ @@ -34,9 +34,10 @@ class FunctionCallMultiDatasetRouter: stream=False, model_parameters={"temperature": 0.2, "top_p": 0.3, "max_tokens": 1500}, ) + usage = result.usage or LLMUsage.empty_usage() if result.message.tool_calls: # get retrieval model config - return result.message.tool_calls[0].function.name - return None + return result.message.tool_calls[0].function.name, usage + return None, usage except Exception: - return None + return None, LLMUsage.empty_usage() diff --git a/api/core/rag/retrieval/router/multi_dataset_react_route.py b/api/core/rag/retrieval/router/multi_dataset_react_route.py index 59d36229b3..8f3bec2704 100644 --- a/api/core/rag/retrieval/router/multi_dataset_react_route.py +++ b/api/core/rag/retrieval/router/multi_dataset_react_route.py @@ -58,15 +58,15 @@ class ReactMultiDatasetRouter: model_instance: ModelInstance, user_id: str, tenant_id: str, - ) -> Union[str, None]: + ) -> tuple[Union[str, None], LLMUsage]: """Given input, decided what to do. Returns: Action specifying what tool to use. """ if len(dataset_tools) == 0: - return None + return None, LLMUsage.empty_usage() elif len(dataset_tools) == 1: - return dataset_tools[0].name + return dataset_tools[0].name, LLMUsage.empty_usage() try: return self._react_invoke( @@ -78,7 +78,7 @@ class ReactMultiDatasetRouter: tenant_id=tenant_id, ) except Exception: - return None + return None, LLMUsage.empty_usage() def _react_invoke( self, @@ -91,7 +91,7 @@ class ReactMultiDatasetRouter: prefix: str = PREFIX, suffix: str = SUFFIX, format_instructions: str = FORMAT_INSTRUCTIONS, - ) -> Union[str, None]: + ) -> tuple[Union[str, None], LLMUsage]: prompt: Union[list[ChatModelMessage], CompletionModelPromptTemplate] if model_config.mode == "chat": prompt = self.create_chat_prompt( @@ -120,7 +120,7 @@ class ReactMultiDatasetRouter: memory=None, model_config=model_config, ) - result_text, _ = self._invoke_llm( + result_text, usage = self._invoke_llm( completion_param=model_config.parameters, model_instance=model_instance, prompt_messages=prompt_messages, @@ -131,8 +131,8 @@ class ReactMultiDatasetRouter: output_parser = StructuredChatOutputParser() react_decision = output_parser.parse(result_text) if isinstance(react_decision, ReactAction): - return react_decision.tool - return None + return react_decision.tool, usage + return None, usage def _invoke_llm( self, diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index bc5b12c102..2cd46647a0 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -1,13 +1,14 @@ import json import logging -from collections.abc import Generator -from typing import Any +from collections.abc import Generator, Mapping, Sequence +from typing import Any, cast from flask import has_request_context from sqlalchemy import select from sqlalchemy.orm import Session from core.file import FILE_MODEL_IDENTITY, File, FileTransferMethod +from core.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_entities import ( @@ -49,6 +50,7 @@ class WorkflowTool(Tool): self.workflow_entities = workflow_entities self.workflow_call_depth = workflow_call_depth self.label = label + self._latest_usage = LLMUsage.empty_usage() super().__init__(entity=entity, runtime=runtime) @@ -84,10 +86,11 @@ class WorkflowTool(Tool): assert self.runtime.invoke_from is not None user = self._resolve_user(user_id=user_id) - if user is None: raise ToolInvokeError("User not found") + self._latest_usage = LLMUsage.empty_usage() + result = generator.generate( app_model=app, workflow=workflow, @@ -111,9 +114,68 @@ class WorkflowTool(Tool): for file in files: yield self.create_file_message(file) # type: ignore + self._latest_usage = self._derive_usage_from_result(data) + yield self.create_text_message(json.dumps(outputs, ensure_ascii=False)) yield self.create_json_message(outputs) + @property + def latest_usage(self) -> LLMUsage: + return self._latest_usage + + @classmethod + def _derive_usage_from_result(cls, data: Mapping[str, Any]) -> LLMUsage: + usage_dict = cls._extract_usage_dict(data) + if usage_dict is not None: + return LLMUsage.from_metadata(cast(LLMUsageMetadata, dict(usage_dict))) + + total_tokens = data.get("total_tokens") + total_price = data.get("total_price") + if total_tokens is None and total_price is None: + return LLMUsage.empty_usage() + + usage_metadata: dict[str, Any] = {} + if total_tokens is not None: + try: + usage_metadata["total_tokens"] = int(str(total_tokens)) + except (TypeError, ValueError): + pass + if total_price is not None: + usage_metadata["total_price"] = str(total_price) + currency = data.get("currency") + if currency is not None: + usage_metadata["currency"] = currency + + if not usage_metadata: + return LLMUsage.empty_usage() + + return LLMUsage.from_metadata(cast(LLMUsageMetadata, usage_metadata)) + + @classmethod + def _extract_usage_dict(cls, payload: Mapping[str, Any]) -> Mapping[str, Any] | None: + usage_candidate = payload.get("usage") + if isinstance(usage_candidate, Mapping): + return usage_candidate + + metadata_candidate = payload.get("metadata") + if isinstance(metadata_candidate, Mapping): + usage_candidate = metadata_candidate.get("usage") + if isinstance(usage_candidate, Mapping): + return usage_candidate + + for value in payload.values(): + if isinstance(value, Mapping): + found = cls._extract_usage_dict(value) + if found is not None: + return found + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for item in value: + if isinstance(item, Mapping): + found = cls._extract_usage_dict(item) + if found is not None: + return found + return None + def fork_tool_runtime(self, runtime: ToolRuntime) -> "WorkflowTool": """ fork a new tool with metadata diff --git a/api/core/workflow/nodes/base/__init__.py b/api/core/workflow/nodes/base/__init__.py index 8cf31dc342..f83df0e323 100644 --- a/api/core/workflow/nodes/base/__init__.py +++ b/api/core/workflow/nodes/base/__init__.py @@ -1,4 +1,5 @@ from .entities import BaseIterationNodeData, BaseIterationState, BaseLoopNodeData, BaseLoopState, BaseNodeData +from .usage_tracking_mixin import LLMUsageTrackingMixin __all__ = [ "BaseIterationNodeData", @@ -6,4 +7,5 @@ __all__ = [ "BaseLoopNodeData", "BaseLoopState", "BaseNodeData", + "LLMUsageTrackingMixin", ] diff --git a/api/core/workflow/nodes/base/usage_tracking_mixin.py b/api/core/workflow/nodes/base/usage_tracking_mixin.py new file mode 100644 index 0000000000..d9a0ef8972 --- /dev/null +++ b/api/core/workflow/nodes/base/usage_tracking_mixin.py @@ -0,0 +1,28 @@ +from core.model_runtime.entities.llm_entities import LLMUsage +from core.workflow.runtime import GraphRuntimeState + + +class LLMUsageTrackingMixin: + """Provides shared helpers for merging and recording LLM usage within workflow nodes.""" + + graph_runtime_state: GraphRuntimeState + + @staticmethod + def _merge_usage(current: LLMUsage, new_usage: LLMUsage | None) -> LLMUsage: + """Return a combined usage snapshot, preserving zero-value inputs.""" + if new_usage is None or new_usage.total_tokens <= 0: + return current + if current.total_tokens == 0: + return new_usage + return current.plus(new_usage) + + def _accumulate_usage(self, usage: LLMUsage) -> None: + """Push usage into the graph runtime accumulator for downstream reporting.""" + if usage.total_tokens <= 0: + return + + current_usage = self.graph_runtime_state.llm_usage + if current_usage.total_tokens == 0: + self.graph_runtime_state.llm_usage = usage.model_copy() + else: + self.graph_runtime_state.llm_usage = current_usage.plus(usage) diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index 41060bd569..3a3a2290be 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, NewType, cast from flask import Flask, current_app from typing_extensions import TypeIs +from core.model_runtime.entities.llm_entities import LLMUsage from core.variables import IntegerVariable, NoneSegment from core.variables.segments import ArrayAnySegment, ArraySegment from core.variables.variables import VariableUnion @@ -34,6 +35,7 @@ from core.workflow.node_events import ( NodeRunResult, StreamCompletedEvent, ) +from core.workflow.nodes.base import LLMUsageTrackingMixin from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig from core.workflow.nodes.base.node import Node from core.workflow.nodes.iteration.entities import ErrorHandleMode, IterationNodeData @@ -58,7 +60,7 @@ logger = logging.getLogger(__name__) EmptyArraySegment = NewType("EmptyArraySegment", ArraySegment) -class IterationNode(Node): +class IterationNode(LLMUsageTrackingMixin, Node): """ Iteration Node. """ @@ -118,6 +120,7 @@ class IterationNode(Node): started_at = naive_utc_now() iter_run_map: dict[str, float] = {} outputs: list[object] = [] + usage_accumulator = [LLMUsage.empty_usage()] yield IterationStartedEvent( start_at=started_at, @@ -130,22 +133,27 @@ class IterationNode(Node): iterator_list_value=iterator_list_value, outputs=outputs, iter_run_map=iter_run_map, + usage_accumulator=usage_accumulator, ) + self._accumulate_usage(usage_accumulator[0]) yield from self._handle_iteration_success( started_at=started_at, inputs=inputs, outputs=outputs, iterator_list_value=iterator_list_value, iter_run_map=iter_run_map, + usage=usage_accumulator[0], ) except IterationNodeError as e: + self._accumulate_usage(usage_accumulator[0]) yield from self._handle_iteration_failure( started_at=started_at, inputs=inputs, outputs=outputs, iterator_list_value=iterator_list_value, iter_run_map=iter_run_map, + usage=usage_accumulator[0], error=e, ) @@ -196,6 +204,7 @@ class IterationNode(Node): iterator_list_value: Sequence[object], outputs: list[object], iter_run_map: dict[str, float], + usage_accumulator: list[LLMUsage], ) -> Generator[GraphNodeEventBase | NodeEventBase, None, None]: if self._node_data.is_parallel: # Parallel mode execution @@ -203,6 +212,7 @@ class IterationNode(Node): iterator_list_value=iterator_list_value, outputs=outputs, iter_run_map=iter_run_map, + usage_accumulator=usage_accumulator, ) else: # Sequential mode execution @@ -228,6 +238,9 @@ class IterationNode(Node): # Update the total tokens from this iteration self.graph_runtime_state.total_tokens += graph_engine.graph_runtime_state.total_tokens + usage_accumulator[0] = self._merge_usage( + usage_accumulator[0], graph_engine.graph_runtime_state.llm_usage + ) iter_run_map[str(index)] = (datetime.now(UTC).replace(tzinfo=None) - iter_start_at).total_seconds() def _execute_parallel_iterations( @@ -235,6 +248,7 @@ class IterationNode(Node): iterator_list_value: Sequence[object], outputs: list[object], iter_run_map: dict[str, float], + usage_accumulator: list[LLMUsage], ) -> Generator[GraphNodeEventBase | NodeEventBase, None, None]: # Initialize outputs list with None values to maintain order outputs.extend([None] * len(iterator_list_value)) @@ -245,7 +259,16 @@ class IterationNode(Node): with ThreadPoolExecutor(max_workers=max_workers) as executor: # Submit all iteration tasks future_to_index: dict[ - Future[tuple[datetime, list[GraphNodeEventBase], object | None, int, dict[str, VariableUnion]]], + Future[ + tuple[ + datetime, + list[GraphNodeEventBase], + object | None, + int, + dict[str, VariableUnion], + LLMUsage, + ] + ], int, ] = {} for index, item in enumerate(iterator_list_value): @@ -264,7 +287,14 @@ class IterationNode(Node): index = future_to_index[future] try: result = future.result() - iter_start_at, events, output_value, tokens_used, conversation_snapshot = result + ( + iter_start_at, + events, + output_value, + tokens_used, + conversation_snapshot, + iteration_usage, + ) = result # Update outputs at the correct index outputs[index] = output_value @@ -276,6 +306,8 @@ class IterationNode(Node): self.graph_runtime_state.total_tokens += tokens_used iter_run_map[str(index)] = (datetime.now(UTC).replace(tzinfo=None) - iter_start_at).total_seconds() + usage_accumulator[0] = self._merge_usage(usage_accumulator[0], iteration_usage) + # Sync conversation variables after iteration completion self._sync_conversation_variables_from_snapshot(conversation_snapshot) @@ -303,7 +335,7 @@ class IterationNode(Node): item: object, flask_app: Flask, context_vars: contextvars.Context, - ) -> tuple[datetime, list[GraphNodeEventBase], object | None, int, dict[str, VariableUnion]]: + ) -> tuple[datetime, list[GraphNodeEventBase], object | None, int, dict[str, VariableUnion], LLMUsage]: """Execute a single iteration in parallel mode and return results.""" with preserve_flask_contexts(flask_app=flask_app, context_vars=context_vars): iter_start_at = datetime.now(UTC).replace(tzinfo=None) @@ -332,6 +364,7 @@ class IterationNode(Node): output_value, graph_engine.graph_runtime_state.total_tokens, conversation_snapshot, + graph_engine.graph_runtime_state.llm_usage, ) def _handle_iteration_success( @@ -341,6 +374,8 @@ class IterationNode(Node): outputs: list[object], iterator_list_value: Sequence[object], iter_run_map: dict[str, float], + *, + usage: LLMUsage, ) -> Generator[NodeEventBase, None, None]: # Flatten the list of lists if all outputs are lists flattened_outputs = self._flatten_outputs_if_needed(outputs) @@ -351,7 +386,9 @@ class IterationNode(Node): outputs={"output": flattened_outputs}, steps=len(iterator_list_value), metadata={ - WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: usage.currency, WorkflowNodeExecutionMetadataKey.ITERATION_DURATION_MAP: iter_run_map, }, ) @@ -362,8 +399,11 @@ class IterationNode(Node): status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs={"output": flattened_outputs}, metadata={ - WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: usage.currency, }, + llm_usage=usage, ) ) @@ -400,6 +440,8 @@ class IterationNode(Node): outputs: list[object], iterator_list_value: Sequence[object], iter_run_map: dict[str, float], + *, + usage: LLMUsage, error: IterationNodeError, ) -> Generator[NodeEventBase, None, None]: # Flatten the list of lists if all outputs are lists (even in failure case) @@ -411,7 +453,9 @@ class IterationNode(Node): outputs={"output": flattened_outputs}, steps=len(iterator_list_value), metadata={ - WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: usage.currency, WorkflowNodeExecutionMetadataKey.ITERATION_DURATION_MAP: iter_run_map, }, error=str(error), @@ -420,6 +464,12 @@ class IterationNode(Node): node_run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, error=str(error), + metadata={ + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: usage.currency, + }, + llm_usage=usage, ) ) diff --git a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py index ba5134f9e6..4a63900527 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py +++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py @@ -15,14 +15,11 @@ from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEnti from core.entities.agent_entities import PlanningStrategy from core.entities.model_entities import ModelStatus from core.model_manager import ModelInstance, ModelManager -from core.model_runtime.entities.message_entities import ( - PromptMessageRole, -) -from core.model_runtime.entities.model_entities import ( - ModelFeature, - ModelType, -) +from core.model_runtime.entities.llm_entities import LLMUsage +from core.model_runtime.entities.message_entities import PromptMessageRole +from core.model_runtime.entities.model_entities import ModelFeature, ModelType from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel +from core.model_runtime.utils.encoders import jsonable_encoder from core.prompt.simple_prompt_transform import ModelMode from core.rag.datasource.retrieval_service import RetrievalService from core.rag.entities.metadata_entities import Condition, MetadataCondition @@ -33,8 +30,14 @@ from core.variables import ( ) from core.variables.segments import ArrayObjectSegment from core.workflow.entities import GraphInitParams -from core.workflow.enums import ErrorStrategy, NodeType, WorkflowNodeExecutionStatus +from core.workflow.enums import ( + ErrorStrategy, + NodeType, + WorkflowNodeExecutionMetadataKey, + WorkflowNodeExecutionStatus, +) from core.workflow.node_events import ModelInvokeCompletedEvent, NodeRunResult +from core.workflow.nodes.base import LLMUsageTrackingMixin from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig from core.workflow.nodes.base.node import Node from core.workflow.nodes.knowledge_retrieval.template_prompts import ( @@ -80,7 +83,7 @@ default_retrieval_model = { } -class KnowledgeRetrievalNode(Node): +class KnowledgeRetrievalNode(LLMUsageTrackingMixin, Node): node_type = NodeType.KNOWLEDGE_RETRIEVAL _node_data: KnowledgeRetrievalNodeData @@ -182,14 +185,21 @@ class KnowledgeRetrievalNode(Node): ) # retrieve knowledge + usage = LLMUsage.empty_usage() try: - results = self._fetch_dataset_retriever(node_data=self._node_data, query=query) + results, usage = self._fetch_dataset_retriever(node_data=self._node_data, query=query) outputs = {"result": ArrayObjectSegment(value=results)} return NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=variables, - process_data={}, + process_data={"usage": jsonable_encoder(usage)}, outputs=outputs, # type: ignore + metadata={ + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: usage.currency, + }, + llm_usage=usage, ) except KnowledgeRetrievalNodeError as e: @@ -199,6 +209,7 @@ class KnowledgeRetrievalNode(Node): inputs=variables, error=str(e), error_type=type(e).__name__, + llm_usage=usage, ) # Temporary handle all exceptions from DatasetRetrieval class here. except Exception as e: @@ -207,11 +218,15 @@ class KnowledgeRetrievalNode(Node): inputs=variables, error=str(e), error_type=type(e).__name__, + llm_usage=usage, ) finally: db.session.close() - def _fetch_dataset_retriever(self, node_data: KnowledgeRetrievalNodeData, query: str) -> list[dict[str, Any]]: + def _fetch_dataset_retriever( + self, node_data: KnowledgeRetrievalNodeData, query: str + ) -> tuple[list[dict[str, Any]], LLMUsage]: + usage = LLMUsage.empty_usage() available_datasets = [] dataset_ids = node_data.dataset_ids @@ -245,9 +260,10 @@ class KnowledgeRetrievalNode(Node): if not dataset: continue available_datasets.append(dataset) - metadata_filter_document_ids, metadata_condition = self._get_metadata_filter_condition( + metadata_filter_document_ids, metadata_condition, metadata_usage = self._get_metadata_filter_condition( [dataset.id for dataset in available_datasets], query, node_data ) + usage = self._merge_usage(usage, metadata_usage) all_documents = [] dataset_retrieval = DatasetRetrieval() if node_data.retrieval_mode == DatasetRetrieveConfigEntity.RetrieveStrategy.SINGLE: @@ -330,6 +346,8 @@ class KnowledgeRetrievalNode(Node): metadata_filter_document_ids=metadata_filter_document_ids, metadata_condition=metadata_condition, ) + usage = self._merge_usage(usage, dataset_retrieval.llm_usage) + dify_documents = [item for item in all_documents if item.provider == "dify"] external_documents = [item for item in all_documents if item.provider == "external"] retrieval_resource_list = [] @@ -406,11 +424,12 @@ class KnowledgeRetrievalNode(Node): ) for position, item in enumerate(retrieval_resource_list, start=1): item["metadata"]["position"] = position - return retrieval_resource_list + return retrieval_resource_list, usage def _get_metadata_filter_condition( self, dataset_ids: list, query: str, node_data: KnowledgeRetrievalNodeData - ) -> tuple[dict[str, list[str]] | None, MetadataCondition | None]: + ) -> tuple[dict[str, list[str]] | None, MetadataCondition | None, LLMUsage]: + usage = LLMUsage.empty_usage() document_query = db.session.query(Document).where( Document.dataset_id.in_(dataset_ids), Document.indexing_status == "completed", @@ -420,9 +439,12 @@ class KnowledgeRetrievalNode(Node): filters: list[Any] = [] metadata_condition = None if node_data.metadata_filtering_mode == "disabled": - return None, None + return None, None, usage elif node_data.metadata_filtering_mode == "automatic": - automatic_metadata_filters = self._automatic_metadata_filter_func(dataset_ids, query, node_data) + automatic_metadata_filters, automatic_usage = self._automatic_metadata_filter_func( + dataset_ids, query, node_data + ) + usage = self._merge_usage(usage, automatic_usage) if automatic_metadata_filters: conditions = [] for sequence, filter in enumerate(automatic_metadata_filters): @@ -496,11 +518,12 @@ class KnowledgeRetrievalNode(Node): metadata_filter_document_ids = defaultdict(list) if documents else None # type: ignore for document in documents: metadata_filter_document_ids[document.dataset_id].append(document.id) # type: ignore - return metadata_filter_document_ids, metadata_condition + return metadata_filter_document_ids, metadata_condition, usage def _automatic_metadata_filter_func( self, dataset_ids: list, query: str, node_data: KnowledgeRetrievalNodeData - ) -> list[dict[str, Any]]: + ) -> tuple[list[dict[str, Any]], LLMUsage]: + usage = LLMUsage.empty_usage() # get all metadata field stmt = select(DatasetMetadata).where(DatasetMetadata.dataset_id.in_(dataset_ids)) metadata_fields = db.session.scalars(stmt).all() @@ -548,6 +571,7 @@ class KnowledgeRetrievalNode(Node): for event in generator: if isinstance(event, ModelInvokeCompletedEvent): result_text = event.text + usage = self._merge_usage(usage, event.usage) break result_text_json = parse_and_check_json_markdown(result_text, []) @@ -564,8 +588,8 @@ class KnowledgeRetrievalNode(Node): } ) except Exception: - return [] - return automatic_metadata_filters + return [], usage + return automatic_metadata_filters, usage def _process_metadata_filter_func( self, sequence: int, condition: str, metadata_name: str, value: Any, filters: list[Any] diff --git a/api/core/workflow/nodes/loop/loop_node.py b/api/core/workflow/nodes/loop/loop_node.py index c35195e931..ca39e5aa23 100644 --- a/api/core/workflow/nodes/loop/loop_node.py +++ b/api/core/workflow/nodes/loop/loop_node.py @@ -5,6 +5,7 @@ from collections.abc import Callable, Generator, Mapping, Sequence from datetime import datetime from typing import TYPE_CHECKING, Any, Literal, cast +from core.model_runtime.entities.llm_entities import LLMUsage from core.variables import Segment, SegmentType from core.workflow.enums import ( ErrorStrategy, @@ -27,6 +28,7 @@ from core.workflow.node_events import ( NodeRunResult, StreamCompletedEvent, ) +from core.workflow.nodes.base import LLMUsageTrackingMixin from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig from core.workflow.nodes.base.node import Node from core.workflow.nodes.loop.entities import LoopNodeData, LoopVariableData @@ -40,7 +42,7 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -class LoopNode(Node): +class LoopNode(LLMUsageTrackingMixin, Node): """ Loop Node. """ @@ -117,6 +119,7 @@ class LoopNode(Node): loop_duration_map: dict[str, float] = {} single_loop_variable_map: dict[str, dict[str, Any]] = {} # single loop variable output + loop_usage = LLMUsage.empty_usage() # Start Loop event yield LoopStartedEvent( @@ -163,6 +166,9 @@ class LoopNode(Node): # Update the total tokens from this iteration cost_tokens += graph_engine.graph_runtime_state.total_tokens + # Accumulate usage from the sub-graph execution + loop_usage = self._merge_usage(loop_usage, graph_engine.graph_runtime_state.llm_usage) + # Collect loop variable values after iteration single_loop_variable = {} for key, selector in loop_variable_selectors.items(): @@ -189,6 +195,7 @@ class LoopNode(Node): ) self.graph_runtime_state.total_tokens += cost_tokens + self._accumulate_usage(loop_usage) # Loop completed successfully yield LoopSucceededEvent( start_at=start_at, @@ -196,7 +203,9 @@ class LoopNode(Node): outputs=self._node_data.outputs, steps=loop_count, metadata={ - WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: cost_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: loop_usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: loop_usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: loop_usage.currency, "completed_reason": "loop_break" if reach_break_condition else "loop_completed", WorkflowNodeExecutionMetadataKey.LOOP_DURATION_MAP: loop_duration_map, WorkflowNodeExecutionMetadataKey.LOOP_VARIABLE_MAP: single_loop_variable_map, @@ -207,22 +216,28 @@ class LoopNode(Node): node_run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, metadata={ - WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: loop_usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: loop_usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: loop_usage.currency, WorkflowNodeExecutionMetadataKey.LOOP_DURATION_MAP: loop_duration_map, WorkflowNodeExecutionMetadataKey.LOOP_VARIABLE_MAP: single_loop_variable_map, }, outputs=self._node_data.outputs, inputs=inputs, + llm_usage=loop_usage, ) ) except Exception as e: + self._accumulate_usage(loop_usage) yield LoopFailedEvent( start_at=start_at, inputs=inputs, steps=loop_count, metadata={ - WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: loop_usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: loop_usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: loop_usage.currency, "completed_reason": "error", WorkflowNodeExecutionMetadataKey.LOOP_DURATION_MAP: loop_duration_map, WorkflowNodeExecutionMetadataKey.LOOP_VARIABLE_MAP: single_loop_variable_map, @@ -235,10 +250,13 @@ class LoopNode(Node): status=WorkflowNodeExecutionStatus.FAILED, error=str(e), metadata={ - WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: loop_usage.total_tokens, + WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: loop_usage.total_price, + WorkflowNodeExecutionMetadataKey.CURRENCY: loop_usage.currency, WorkflowNodeExecutionMetadataKey.LOOP_DURATION_MAP: loop_duration_map, WorkflowNodeExecutionMetadataKey.LOOP_VARIABLE_MAP: single_loop_variable_map, }, + llm_usage=loop_usage, ) ) diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 2e2c32ac93..69ab6f0718 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -6,10 +6,13 @@ from sqlalchemy.orm import Session from core.callback_handler.workflow_tool_callback_handler import DifyWorkflowCallbackHandler from core.file import File, FileTransferMethod +from core.model_runtime.entities.llm_entities import LLMUsage +from core.tools.__base.tool import Tool from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter from core.tools.errors import ToolInvokeError from core.tools.tool_engine import ToolEngine from core.tools.utils.message_transformer import ToolFileMessageTransformer +from core.tools.workflow_as_tool.tool import WorkflowTool from core.variables.segments import ArrayAnySegment, ArrayFileSegment from core.variables.variables import ArrayAnyVariable from core.workflow.enums import ( @@ -136,13 +139,14 @@ class ToolNode(Node): try: # convert tool messages - yield from self._transform_message( + _ = yield from self._transform_message( messages=message_stream, tool_info=tool_info, parameters_for_log=parameters_for_log, user_id=self.user_id, tenant_id=self.tenant_id, node_id=self._node_id, + tool_runtime=tool_runtime, ) except ToolInvokeError as e: yield StreamCompletedEvent( @@ -236,7 +240,8 @@ class ToolNode(Node): user_id: str, tenant_id: str, node_id: str, - ) -> Generator: + tool_runtime: Tool, + ) -> Generator[NodeEventBase, None, LLMUsage]: """ Convert ToolInvokeMessages into tuple[plain_text, files] """ @@ -424,17 +429,34 @@ class ToolNode(Node): is_final=True, ) + usage = self._extract_tool_usage(tool_runtime) + + metadata: dict[WorkflowNodeExecutionMetadataKey, Any] = { + WorkflowNodeExecutionMetadataKey.TOOL_INFO: tool_info, + } + if usage.total_tokens > 0: + metadata[WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS] = usage.total_tokens + metadata[WorkflowNodeExecutionMetadataKey.TOTAL_PRICE] = usage.total_price + metadata[WorkflowNodeExecutionMetadataKey.CURRENCY] = usage.currency + yield StreamCompletedEvent( node_run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs={"text": text, "files": ArrayFileSegment(value=files), "json": json_output, **variables}, - metadata={ - WorkflowNodeExecutionMetadataKey.TOOL_INFO: tool_info, - }, + metadata=metadata, inputs=parameters_for_log, + llm_usage=usage, ) ) + return usage + + @staticmethod + def _extract_tool_usage(tool_runtime: Tool) -> LLMUsage: + if isinstance(tool_runtime, WorkflowTool): + return tool_runtime.latest_usage + return LLMUsage.empty_usage() + @classmethod def _extract_variable_selector_to_variable_mapping( cls, From caf1a5fbab68c2d97e855ab211b6cc2d399b6aaf Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 21 Oct 2025 16:23:17 +0800 Subject: [PATCH 07/33] Fix variable truncator handling for UpdatedVariable (#27197) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- api/services/variable_truncator.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/api/services/variable_truncator.py b/api/services/variable_truncator.py index a8f37c31c8..9a014b400f 100644 --- a/api/services/variable_truncator.py +++ b/api/services/variable_truncator.py @@ -17,6 +17,7 @@ from core.variables.segments import ( StringSegment, ) from core.variables.utils import dumps_with_segments +from core.workflow.nodes.variable_assigner.common.helpers import UpdatedVariable _MAX_DEPTH = 100 @@ -202,6 +203,9 @@ class VariableTruncator: """Recursively calculate JSON size without serialization.""" if isinstance(value, Segment): return VariableTruncator.calculate_json_size(value.value) + if isinstance(value, UpdatedVariable): + # TODO(Workflow): migrate UpdatedVariable serialization upstream and drop this fallback. + return VariableTruncator.calculate_json_size(value.model_dump(), depth=depth + 1) if depth > _MAX_DEPTH: raise MaxDepthExceededError() if isinstance(value, str): @@ -387,10 +391,13 @@ class VariableTruncator: def _truncate_json_primitives(self, val: None, target_size: int) -> _PartResult[None]: ... def _truncate_json_primitives( - self, val: str | list | dict | bool | int | float | None, target_size: int + self, val: UpdatedVariable | str | list | dict | bool | int | float | None, target_size: int ) -> _PartResult[Any]: """Truncate a value within an object to fit within budget.""" - if isinstance(val, str): + if isinstance(val, UpdatedVariable): + # TODO(Workflow): push UpdatedVariable normalization closer to its producer. + return self._truncate_object(val.model_dump(), target_size) + elif isinstance(val, str): return self._truncate_string(val, target_size) elif isinstance(val, list): return self._truncate_array(val, target_size) From cfc3f1527abd0a716a0f000462deadf0d8e10fe2 Mon Sep 17 00:00:00 2001 From: Nite Knite Date: Tue, 21 Oct 2025 16:23:49 +0800 Subject: [PATCH 08/33] chore: switch support channels according to configuration (#27195) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../header/account-dropdown/support.tsx | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/web/app/components/header/account-dropdown/support.tsx b/web/app/components/header/account-dropdown/support.tsx index fda45f2db5..b165c5fcca 100644 --- a/web/app/components/header/account-dropdown/support.tsx +++ b/web/app/components/header/account-dropdown/support.tsx @@ -1,5 +1,5 @@ import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' -import { RiArrowRightSLine, RiArrowRightUpLine, RiChatSmile2Line, RiDiscordLine, RiFeedbackLine, RiQuestionLine } from '@remixicon/react' +import { RiArrowRightSLine, RiArrowRightUpLine, RiChatSmile2Line, RiDiscordLine, RiFeedbackLine, RiMailSendLine, RiQuestionLine } from '@remixicon/react' import { Fragment } from 'react' import Link from 'next/link' import { useTranslation } from 'react-i18next' @@ -7,6 +7,9 @@ import cn from '@/utils/classnames' import { useProviderContext } from '@/context/provider-context' import { Plan } from '@/app/components/billing/type' import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils' +import { mailToSupport } from '../utils/util' +import { useAppContext } from '@/context/app-context' +import { ZENDESK_WIDGET_KEY } from '@/config' type SupportProps = { closeAccountDropdown: () => void @@ -19,6 +22,7 @@ export default function Support({ closeAccountDropdown }: SupportProps) { ` const { t } = useTranslation() const { plan } = useProviderContext() + const { userProfile, langGeniusVersionInfo } = useAppContext() const hasDedicatedChannel = plan.type !== Plan.sandbox return @@ -50,18 +54,33 @@ export default function Support({ closeAccountDropdown }: SupportProps) { )} >
- {hasDedicatedChannel && - - } + {hasDedicatedChannel && ( + + {ZENDESK_WIDGET_KEY && ZENDESK_WIDGET_KEY.trim() !== '' ? ( + + ) : ( + + +
{t('common.userProfile.emailSupport')}
+ +
+ )} +
+ )} Date: Tue, 21 Oct 2025 17:12:17 +0800 Subject: [PATCH 09/33] fix: eagerly load EndUser attributes to prevent DetachedInstanceError (#27162) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Novice --- api/controllers/mcp/mcp.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index 85b7df229f..8d8fe6b3a8 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -193,15 +193,16 @@ class MCPAppApi(Resource): except ValidationError as e: raise MCPRequestError(mcp_types.INVALID_PARAMS, f"Invalid MCP request: {str(e)}") - def _retrieve_end_user(self, tenant_id: str, mcp_server_id: str, session: Session) -> EndUser | None: - """Get end user from existing session - optimized query""" - return ( - session.query(EndUser) - .where(EndUser.tenant_id == tenant_id) - .where(EndUser.session_id == mcp_server_id) - .where(EndUser.type == "mcp") - .first() - ) + def _retrieve_end_user(self, tenant_id: str, mcp_server_id: str) -> EndUser | None: + """Get end user - manages its own database session""" + with Session(db.engine, expire_on_commit=False) as session, session.begin(): + return ( + session.query(EndUser) + .where(EndUser.tenant_id == tenant_id) + .where(EndUser.session_id == mcp_server_id) + .where(EndUser.type == "mcp") + .first() + ) def _create_end_user( self, client_name: str, tenant_id: str, app_id: str, mcp_server_id: str, session: Session @@ -229,7 +230,7 @@ class MCPAppApi(Resource): request_id: Union[int, str], ) -> mcp_types.JSONRPCResponse | mcp_types.JSONRPCError | None: """Handle MCP request and return response""" - end_user = self._retrieve_end_user(mcp_server.tenant_id, mcp_server.id, session) + end_user = self._retrieve_end_user(mcp_server.tenant_id, mcp_server.id) if not end_user and isinstance(mcp_request.root, mcp_types.InitializeRequest): client_info = mcp_request.root.params.clientInfo From c327cfa86e438367ab44caaa278996deb9948034 Mon Sep 17 00:00:00 2001 From: GuanMu Date: Tue, 21 Oct 2025 17:44:26 +0800 Subject: [PATCH 10/33] fix(storybook): add required handler props and fix TypeScript errors in component stories (#27187) --- web/.storybook/main.ts | 4 -- .../auto-height-textarea/index.stories.tsx | 9 +++++ .../base/chat/chat/answer/index.stories.tsx | 3 +- .../components/base/confirm/index.stories.tsx | 17 ++++++++ .../base/input-number/index.stories.tsx | 23 ++++++++--- .../base/radio-card/index.stories.tsx | 21 ++++++---- .../base/search-input/index.stories.tsx | 33 ++++++++++++--- .../components/base/select/index.stories.tsx | 40 ++++++++++++++----- .../components/base/slider/index.stories.tsx | 32 ++++++++++----- .../base/tag-input/index.stories.tsx | 36 ++++++++++++----- .../base/textarea/index.stories.tsx | 31 +++++++++----- .../base/voice-input/index.stories.tsx | 2 +- 12 files changed, 189 insertions(+), 62 deletions(-) diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index e656115ceb..57abae42ab 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -1,9 +1,5 @@ import type { StorybookConfig } from '@storybook/nextjs' import path from 'node:path' -import { fileURLToPath } from 'node:url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) const config: StorybookConfig = { stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'], diff --git a/web/app/components/base/auto-height-textarea/index.stories.tsx b/web/app/components/base/auto-height-textarea/index.stories.tsx index f083e4f56d..dcbcb253c6 100644 --- a/web/app/components/base/auto-height-textarea/index.stories.tsx +++ b/web/app/components/base/auto-height-textarea/index.stories.tsx @@ -23,6 +23,10 @@ const meta = { control: 'text', description: 'Textarea value', }, + onChange: { + action: 'changed', + description: 'Change handler', + }, minHeight: { control: 'number', description: 'Minimum height in pixels', @@ -44,6 +48,11 @@ const meta = { description: 'Wrapper CSS classes', }, }, + args: { + onChange: (e) => { + console.log('Text changed:', e.target.value) + }, + }, } satisfies Meta export default meta diff --git a/web/app/components/base/chat/chat/answer/index.stories.tsx b/web/app/components/base/chat/chat/answer/index.stories.tsx index a83c0fea61..02d0f015b5 100644 --- a/web/app/components/base/chat/chat/answer/index.stories.tsx +++ b/web/app/components/base/chat/chat/answer/index.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs' +import { WorkflowRunningStatus } from '@/app/components/workflow/types' import type { ChatItem } from '../../types' import { markdownContent } from './__mocks__/markdownContent' import { markdownContentSVG } from './__mocks__/markdownContentSVG' @@ -33,7 +34,7 @@ const mockedBaseChatItem = { } satisfies ChatItem const mockedWorkflowProcess = { - status: 'succeeded', + status: WorkflowRunningStatus.Succeeded, tracing: [], } diff --git a/web/app/components/base/confirm/index.stories.tsx b/web/app/components/base/confirm/index.stories.tsx index dfbe00f293..a524137b79 100644 --- a/web/app/components/base/confirm/index.stories.tsx +++ b/web/app/components/base/confirm/index.stories.tsx @@ -62,6 +62,14 @@ const meta = { description: 'Whether clicking mask closes dialog', }, }, + args: { + onConfirm: () => { + console.log('✅ User clicked confirm') + }, + onCancel: () => { + console.log('❌ User clicked cancel') + }, + }, } satisfies Meta export default meta @@ -99,6 +107,7 @@ export const WarningDialog: Story = { type: 'warning', title: 'Delete Confirmation', content: 'Are you sure you want to delete this project? This action cannot be undone.', + isShow: false, }, } @@ -109,6 +118,7 @@ export const InfoDialog: Story = { type: 'info', title: 'Notice', content: 'Your changes have been saved. Do you want to proceed to the next step?', + isShow: false, }, } @@ -121,6 +131,7 @@ export const CustomButtonText: Story = { content: 'You have unsaved changes. Are you sure you want to exit?', confirmText: 'Discard Changes', cancelText: 'Continue Editing', + isShow: false, }, } @@ -132,6 +143,7 @@ export const LoadingState: Story = { title: 'Deleting...', content: 'Please wait while we delete the file...', isLoading: true, + isShow: false, }, } @@ -143,6 +155,7 @@ export const DisabledState: Story = { title: 'Verification Required', content: 'Please complete email verification before proceeding.', isDisabled: true, + isShow: false, }, } @@ -155,6 +168,7 @@ export const AlertStyle: Story = { content: 'Your settings have been updated!', showCancel: false, confirmText: 'Got it', + isShow: false, }, } @@ -167,6 +181,7 @@ export const DangerousAction: Story = { content: 'This action will permanently delete your account and all associated data, including: all projects and files, collaboration history, and personal settings. This action cannot be reversed!', confirmText: 'Delete My Account', cancelText: 'Keep My Account', + isShow: false, }, } @@ -178,6 +193,7 @@ export const NotMaskClosable: Story = { title: 'Important Action', content: 'This action requires your explicit choice. Clicking outside will not close this dialog.', maskClosable: false, + isShow: false, }, } @@ -195,5 +211,6 @@ export const Playground: Story = { showConfirm: true, showCancel: true, maskClosable: true, + isShow: false, }, } diff --git a/web/app/components/base/input-number/index.stories.tsx b/web/app/components/base/input-number/index.stories.tsx index 0fca2e52f9..9bb3ec1f8c 100644 --- a/web/app/components/base/input-number/index.stories.tsx +++ b/web/app/components/base/input-number/index.stories.tsx @@ -49,6 +49,11 @@ const meta = { description: 'Default value when undefined', }, }, + args: { + onChange: (value) => { + console.log('Value changed:', value) + }, + }, } satisfies Meta export default meta @@ -196,7 +201,8 @@ const SizeComparisonDemo = () => { export const SizeComparison: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Font size picker const FontSizePickerDemo = () => { @@ -228,7 +234,8 @@ const FontSizePickerDemo = () => { export const FontSizePicker: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Quantity selector const QuantitySelectorDemo = () => { @@ -268,7 +275,8 @@ const QuantitySelectorDemo = () => { export const QuantitySelector: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Timer settings const TimerSettingsDemo = () => { @@ -324,7 +332,8 @@ const TimerSettingsDemo = () => { export const TimerSettings: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Animation settings const AnimationSettingsDemo = () => { @@ -380,7 +389,8 @@ const AnimationSettingsDemo = () => { export const AnimationSettings: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Temperature control const TemperatureControlDemo = () => { @@ -420,7 +430,8 @@ const TemperatureControlDemo = () => { export const TemperatureControl: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Interactive playground export const Playground: Story = { diff --git a/web/app/components/base/radio-card/index.stories.tsx b/web/app/components/base/radio-card/index.stories.tsx index 81c89fca24..e129cd7033 100644 --- a/web/app/components/base/radio-card/index.stories.tsx +++ b/web/app/components/base/radio-card/index.stories.tsx @@ -138,7 +138,8 @@ const WithConfigurationDemo = () => { export const WithConfiguration: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Multiple cards selection const MultipleCardsDemo = () => { @@ -190,7 +191,8 @@ const MultipleCardsDemo = () => { export const MultipleCards: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Cloud provider selection const CloudProviderSelectionDemo = () => { @@ -247,7 +249,8 @@ const CloudProviderSelectionDemo = () => { export const CloudProviderSelection: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Deployment strategy const DeploymentStrategyDemo = () => { @@ -313,7 +316,8 @@ const DeploymentStrategyDemo = () => { export const DeploymentStrategy: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Storage options const StorageOptionsDemo = () => { @@ -388,7 +392,8 @@ const StorageOptionsDemo = () => { export const StorageOptions: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - API authentication method const APIAuthMethodDemo = () => { @@ -458,7 +463,8 @@ const APIAuthMethodDemo = () => { export const APIAuthMethod: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Interactive playground const PlaygroundDemo = () => { @@ -501,4 +507,5 @@ const PlaygroundDemo = () => { export const Playground: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story diff --git a/web/app/components/base/search-input/index.stories.tsx b/web/app/components/base/search-input/index.stories.tsx index 53d1fc5218..99d60d52ff 100644 --- a/web/app/components/base/search-input/index.stories.tsx +++ b/web/app/components/base/search-input/index.stories.tsx @@ -19,6 +19,10 @@ const meta = { control: 'text', description: 'Search input value', }, + onChange: { + action: 'changed', + description: 'Change handler', + }, placeholder: { control: 'text', description: 'Placeholder text', @@ -32,6 +36,11 @@ const meta = { description: 'Additional CSS classes', }, }, + args: { + onChange: (v) => { + console.log('Search value changed:', v) + }, + }, } satisfies Meta export default meta @@ -66,6 +75,10 @@ export const Default: Story = { args: { placeholder: 'Search...', white: false, + value: '', + onChange: (v) => { + console.log('Search value changed:', v) + }, }, } @@ -75,6 +88,7 @@ export const WhiteBackground: Story = { args: { placeholder: 'Search...', white: true, + value: '', }, } @@ -94,6 +108,7 @@ export const CustomPlaceholder: Story = { args: { placeholder: 'Search documents, files, and more...', white: false, + value: '', }, } @@ -156,7 +171,8 @@ const UserListSearchDemo = () => { export const UserListSearch: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Product search const ProductSearchDemo = () => { @@ -209,7 +225,8 @@ const ProductSearchDemo = () => { export const ProductSearch: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Documentation search const DocumentationSearchDemo = () => { @@ -271,7 +288,8 @@ const DocumentationSearchDemo = () => { export const DocumentationSearch: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Command palette const CommandPaletteDemo = () => { @@ -330,7 +348,8 @@ const CommandPaletteDemo = () => { export const CommandPalette: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Live search with results count const LiveSearchWithCountDemo = () => { @@ -384,7 +403,8 @@ const LiveSearchWithCountDemo = () => { export const LiveSearchWithCount: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Size variations const SizeVariationsDemo = () => { @@ -422,7 +442,8 @@ const SizeVariationsDemo = () => { export const SizeVariations: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Interactive playground export const Playground: Story = { diff --git a/web/app/components/base/select/index.stories.tsx b/web/app/components/base/select/index.stories.tsx index a54476cc0a..2a107155a5 100644 --- a/web/app/components/base/select/index.stories.tsx +++ b/web/app/components/base/select/index.stories.tsx @@ -33,6 +33,11 @@ const meta = { description: 'Hide check icon on selected item', }, }, + args: { + onSelect: (item) => { + console.log('Selected:', item) + }, + }, } satisfies Meta export default meta @@ -87,6 +92,7 @@ export const Default: Story = { args: { placeholder: 'Select a fruit...', defaultValue: 'apple', + items: [], }, } @@ -96,6 +102,7 @@ export const WithPlaceholder: Story = { args: { placeholder: 'Choose an option...', defaultValue: '', + items: [], }, } @@ -106,6 +113,7 @@ export const Disabled: Story = { placeholder: 'Select a fruit...', defaultValue: 'banana', disabled: true, + items: [], }, } @@ -116,6 +124,7 @@ export const NotClearable: Story = { placeholder: 'Select a fruit...', defaultValue: 'cherry', notClearable: true, + items: [], }, } @@ -126,6 +135,7 @@ export const HideChecked: Story = { placeholder: 'Select a fruit...', defaultValue: 'apple', hideChecked: true, + items: [], }, } @@ -153,7 +163,8 @@ const WithSearchDemo = () => { export const WithSearch: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // PortalSelect const PortalSelectVariantDemo = () => { @@ -179,7 +190,8 @@ const PortalSelectVariantDemo = () => { export const PortalSelectVariant: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Custom render option const CustomRenderOptionDemo = () => { @@ -215,7 +227,8 @@ const CustomRenderOptionDemo = () => { export const CustomRenderOption: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Loading state export const LoadingState: Story = { @@ -232,7 +245,8 @@ export const LoadingState: Story = {
) }, -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Form field const FormFieldDemo = () => { @@ -297,7 +311,8 @@ const FormFieldDemo = () => { export const FormField: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Filter selector const FilterSelectorDemo = () => { @@ -359,7 +374,8 @@ const FilterSelectorDemo = () => { export const FilterSelector: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Version selector with badge const VersionSelectorDemo = () => { @@ -398,7 +414,8 @@ const VersionSelectorDemo = () => { export const VersionSelector: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Settings dropdown const SettingsDropdownDemo = () => { @@ -447,7 +464,8 @@ const SettingsDropdownDemo = () => { export const SettingsDropdown: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Comparison of variants const VariantComparisonDemo = () => { @@ -504,7 +522,8 @@ const VariantComparisonDemo = () => { export const VariantComparison: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Interactive playground const PlaygroundDemo = () => { @@ -524,4 +543,5 @@ const PlaygroundDemo = () => { export const Playground: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story diff --git a/web/app/components/base/slider/index.stories.tsx b/web/app/components/base/slider/index.stories.tsx index de4258b3ac..d350877d18 100644 --- a/web/app/components/base/slider/index.stories.tsx +++ b/web/app/components/base/slider/index.stories.tsx @@ -36,6 +36,11 @@ const meta = { description: 'Disabled state', }, }, + args: { + onChange: (value) => { + console.log('Slider value:', value) + }, + }, } satisfies Meta export default meta @@ -157,7 +162,8 @@ const VolumeControlDemo = () => { export const VolumeControl: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Brightness control const BrightnessControlDemo = () => { @@ -187,7 +193,8 @@ const BrightnessControlDemo = () => { export const BrightnessControl: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Price range filter const PriceRangeFilterDemo = () => { @@ -239,7 +246,8 @@ const PriceRangeFilterDemo = () => { export const PriceRangeFilter: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Temperature selector const TemperatureSelectorDemo = () => { @@ -279,7 +287,8 @@ const TemperatureSelectorDemo = () => { export const TemperatureSelector: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Progress/completion slider const ProgressSliderDemo = () => { @@ -325,7 +334,8 @@ const ProgressSliderDemo = () => { export const ProgressSlider: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Zoom control const ZoomControlDemo = () => { @@ -371,7 +381,8 @@ const ZoomControlDemo = () => { export const ZoomControl: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - AI model parameters const AIModelParametersDemo = () => { @@ -445,7 +456,8 @@ const AIModelParametersDemo = () => { export const AIModelParameters: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Image quality selector const ImageQualitySelectorDemo = () => { @@ -488,7 +500,8 @@ const ImageQualitySelectorDemo = () => { export const ImageQualitySelector: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Multiple sliders const MultipleSlidersDemo = () => { @@ -545,7 +558,8 @@ const MultipleSlidersDemo = () => { export const MultipleSliders: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Interactive playground export const Playground: Story = { diff --git a/web/app/components/base/tag-input/index.stories.tsx b/web/app/components/base/tag-input/index.stories.tsx index 8b5a33201e..dacb222c8c 100644 --- a/web/app/components/base/tag-input/index.stories.tsx +++ b/web/app/components/base/tag-input/index.stories.tsx @@ -19,6 +19,10 @@ const meta = { control: 'object', description: 'Array of tag strings', }, + onChange: { + action: 'changed', + description: 'Change handler', + }, disableAdd: { control: 'boolean', description: 'Disable adding new tags', @@ -41,6 +45,11 @@ const meta = { description: 'Require non-empty tags', }, }, + args: { + onChange: (items) => { + console.log('Tags updated:', items) + }, + }, } satisfies Meta export default meta @@ -155,7 +164,8 @@ const SkillTagsDemo = () => { export const SkillTags: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Email tags const EmailTagsDemo = () => { @@ -192,7 +202,8 @@ const EmailTagsDemo = () => { export const EmailTags: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Search filters const SearchFiltersDemo = () => { @@ -246,7 +257,8 @@ const SearchFiltersDemo = () => { export const SearchFilters: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Product categories const ProductCategoriesDemo = () => { @@ -292,7 +304,8 @@ const ProductCategoriesDemo = () => { export const ProductCategories: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Keyword extraction const KeywordExtractionDemo = () => { @@ -328,7 +341,8 @@ const KeywordExtractionDemo = () => { export const KeywordExtraction: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Tags with suggestions const TagsWithSuggestionsDemo = () => { @@ -371,7 +385,8 @@ const TagsWithSuggestionsDemo = () => { export const TagsWithSuggestions: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Stop sequences (Tab mode) const StopSequencesDemo = () => { @@ -425,7 +440,8 @@ const StopSequencesDemo = () => { export const StopSequences: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Multi-language tags const MultiLanguageTagsDemo = () => { @@ -461,7 +477,8 @@ const MultiLanguageTagsDemo = () => { export const MultiLanguageTags: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Validation showcase const ValidationShowcaseDemo = () => { @@ -500,7 +517,8 @@ const ValidationShowcaseDemo = () => { export const ValidationShowcase: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Interactive playground export const Playground: Story = { diff --git a/web/app/components/base/textarea/index.stories.tsx b/web/app/components/base/textarea/index.stories.tsx index 095420ce17..d03b3decb7 100644 --- a/web/app/components/base/textarea/index.stories.tsx +++ b/web/app/components/base/textarea/index.stories.tsx @@ -76,6 +76,7 @@ export const Default: Story = { size: 'regular', placeholder: 'Enter text...', rows: 4, + value: '', }, } @@ -86,6 +87,7 @@ export const SmallSize: Story = { size: 'small', placeholder: 'Small textarea...', rows: 3, + value: '', }, } @@ -96,6 +98,7 @@ export const LargeSize: Story = { size: 'large', placeholder: 'Large textarea...', rows: 5, + value: '', }, } @@ -175,7 +178,8 @@ const SizeComparisonDemo = () => { export const SizeComparison: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // State comparison const StateComparisonDemo = () => { @@ -216,7 +220,8 @@ const StateComparisonDemo = () => { export const StateComparison: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Comment form const CommentFormDemo = () => { @@ -250,7 +255,8 @@ const CommentFormDemo = () => { export const CommentForm: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Feedback form const FeedbackFormDemo = () => { @@ -291,7 +297,8 @@ const FeedbackFormDemo = () => { export const FeedbackForm: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Code snippet const CodeSnippetDemo = () => { @@ -322,7 +329,8 @@ const CodeSnippetDemo = () => { export const CodeSnippet: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Message composer const MessageComposerDemo = () => { @@ -372,7 +380,8 @@ const MessageComposerDemo = () => { export const MessageComposer: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Bio editor const BioEditorDemo = () => { @@ -408,7 +417,8 @@ const BioEditorDemo = () => { export const BioEditor: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - JSON editor const JSONEditorDemo = () => { @@ -472,7 +482,8 @@ const JSONEditorDemo = () => { export const JSONEditor: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Real-world example - Task description const TaskDescriptionDemo = () => { @@ -520,7 +531,8 @@ const TaskDescriptionDemo = () => { export const TaskDescription: Story = { render: () => , -} + parameters: { controls: { disable: true } }, +} as unknown as Story // Interactive playground export const Playground: Story = { @@ -531,5 +543,6 @@ export const Playground: Story = { rows: 4, disabled: false, destructive: false, + value: '', }, } diff --git a/web/app/components/base/voice-input/index.stories.tsx b/web/app/components/base/voice-input/index.stories.tsx index 8cfbed434b..8d92f587c4 100644 --- a/web/app/components/base/voice-input/index.stories.tsx +++ b/web/app/components/base/voice-input/index.stories.tsx @@ -29,7 +29,7 @@ const VoiceInputMock = ({ onConverted, onCancel }: any) => {
{/* Waveform visualization placeholder */}
- {new Array(40).fill().map((_, i) => ( + {new Array(40).fill(0).map((_, i) => (
Date: Tue, 21 Oct 2025 17:49:38 +0800 Subject: [PATCH 11/33] add billing enable check (#27213) --- api/core/app/apps/pipeline/pipeline_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/app/apps/pipeline/pipeline_generator.py b/api/core/app/apps/pipeline/pipeline_generator.py index 1fb076b685..f8bfbce37a 100644 --- a/api/core/app/apps/pipeline/pipeline_generator.py +++ b/api/core/app/apps/pipeline/pipeline_generator.py @@ -255,7 +255,7 @@ class PipelineGenerator(BaseAppGenerator): json_text = json.dumps(text) upload_file = FileService(db.engine).upload_text(json_text, name, user.id, dataset.tenant_id) features = FeatureService.get_features(dataset.tenant_id) - if features.billing.subscription.plan == "sandbox": + if features.billing.enabled and features.billing.subscription.plan == "sandbox": tenant_pipeline_task_key = f"tenant_pipeline_task:{dataset.tenant_id}" tenant_self_pipeline_task_queue = f"tenant_self_pipeline_task_queue:{dataset.tenant_id}" From a893ee0ffc8a58d84c17bc0587b25c69a98b04d7 Mon Sep 17 00:00:00 2001 From: Jyong <76649700+JohnJyong@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:40:36 +0800 Subject: [PATCH 12/33] Feat/add celery prefetch setting (#27218) --- api/docker/entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/docker/entrypoint.sh b/api/docker/entrypoint.sh index 08c0a1f35e..421d72a3a9 100755 --- a/api/docker/entrypoint.sh +++ b/api/docker/entrypoint.sh @@ -32,7 +32,8 @@ if [[ "${MODE}" == "worker" ]]; then exec celery -A celery_entrypoint.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION \ --max-tasks-per-child ${MAX_TASKS_PER_CHILD:-50} --loglevel ${LOG_LEVEL:-INFO} \ - -Q ${CELERY_QUEUES:-dataset,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation} + -Q ${CELERY_QUEUES:-dataset,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation} \ + --prefetch-multiplier=1 elif [[ "${MODE}" == "beat" ]]; then exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO} From 0c6cae2d594f93a0404d35743137b7eedf053751 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 21 Oct 2025 20:12:07 +0800 Subject: [PATCH 13/33] chore: align version identifiers with 1.9.2 (#27212) --- api/pyproject.toml | 2 +- api/uv.lock | 2 +- docker/docker-compose-template.yaml | 24 +++++------------------- docker/docker-compose.middleware.yaml | 2 +- docker/docker-compose.yaml | 24 +++++------------------- web/package.json | 2 +- 6 files changed, 14 insertions(+), 42 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 040d9658b3..5a9becaaef 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dify-api" -version = "1.9.1" +version = "1.9.2" requires-python = ">=3.11,<3.13" dependencies = [ diff --git a/api/uv.lock b/api/uv.lock index e7e51acedf..066f9a58a4 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1292,7 +1292,7 @@ wheels = [ [[package]] name = "dify-api" -version = "1.9.1" +version = "1.9.2" source = { virtual = "." } dependencies = [ { name = "arize-phoenix-otel" }, diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 8e4b8b8d7c..9650be90db 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.9.1 + image: langgenius/dify-api:1.9.2 restart: always environment: # Use the shared environment variables. @@ -24,13 +24,6 @@ services: volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage - # TODO: Remove this entrypoint override when weaviate-client 4.17.0 is included in the next Dify release - entrypoint: - - /bin/bash - - -c - - | - uv pip install --system weaviate-client==4.17.0 - exec /bin/bash /app/api/docker/entrypoint.sh networks: - ssrf_proxy_network - default @@ -38,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.9.1 + image: langgenius/dify-api:1.9.2 restart: always environment: # Use the shared environment variables. @@ -58,13 +51,6 @@ services: volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage - # TODO: Remove this entrypoint override when weaviate-client 4.17.0 is included in the next Dify release - entrypoint: - - /bin/bash - - -c - - | - uv pip install --system weaviate-client==4.17.0 - exec /bin/bash /app/api/docker/entrypoint.sh networks: - ssrf_proxy_network - default @@ -72,7 +58,7 @@ services: # worker_beat service # Celery beat for scheduling periodic tasks. worker_beat: - image: langgenius/dify-api:1.9.1 + image: langgenius/dify-api:1.9.2 restart: always environment: # Use the shared environment variables. @@ -90,7 +76,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.9.1 + image: langgenius/dify-web:1.9.2 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -193,7 +179,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.3.0-local + image: langgenius/dify-plugin-daemon:0.3.3-local restart: always environment: # Use the shared environment variables. diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index be0e321df5..9a1b9b53ba 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -87,7 +87,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.3.0-local + image: langgenius/dify-plugin-daemon:0.3.3-local restart: always env_file: - ./middleware.env diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index d384b8735f..d2ca6b859e 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -611,7 +611,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.9.1 + image: langgenius/dify-api:1.9.2 restart: always environment: # Use the shared environment variables. @@ -633,13 +633,6 @@ services: volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage - # TODO: Remove this entrypoint override when weaviate-client 4.17.0 is included in the next Dify release - entrypoint: - - /bin/bash - - -c - - | - uv pip install --system weaviate-client==4.17.0 - exec /bin/bash /app/api/docker/entrypoint.sh networks: - ssrf_proxy_network - default @@ -647,7 +640,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.9.1 + image: langgenius/dify-api:1.9.2 restart: always environment: # Use the shared environment variables. @@ -667,13 +660,6 @@ services: volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage - # TODO: Remove this entrypoint override when weaviate-client 4.17.0 is included in the next Dify release - entrypoint: - - /bin/bash - - -c - - | - uv pip install --system weaviate-client==4.17.0 - exec /bin/bash /app/api/docker/entrypoint.sh networks: - ssrf_proxy_network - default @@ -681,7 +667,7 @@ services: # worker_beat service # Celery beat for scheduling periodic tasks. worker_beat: - image: langgenius/dify-api:1.9.1 + image: langgenius/dify-api:1.9.2 restart: always environment: # Use the shared environment variables. @@ -699,7 +685,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.9.1 + image: langgenius/dify-web:1.9.2 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -802,7 +788,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.3.0-local + image: langgenius/dify-plugin-daemon:0.3.3-local restart: always environment: # Use the shared environment variables. diff --git a/web/package.json b/web/package.json index b8916cb7ba..6487108bfc 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "1.9.1", + "version": "1.9.2", "private": true, "packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d", "engines": { From 845adb664a7999ee8fbc11bd36c9f576b35ad237 Mon Sep 17 00:00:00 2001 From: Garfield Dai Date: Wed, 22 Oct 2025 10:29:27 +0800 Subject: [PATCH 14/33] knowledge-pipeline-for-enterprise (#27240) --- api/services/feature_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 19d96cb972..148442f76e 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -174,6 +174,7 @@ class FeatureService: if dify_config.ENTERPRISE_ENABLED: features.webapp_copyright_enabled = True + features.knowledge_pipeline.publish_enabled = True cls._fulfill_params_from_workspace_info(features, tenant_id) return features From f9090405674d6a4465ba61269c14f3c133676e5d Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:49:49 +0800 Subject: [PATCH 15/33] feat: Enhance knowledge base node validation by adding checks for embedding and reranking models (#27241) --- .../workflow/hooks/use-checklist.ts | 14 ++++++- .../components/embedding-model.tsx | 1 + .../reranking-model-selector.tsx | 1 + .../workflow/nodes/knowledge-base/default.ts | 40 +++++++++++++++---- .../workflow/nodes/knowledge-base/types.ts | 3 ++ web/i18n/en-US/workflow.ts | 2 + web/i18n/zh-Hans/workflow.ts | 2 + 7 files changed, 54 insertions(+), 9 deletions(-) diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 8a29551b89..1f474a699a 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -42,6 +42,9 @@ import { fetchDatasets } from '@/service/datasets' import { MAX_TREE_DEPTH } from '@/config' import useNodesAvailableVarList, { useGetNodesAvailableVarList } from './use-nodes-available-var-list' import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils' +import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { KnowledgeBaseNodeType } from '../nodes/knowledge-base/types' export const useChecklist = (nodes: Node[], edges: Edge[]) => { const { t } = useTranslation() @@ -57,6 +60,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { const getToolIcon = useGetToolIcon() const map = useNodesAvailableVarList(nodes) + const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding) + const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank) const getCheckData = useCallback((data: CommonNodeType<{}>) => { let checkData = data @@ -72,8 +77,15 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { _datasets, } as CommonNodeType } + else if (data.type === BlockEnum.KnowledgeBase) { + checkData = { + ...data, + _embeddingModelList: embeddingModelList, + _rerankModelList: rerankModelList, + } as CommonNodeType + } return checkData - }, [datasetsDetail]) + }, [datasetsDetail, embeddingModelList, rerankModelList]) const needWarningNodes = useMemo(() => { const list = [] diff --git a/web/app/components/workflow/nodes/knowledge-base/components/embedding-model.tsx b/web/app/components/workflow/nodes/knowledge-base/components/embedding-model.tsx index 23481cb529..7709fb49d7 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/embedding-model.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/embedding-model.tsx @@ -57,6 +57,7 @@ const EmbeddingModel = ({ modelList={embeddingModelList} onSelect={handleEmbeddingModelChange} readonly={readonly} + showDeprecatedWarnIcon /> ) diff --git a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/reranking-model-selector.tsx b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/reranking-model-selector.tsx index e1eccaf309..19566362a1 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/reranking-model-selector.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/reranking-model-selector.tsx @@ -44,6 +44,7 @@ const RerankingModelSelector = ({ modelList={rerankModelList} onSelect={handleRerankingModelChange} readonly={readonly} + showDeprecatedWarnIcon /> ) } diff --git a/web/app/components/workflow/nodes/knowledge-base/default.ts b/web/app/components/workflow/nodes/knowledge-base/default.ts index 190addde4d..952eb10fa0 100644 --- a/web/app/components/workflow/nodes/knowledge-base/default.ts +++ b/web/app/components/workflow/nodes/knowledge-base/default.ts @@ -31,6 +31,8 @@ const nodeDefault: NodeDefault = { embedding_model, embedding_model_provider, index_chunk_variable_selector, + _embeddingModelList, + _rerankModelList, } = payload const { @@ -39,6 +41,12 @@ const nodeDefault: NodeDefault = { reranking_model, } = retrieval_model || {} + const currentEmbeddingModelProvider = _embeddingModelList?.find(provider => provider.provider === embedding_model_provider) + const currentEmbeddingModel = currentEmbeddingModelProvider?.models.find(model => model.model === embedding_model) + + const currentRerankingModelProvider = _rerankModelList?.find(provider => provider.provider === reranking_model?.reranking_provider_name) + const currentRerankingModel = currentRerankingModelProvider?.models.find(model => model.model === reranking_model?.reranking_model_name) + if (!chunk_structure) { return { isValid: false, @@ -60,10 +68,18 @@ const nodeDefault: NodeDefault = { } } - if (indexing_technique === IndexingType.QUALIFIED && (!embedding_model || !embedding_model_provider)) { - return { - isValid: false, - errorMessage: t('workflow.nodes.knowledgeBase.embeddingModelIsRequired'), + if (indexing_technique === IndexingType.QUALIFIED) { + if (!embedding_model || !embedding_model_provider) { + return { + isValid: false, + errorMessage: t('workflow.nodes.knowledgeBase.embeddingModelIsRequired'), + } + } + else if (!currentEmbeddingModel) { + return { + isValid: false, + errorMessage: t('workflow.nodes.knowledgeBase.embeddingModelIsInvalid'), + } } } @@ -74,10 +90,18 @@ const nodeDefault: NodeDefault = { } } - if (reranking_enable && (!reranking_model || !reranking_model.reranking_provider_name || !reranking_model.reranking_model_name)) { - return { - isValid: false, - errorMessage: t('workflow.nodes.knowledgeBase.rerankingModelIsRequired'), + if (reranking_enable) { + if (!reranking_model || !reranking_model.reranking_provider_name || !reranking_model.reranking_model_name) { + return { + isValid: false, + errorMessage: t('workflow.nodes.knowledgeBase.rerankingModelIsRequired'), + } + } + else if (!currentRerankingModel) { + return { + isValid: false, + errorMessage: t('workflow.nodes.knowledgeBase.rerankingModelIsInvalid'), + } } } diff --git a/web/app/components/workflow/nodes/knowledge-base/types.ts b/web/app/components/workflow/nodes/knowledge-base/types.ts index a8a0811c54..1f484a5c55 100644 --- a/web/app/components/workflow/nodes/knowledge-base/types.ts +++ b/web/app/components/workflow/nodes/knowledge-base/types.ts @@ -3,6 +3,7 @@ import type { IndexingType } from '@/app/components/datasets/create/step-two' import type { RETRIEVE_METHOD } from '@/types/app' import type { WeightedScoreEnum } from '@/models/datasets' import type { RerankingModeEnum } from '@/models/datasets' +import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations' export { WeightedScoreEnum } from '@/models/datasets' export { IndexingType as IndexMethodEnum } from '@/app/components/datasets/create/step-two' export { RETRIEVE_METHOD as RetrievalSearchMethodEnum } from '@/types/app' @@ -49,4 +50,6 @@ export type KnowledgeBaseNodeType = CommonNodeType & { embedding_model_provider?: string keyword_number: number retrieval_model: RetrievalSetting + _embeddingModelList?: Model[] + _rerankModelList?: Model[] } diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 01d17a4111..e07fc3f109 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -959,8 +959,10 @@ const translation = { indexMethodIsRequired: 'Index method is required', chunksVariableIsRequired: 'Chunks variable is required', embeddingModelIsRequired: 'Embedding model is required', + embeddingModelIsInvalid: 'Embedding model is invalid', retrievalSettingIsRequired: 'Retrieval setting is required', rerankingModelIsRequired: 'Reranking model is required', + rerankingModelIsInvalid: 'Reranking model is invalid', }, }, tracing: { diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 441c0a707e..c5c72eb712 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -959,8 +959,10 @@ const translation = { indexMethodIsRequired: '索引方法是必需的', chunksVariableIsRequired: 'Chunks 变量是必需的', embeddingModelIsRequired: 'Embedding 模型是必需的', + embeddingModelIsInvalid: '无效的 Embedding 模型', retrievalSettingIsRequired: '检索设置是必需的', rerankingModelIsRequired: 'Reranking 模型是必需的', + rerankingModelIsInvalid: '无效的 Reranking 模型', }, }, tracing: { From 9a8cf709baeb19d3197b51f01ac4a2f009728169 Mon Sep 17 00:00:00 2001 From: Nite Knite Date: Wed, 22 Oct 2025 11:05:27 +0800 Subject: [PATCH 16/33] chore: adjust the route scope for loading Zendesk scripts (#27244) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- web/app/(commonLayout)/layout.tsx | 2 ++ web/app/layout.tsx | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index ed1c995e25..be9c4fe49a 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -9,6 +9,7 @@ import { EventEmitterContextProvider } from '@/context/event-emitter' import { ProviderContextProvider } from '@/context/provider-context' import { ModalContextProvider } from '@/context/modal-context' import GotoAnything from '@/app/components/goto-anything' +import Zendesk from '@/app/components/base/zendesk' const Layout = ({ children }: { children: ReactNode }) => { return ( @@ -28,6 +29,7 @@ const Layout = ({ children }: { children: ReactNode }) => { + ) diff --git a/web/app/layout.tsx b/web/app/layout.tsx index fde265cd22..1be802460b 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -3,7 +3,6 @@ import type { Viewport } from 'next' import I18nServer from './components/i18n-server' import BrowserInitializer from './components/browser-initializer' import SentryInitializer from './components/sentry-initializer' -import Zendesk from './components/base/zendesk' import { getLocaleOnServer } from '@/i18n-config/server' import { TanstackQueryInitializer } from '@/context/query-client' import { ThemeProvider } from 'next-themes' @@ -105,7 +104,6 @@ const LocaleLayout = async ({ - ) From e1ca7a9bdb0443b9f027b6de16c019fff434384c Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 22 Oct 2025 11:20:31 +0800 Subject: [PATCH 17/33] chore: hide useless error info in login page (#27245) --- web/service/use-common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/service/use-common.ts b/web/service/use-common.ts index 57b9c8b165..51b35c453b 100644 --- a/web/service/use-common.ts +++ b/web/service/use-common.ts @@ -118,7 +118,7 @@ export const useIsLogin = () => { gcTime: 0, queryFn: async (): Promise => { try { - await get('/account/profile', { + await get('/account/profile', {}, { silent: true, }) } From 523da66134e67345f818009827c37d268905ee88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:41:49 +0800 Subject: [PATCH 18/33] chore: translate i18n files and update type definitions (#27243) Co-authored-by: WTW0313 <30284043+WTW0313@users.noreply.github.com> --- web/i18n/de-DE/workflow.ts | 2 ++ web/i18n/es-ES/workflow.ts | 2 ++ web/i18n/fa-IR/workflow.ts | 2 ++ web/i18n/fr-FR/workflow.ts | 2 ++ web/i18n/hi-IN/workflow.ts | 2 ++ web/i18n/id-ID/workflow.ts | 2 ++ web/i18n/it-IT/workflow.ts | 2 ++ web/i18n/ja-JP/workflow.ts | 2 ++ web/i18n/ko-KR/workflow.ts | 2 ++ web/i18n/pl-PL/workflow.ts | 2 ++ web/i18n/pt-BR/workflow.ts | 2 ++ web/i18n/ro-RO/workflow.ts | 2 ++ web/i18n/ru-RU/workflow.ts | 2 ++ web/i18n/sl-SI/workflow.ts | 2 ++ web/i18n/th-TH/workflow.ts | 2 ++ web/i18n/tr-TR/workflow.ts | 2 ++ web/i18n/uk-UA/workflow.ts | 2 ++ web/i18n/vi-VN/workflow.ts | 2 ++ web/i18n/zh-Hant/workflow.ts | 2 ++ 19 files changed, 38 insertions(+) diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index 55bf4ddf72..4353e5e10c 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -949,6 +949,8 @@ const translation = { embeddingModelIsRequired: 'Ein Einbettungsmodell ist erforderlich', chunksVariableIsRequired: 'Die Variable \'Chunks\' ist erforderlich', rerankingModelIsRequired: 'Ein Reranking-Modell ist erforderlich', + embeddingModelIsInvalid: 'Einbettungsmodell ist ungültig', + rerankingModelIsInvalid: 'Das Reranking-Modell ist ungültig', }, }, tracing: { diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index b4250284aa..d7a6bef9e7 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -949,6 +949,8 @@ const translation = { embeddingModelIsRequired: 'Se requiere un modelo de incrustación', rerankingModelIsRequired: 'Se requiere un modelo de reordenamiento', chunksVariableIsRequired: 'La variable Chunks es obligatoria', + rerankingModelIsInvalid: 'El modelo de reordenación no es válido', + embeddingModelIsInvalid: 'El modelo de incrustación no es válido', }, }, tracing: { diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts index 94d966f2a9..aba3a25010 100644 --- a/web/i18n/fa-IR/workflow.ts +++ b/web/i18n/fa-IR/workflow.ts @@ -949,6 +949,8 @@ const translation = { embeddingModelIsRequired: 'مدل جاسازی مورد نیاز است', chunksVariableIsRequired: 'متغیر تکه‌ها الزامی است', rerankingModelIsRequired: 'مدل رتبه‌بندی مجدد مورد نیاز است', + embeddingModelIsInvalid: 'مدل جاسازی نامعتبر است', + rerankingModelIsInvalid: 'مدل رتبه‌بندی مجدد نامعتبر است', }, }, tracing: { diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index e7389b4b23..f6c1899cac 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -949,6 +949,8 @@ const translation = { rerankingModelIsRequired: 'Un modèle de rerankage est requis', embeddingModelIsRequired: 'Un modèle d\'intégration est requis', chunksVariableIsRequired: 'La variable Chunks est requise', + rerankingModelIsInvalid: 'Le modèle de rerank est invalide', + embeddingModelIsInvalid: 'Le modèle d\'intégration est invalide', }, }, tracing: { diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index bf4c6cd8d3..224f3acaeb 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -969,6 +969,8 @@ const translation = { chunksVariableIsRequired: 'टुकड़े चर आवश्यक है', embeddingModelIsRequired: 'एम्बेडिंग मॉडल आवश्यक है', rerankingModelIsRequired: 'पुनः क्रमांकन मॉडल की आवश्यकता है', + rerankingModelIsInvalid: 'पुनः क्रमांकन मॉडल अमान्य है', + embeddingModelIsInvalid: 'एम्बेडिंग मॉडल अमान्य है', }, }, tracing: { diff --git a/web/i18n/id-ID/workflow.ts b/web/i18n/id-ID/workflow.ts index 2ec7ace93f..4ef6b2b832 100644 --- a/web/i18n/id-ID/workflow.ts +++ b/web/i18n/id-ID/workflow.ts @@ -924,6 +924,8 @@ const translation = { chunksVariableIsRequired: 'Variabel Chunks diperlukan', rerankingModelIsRequired: 'Model reranking diperlukan', embeddingModelIsRequired: 'Model embedding diperlukan', + rerankingModelIsInvalid: 'Model reranking tidak valid', + embeddingModelIsInvalid: 'Model embedding tidak valid', }, }, tracing: {}, diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index a874c7a9b6..314b8e0c52 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -975,6 +975,8 @@ const translation = { chunksVariableIsRequired: 'La variabile Chunks è richiesta', rerankingModelIsRequired: 'È richiesto un modello di riordinamento', embeddingModelIsRequired: 'È necessario un modello di embedding', + embeddingModelIsInvalid: 'Il modello di embedding non è valido', + rerankingModelIsInvalid: 'Il modello di riorganizzazione è non valido', }, }, tracing: { diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index b3b61f974f..3320f5a89f 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -961,6 +961,8 @@ const translation = { chunksVariableIsRequired: 'Chunks変数は必須です', embeddingModelIsRequired: '埋め込みモデルが必要です', rerankingModelIsRequired: '再ランキングモデルが必要です', + embeddingModelIsInvalid: '埋め込みモデルが無効です', + rerankingModelIsInvalid: 'リランキングモデルは無効です', }, }, tracing: { diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index 2199054f0e..427452943a 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -997,6 +997,8 @@ const translation = { chunksVariableIsRequired: 'Chunks 변수는 필수입니다', embeddingModelIsRequired: '임베딩 모델이 필요합니다', rerankingModelIsRequired: '재순위 모델이 필요합니다', + rerankingModelIsInvalid: '재정렬 모델이 유효하지 않습니다', + embeddingModelIsInvalid: '임베딩 모델이 유효하지 않습니다', }, }, tracing: { diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index 7eac765449..7c4d85e3ec 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -949,6 +949,8 @@ const translation = { embeddingModelIsRequired: 'Wymagany jest model osadzania', chunksVariableIsRequired: 'Wymagana jest zmienna Chunks', rerankingModelIsRequired: 'Wymagany jest model ponownego rankingu', + embeddingModelIsInvalid: 'Model osadzania jest nieprawidłowy', + rerankingModelIsInvalid: 'Model ponownego rankingowania jest nieprawidłowy', }, }, tracing: { diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index 9dc7439286..bd5cf49ed7 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -949,6 +949,8 @@ const translation = { chunksVariableIsRequired: 'A variável \'chunks\' é obrigatória', embeddingModelIsRequired: 'Modelo de incorporação é necessário', rerankingModelIsRequired: 'Um modelo de reclassificação é necessário', + embeddingModelIsInvalid: 'O modelo de incorporação é inválido', + rerankingModelIsInvalid: 'O modelo de reclassificação é inválido', }, }, tracing: { diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index b91cecac30..ffa1282380 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -949,6 +949,8 @@ const translation = { chunksVariableIsRequired: 'Variabila Chunks este obligatorie', embeddingModelIsRequired: 'Este necesar un model de încorporare', rerankingModelIsRequired: 'Este necesar un model de reordonare', + rerankingModelIsInvalid: 'Modelul de reordonare este invalid', + embeddingModelIsInvalid: 'Modelul de încorporare este invalid', }, }, tracing: { diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index f3be73ed99..78be03ba91 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -949,6 +949,8 @@ const translation = { chunksVariableIsRequired: 'Переменная chunks обязательна', embeddingModelIsRequired: 'Требуется модель встраивания', rerankingModelIsRequired: 'Требуется модель перераспределения рангов', + rerankingModelIsInvalid: 'Модель повторной ранжировки недействительна', + embeddingModelIsInvalid: 'Модель встраивания недействительна', }, }, tracing: { diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts index 0aa445e820..dbc4a75c43 100644 --- a/web/i18n/sl-SI/workflow.ts +++ b/web/i18n/sl-SI/workflow.ts @@ -956,6 +956,8 @@ const translation = { chunksVariableIsRequired: 'Spremenljivka Chunks je obvezna', embeddingModelIsRequired: 'Zahteva se vgrajevalni model', rerankingModelIsRequired: 'Potreben je model za ponovno razvrščanje', + rerankingModelIsInvalid: 'Model prerazvrščanja ni veljaven', + embeddingModelIsInvalid: 'Vdelovalni model ni veljaven', }, }, tracing: { diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts index 3491814b73..419b577a02 100644 --- a/web/i18n/th-TH/workflow.ts +++ b/web/i18n/th-TH/workflow.ts @@ -949,6 +949,8 @@ const translation = { chunksVariableIsRequired: 'ตัวแปร Chunks เป็นสิ่งจำเป็น', embeddingModelIsRequired: 'จำเป็นต้องใช้โมเดลฝัง', rerankingModelIsRequired: 'จำเป็นต้องมีโมเดลการจัดอันดับใหม่', + embeddingModelIsInvalid: 'แบบจำลองการฝังไม่ถูกต้อง', + rerankingModelIsInvalid: 'โมเดลการจัดอันดับใหม่ไม่ถูกต้อง', }, }, tracing: { diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts index bd853917b2..930664ce57 100644 --- a/web/i18n/tr-TR/workflow.ts +++ b/web/i18n/tr-TR/workflow.ts @@ -950,6 +950,8 @@ const translation = { embeddingModelIsRequired: 'Gömme modeli gereklidir', chunksVariableIsRequired: 'Chunks değişkeni gereklidir', rerankingModelIsRequired: 'Yeniden sıralama modeli gereklidir', + rerankingModelIsInvalid: 'Yeniden sıralama modeli geçersiz', + embeddingModelIsInvalid: 'Gömme modeli geçersiz', }, }, tracing: { diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index 069cbcf74c..2f4f298204 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -949,6 +949,8 @@ const translation = { chunksVariableIsRequired: 'Змінна chunks є обов\'язковою', embeddingModelIsRequired: 'Потрібна модель вбудовування', rerankingModelIsRequired: 'Потрібна модель повторного ранжування', + embeddingModelIsInvalid: 'Модель вбудовування недійсна', + rerankingModelIsInvalid: 'Модель переналаштування недійсна', }, }, tracing: { diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index f7b62ee78d..4a3a720cb3 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -949,6 +949,8 @@ const translation = { chunksVariableIsRequired: 'Biến Chunks là bắt buộc', embeddingModelIsRequired: 'Cần có mô hình nhúng', rerankingModelIsRequired: 'Cần có mô hình sắp xếp lại', + embeddingModelIsInvalid: 'Mô hình nhúng không hợp lệ', + rerankingModelIsInvalid: 'Mô hình xếp hạng lại không hợp lệ', }, }, tracing: { diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 83464e5a7c..faa80b0fa4 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -949,6 +949,8 @@ const translation = { rerankingModelIsRequired: '需要重新排序模型', chunksVariableIsRequired: 'Chunks 變數是必需的', embeddingModelIsRequired: '需要嵌入模型', + rerankingModelIsInvalid: '重排序模型無效', + embeddingModelIsInvalid: '嵌入模型無效', }, }, tracing: { From bebb4ffbaadf3462ac78a1a3598e8498e8e5f7b5 Mon Sep 17 00:00:00 2001 From: GuanMu Date: Wed, 22 Oct 2025 11:43:37 +0800 Subject: [PATCH 19/33] Fix type error (#27217) --- .../chat/embedded-chatbot/chat-wrapper.tsx | 2 +- web/app/components/base/mermaid/index.tsx | 4 +-- web/app/components/billing/type.ts | 2 +- web/context/provider-context.tsx | 3 +- web/types/lamejs.d.ts | 36 +++++++++++++++++++ web/types/react-18-input-autosize.d.ts | 23 ++++++++++++ 6 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 web/types/lamejs.d.ts create mode 100644 web/types/react-18-input-autosize.d.ts diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx index 7502b5b767..1bb3dbf56f 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -237,7 +237,7 @@ const ChatWrapper = () => { return ( { const containerRef = useRef(null) const chartId = useRef(`mermaid-chart-${Math.random().toString(36).slice(2, 11)}`).current const [isLoading, setIsLoading] = useState(true) - const renderTimeoutRef = useRef() + const renderTimeoutRef = useRef(undefined) const [errMsg, setErrMsg] = useState('') const [imagePreviewUrl, setImagePreviewUrl] = useState('') @@ -187,7 +187,7 @@ const Flowchart = (props: FlowchartProps) => { }, []) // Update theme when prop changes, but allow internal override. - const prevThemeRef = useRef() + const prevThemeRef = useRef(undefined) useEffect(() => { // Only react if the theme prop from the outside has actually changed. if (props.theme && props.theme !== prevThemeRef.current) { diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts index 72d46f1e70..7cc4d19755 100644 --- a/web/app/components/billing/type.ts +++ b/web/app/components/billing/type.ts @@ -10,7 +10,7 @@ export enum Priority { topPriority = 'top-priority', } -export type BasicPlan = Plan.sandbox | Plan.professional | Plan.team | Plan.enterprise +export type BasicPlan = Plan.sandbox | Plan.professional | Plan.team export type PlanInfo = { level: number diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 6c558948d2..755131c859 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -17,7 +17,6 @@ import { } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { RETRIEVE_METHOD } from '@/types/app' -import type { BasicPlan } from '@/app/components/billing/type' import { Plan, type UsagePlanInfo } from '@/app/components/billing/type' import { fetchCurrentPlanInfo } from '@/service/billing' import { parseCurrentPlan } from '@/app/components/billing/utils' @@ -37,7 +36,7 @@ type ProviderContextState = { supportRetrievalMethods: RETRIEVE_METHOD[] isAPIKeySet: boolean plan: { - type: BasicPlan + type: Plan usage: UsagePlanInfo total: UsagePlanInfo } diff --git a/web/types/lamejs.d.ts b/web/types/lamejs.d.ts new file mode 100644 index 0000000000..7ce3e6f89e --- /dev/null +++ b/web/types/lamejs.d.ts @@ -0,0 +1,36 @@ +declare module 'lamejs' { + export class Mp3Encoder { + constructor(channels: number, sampleRate: number, bitRate: number) + encodeBuffer(left: Int16Array, right?: Int16Array | null): Int8Array + flush(): Int8Array + } + + export class WavHeader { + static readHeader(data: DataView): { + channels: number + sampleRate: number + } + } + + const lamejs: { + Mp3Encoder: typeof Mp3Encoder + WavHeader: typeof WavHeader + } + + export default lamejs +} + +declare module 'lamejs/src/js/MPEGMode' { + const MPEGMode: any + export default MPEGMode +} + +declare module 'lamejs/src/js/Lame' { + const Lame: any + export default Lame +} + +declare module 'lamejs/src/js/BitStream' { + const BitStream: any + export default BitStream +} diff --git a/web/types/react-18-input-autosize.d.ts b/web/types/react-18-input-autosize.d.ts new file mode 100644 index 0000000000..0864b33e6a --- /dev/null +++ b/web/types/react-18-input-autosize.d.ts @@ -0,0 +1,23 @@ +declare module 'react-18-input-autosize' { + import type { CSSProperties, ChangeEvent, FocusEvent, KeyboardEvent } from 'react' + + export type AutosizeInputProps = { + value?: string | number + defaultValue?: string | number + onChange?: (event: ChangeEvent) => void + onFocus?: (event: FocusEvent) => void + onBlur?: (event: FocusEvent) => void + onKeyDown?: (event: KeyboardEvent) => void + placeholder?: string + className?: string + inputClassName?: string + style?: CSSProperties + inputStyle?: CSSProperties + minWidth?: number | string + maxWidth?: number | string + [key: string]: any + } + + const AutosizeInput: React.FC + export default AutosizeInput +} From 26ff59172eb2605a206997b7eb8b15f8b31c31a7 Mon Sep 17 00:00:00 2001 From: Alain Date: Wed, 22 Oct 2025 11:45:31 +0800 Subject: [PATCH 20/33] fix: fix OpenAPI Schema Import Pydantic Validation Errors for Complex Default Values (#27159) Co-authored-by: Alain Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/plugin/entities/parameters.py | 2 +- api/core/tools/utils/parser.py | 39 +++++++-- .../core/tools/utils/test_parser.py | 80 +++++++++++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/api/core/plugin/entities/parameters.py b/api/core/plugin/entities/parameters.py index 68b5c1084a..1e7f8e4c86 100644 --- a/api/core/plugin/entities/parameters.py +++ b/api/core/plugin/entities/parameters.py @@ -76,7 +76,7 @@ class PluginParameter(BaseModel): auto_generate: PluginParameterAutoGenerate | None = None template: PluginParameterTemplate | None = None required: bool = False - default: Union[float, int, str] | None = None + default: Union[float, int, str, bool] | None = None min: Union[float, int] | None = None max: Union[float, int] | None = None precision: int | None = None diff --git a/api/core/tools/utils/parser.py b/api/core/tools/utils/parser.py index c7ac3387e5..6eabde3991 100644 --- a/api/core/tools/utils/parser.py +++ b/api/core/tools/utils/parser.py @@ -62,6 +62,11 @@ class ApiBasedToolSchemaParser: root = root[ref] interface["operation"]["parameters"][i] = root for parameter in interface["operation"]["parameters"]: + # Handle complex type defaults that are not supported by PluginParameter + default_value = None + if "schema" in parameter and "default" in parameter["schema"]: + default_value = ApiBasedToolSchemaParser._sanitize_default_value(parameter["schema"]["default"]) + tool_parameter = ToolParameter( name=parameter["name"], label=I18nObject(en_US=parameter["name"], zh_Hans=parameter["name"]), @@ -72,9 +77,7 @@ class ApiBasedToolSchemaParser: required=parameter.get("required", False), form=ToolParameter.ToolParameterForm.LLM, llm_description=parameter.get("description"), - default=parameter["schema"]["default"] - if "schema" in parameter and "default" in parameter["schema"] - else None, + default=default_value, placeholder=I18nObject( en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "") ), @@ -134,6 +137,11 @@ class ApiBasedToolSchemaParser: required = body_schema.get("required", []) properties = body_schema.get("properties", {}) for name, property in properties.items(): + # Handle complex type defaults that are not supported by PluginParameter + default_value = ApiBasedToolSchemaParser._sanitize_default_value( + property.get("default", None) + ) + tool = ToolParameter( name=name, label=I18nObject(en_US=name, zh_Hans=name), @@ -144,12 +152,11 @@ class ApiBasedToolSchemaParser: required=name in required, form=ToolParameter.ToolParameterForm.LLM, llm_description=property.get("description", ""), - default=property.get("default", None), + default=default_value, placeholder=I18nObject( en_US=property.get("description", ""), zh_Hans=property.get("description", "") ), ) - # check if there is a type typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property) if typ: @@ -197,6 +204,22 @@ class ApiBasedToolSchemaParser: return bundles + @staticmethod + def _sanitize_default_value(value): + """ + Sanitize default values for PluginParameter compatibility. + Complex types (list, dict) are converted to None to avoid validation errors. + + Args: + value: The default value from OpenAPI schema + + Returns: + None for complex types (list, dict), otherwise the original value + """ + if isinstance(value, (list, dict)): + return None + return value + @staticmethod def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType | None: parameter = parameter or {} @@ -217,7 +240,11 @@ class ApiBasedToolSchemaParser: return ToolParameter.ToolParameterType.STRING elif typ == "array": items = parameter.get("items") or parameter.get("schema", {}).get("items") - return ToolParameter.ToolParameterType.FILES if items and items.get("format") == "binary" else None + if items and items.get("format") == "binary": + return ToolParameter.ToolParameterType.FILES + else: + # For regular arrays, return ARRAY type instead of None + return ToolParameter.ToolParameterType.ARRAY else: return None diff --git a/api/tests/unit_tests/core/tools/utils/test_parser.py b/api/tests/unit_tests/core/tools/utils/test_parser.py index e1eab21ca4..f39158aa59 100644 --- a/api/tests/unit_tests/core/tools/utils/test_parser.py +++ b/api/tests/unit_tests/core/tools/utils/test_parser.py @@ -109,3 +109,83 @@ def test_parse_openapi_to_tool_bundle_properties_all_of(app): assert tool_bundles[0].parameters[0].llm_description == "desc prop1" # TODO: support enum in OpenAPI # assert set(tool_bundles[0].parameters[0].options) == {"option1", "option2", "option3"} + + +def test_parse_openapi_to_tool_bundle_default_value_type_casting(app): + """ + Test that default values are properly cast to match parameter types. + This addresses the issue where array default values like [] cause validation errors + when parameter type is inferred as string/number/boolean. + """ + openapi = { + "openapi": "3.0.0", + "info": {"title": "Test API", "version": "1.0.0"}, + "servers": [{"url": "https://example.com"}], + "paths": { + "/product/create": { + "post": { + "operationId": "createProduct", + "summary": "Create a product", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "categories": { + "description": "List of category identifiers", + "default": [], + "type": "array", + "items": {"type": "string"}, + }, + "name": { + "description": "Product name", + "default": "Default Product", + "type": "string", + }, + "price": {"description": "Product price", "default": 0.0, "type": "number"}, + "available": { + "description": "Product availability", + "default": True, + "type": "boolean", + }, + }, + } + } + } + }, + "responses": {"200": {"description": "Default Response"}}, + } + } + }, + } + + with app.test_request_context(): + tool_bundles = ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi) + + assert len(tool_bundles) == 1 + bundle = tool_bundles[0] + assert len(bundle.parameters) == 4 + + # Find parameters by name + params_by_name = {param.name: param for param in bundle.parameters} + + # Check categories parameter (array type with [] default) + categories_param = params_by_name["categories"] + assert categories_param.type == "array" # Will be detected by _get_tool_parameter_type + assert categories_param.default is None # Array default [] is converted to None + + # Check name parameter (string type with string default) + name_param = params_by_name["name"] + assert name_param.type == "string" + assert name_param.default == "Default Product" + + # Check price parameter (number type with number default) + price_param = params_by_name["price"] + assert price_param.type == "number" + assert price_param.default == 0.0 + + # Check available parameter (boolean type with boolean default) + available_param = params_by_name["available"] + assert available_param.type == "boolean" + assert available_param.default is True From 73e217ab0d79dde2243863aa073a5da65e894648 Mon Sep 17 00:00:00 2001 From: GuanMu Date: Wed, 22 Oct 2025 13:06:15 +0800 Subject: [PATCH 21/33] Fix type error (#27250) --- web/app/components/base/pagination/type.ts | 12 +++++++++--- web/app/components/tools/labels/constant.ts | 5 ++++- .../workflow/store/workflow/debug/mock-data.ts | 18 ++++++++++++++++++ web/app/components/workflow/utils/debug.ts | 3 +++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/web/app/components/base/pagination/type.ts b/web/app/components/base/pagination/type.ts index c3744c06c2..d8b7cf1614 100644 --- a/web/app/components/base/pagination/type.ts +++ b/web/app/components/base/pagination/type.ts @@ -1,5 +1,11 @@ import type { ButtonHTMLAttributes } from 'react' +type ElementProps = { + className?: string + children?: React.ReactNode + [key: string]: unknown +} + type IBasePaginationProps = { currentPage: number setCurrentPage: (page: number) => void @@ -31,7 +37,7 @@ type IPagination = IUsePagination & { } type ButtonProps = ButtonHTMLAttributes & { - as?: React.ReactNode + as?: React.ReactElement children?: string | React.ReactNode className?: string dataTestId?: string @@ -39,9 +45,9 @@ type ButtonProps = ButtonHTMLAttributes & { type PageButtonProps = ButtonProps & { /** - * Provide a custom ReactNode (e.g. Next/Link) + * Provide a custom ReactElement (e.g. Next/Link) */ - as?: React.ReactNode + as?: React.ReactElement activeClassName?: string inactiveClassName?: string dataTestIdActive?: string diff --git a/web/app/components/tools/labels/constant.ts b/web/app/components/tools/labels/constant.ts index ad4836e6a8..e7d3503e73 100644 --- a/web/app/components/tools/labels/constant.ts +++ b/web/app/components/tools/labels/constant.ts @@ -1,4 +1,7 @@ +import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations' + export type Label = { name: string - label: string + label: TypeWithI18N + icon: string } diff --git a/web/app/components/workflow/store/workflow/debug/mock-data.ts b/web/app/components/workflow/store/workflow/debug/mock-data.ts index 9d1bf80076..0bc5555d8c 100644 --- a/web/app/components/workflow/store/workflow/debug/mock-data.ts +++ b/web/app/components/workflow/store/workflow/debug/mock-data.ts @@ -12,6 +12,9 @@ export const vars: VarInInspect[] = [ value_type: VarType.string, value: 'text value...', edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, }, { id: 'fdklajljgldjglkagjlk', @@ -22,6 +25,9 @@ export const vars: VarInInspect[] = [ value_type: VarType.string, value: 'made zhizhang', edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, }, ] @@ -35,6 +41,9 @@ export const conversationVars: VarInInspect[] = [ value_type: VarType.string, value: 'conversation var value...', edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, }, { id: 'con2', @@ -45,6 +54,9 @@ export const conversationVars: VarInInspect[] = [ value_type: VarType.number, value: 456, edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, }, ] @@ -58,6 +70,9 @@ export const systemVars: VarInInspect[] = [ value_type: VarType.string, value: 'Hello robot!', edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, }, { id: 'sys2', @@ -68,5 +83,8 @@ export const systemVars: VarInInspect[] = [ value_type: VarType.string, value: 'djflakjerlkjdlksfjslakjsdfl', edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, }, ] diff --git a/web/app/components/workflow/utils/debug.ts b/web/app/components/workflow/utils/debug.ts index 6dd111d714..4f47153111 100644 --- a/web/app/components/workflow/utils/debug.ts +++ b/web/app/components/workflow/utils/debug.ts @@ -21,5 +21,8 @@ export const outputToVarInInspect = ({ value_type: VarType.string, // TODO: wait for api or get from node value, edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, } } From 8e45753c6854b0c9822b2b7fddbfc87d3bce904e Mon Sep 17 00:00:00 2001 From: Cris <92035086+cuixiaojun001@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:36:29 +0800 Subject: [PATCH 22/33] fix:restore correct numeric values for ParamsAutoGenerated (#27252) --- api/core/workflow/nodes/agent/entities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core/workflow/nodes/agent/entities.py b/api/core/workflow/nodes/agent/entities.py index ce6eb33ecc..985ee5eef2 100644 --- a/api/core/workflow/nodes/agent/entities.py +++ b/api/core/workflow/nodes/agent/entities.py @@ -26,8 +26,8 @@ class AgentNodeData(BaseNodeData): class ParamsAutoGenerated(IntEnum): - CLOSE = auto() - OPEN = auto() + CLOSE = 0 + OPEN = 1 class AgentOldVersionModelFeatures(StrEnum): From 40d3332690ab3a10e937f8ef8472ba09f8276464 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 22 Oct 2025 14:59:08 +0800 Subject: [PATCH 23/33] fix: preserve share code headers after login redirect (#27225) Co-authored-by: yunlu.wen Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/web/passport.py | 33 ++++++++++++-------------------- web/service/fetch.ts | 34 ++++++++++++++++++++++++++++----- web/service/share.ts | 4 +++- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/api/controllers/web/passport.py b/api/controllers/web/passport.py index 776b743e92..a344777783 100644 --- a/api/controllers/web/passport.py +++ b/api/controllers/web/passport.py @@ -14,8 +14,6 @@ from extensions.ext_database import db from libs.passport import PassportService from libs.token import extract_access_token from models.model import App, EndUser, Site -from services.app_service import AppService -from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import FeatureService from services.webapp_auth_service import WebAppAuthService, WebAppAuthType @@ -38,22 +36,17 @@ class PassportResource(Resource): app_code = request.headers.get(HEADER_NAME_APP_CODE) user_id = request.args.get("user_id") access_token = extract_access_token(request) - if app_code is None: raise Unauthorized("X-App-Code header is missing.") - app_id = AppService.get_app_id_by_code(app_code) - # exchange token for enterprise logined web user - enterprise_user_decoded = decode_enterprise_webapp_user_id(access_token) - if enterprise_user_decoded: - # a web user has already logged in, exchange a token for this app without redirecting to the login page - return exchange_token_for_existing_web_user( - app_code=app_code, enterprise_user_decoded=enterprise_user_decoded - ) - if system_features.webapp_auth.enabled: - app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=app_id) - if not app_settings or not app_settings.access_mode == "public": - raise WebAppAuthRequiredError() + enterprise_user_decoded = decode_enterprise_webapp_user_id(access_token) + app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code) + if app_auth_type != WebAppAuthType.PUBLIC: + if not enterprise_user_decoded: + raise WebAppAuthRequiredError() + return exchange_token_for_existing_web_user( + app_code=app_code, enterprise_user_decoded=enterprise_user_decoded, auth_type=app_auth_type + ) # get site from db and check if it is normal site = db.session.scalar(select(Site).where(Site.code == app_code, Site.status == "normal")) @@ -124,7 +117,7 @@ def decode_enterprise_webapp_user_id(jwt_token: str | None): return decoded -def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: dict): +def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: dict, auth_type: WebAppAuthType): """ Exchange a token for an existing web user session. """ @@ -145,13 +138,11 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: if not app_model or app_model.status != "normal" or not app_model.enable_site: raise NotFound() - app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code) - - if app_auth_type == WebAppAuthType.PUBLIC: + if auth_type == WebAppAuthType.PUBLIC: return _exchange_for_public_app_token(app_model, site, enterprise_user_decoded) - elif app_auth_type == WebAppAuthType.EXTERNAL and user_auth_type != "external": + elif auth_type == WebAppAuthType.EXTERNAL and user_auth_type != "external": raise WebAppAuthRequiredError("Please login as external user.") - elif app_auth_type == WebAppAuthType.INTERNAL and user_auth_type != "internal": + elif auth_type == WebAppAuthType.INTERNAL and user_auth_type != "internal": raise WebAppAuthRequiredError("Please login as internal user.") end_user = None diff --git a/web/service/fetch.ts b/web/service/fetch.ts index 541b1246d4..8d663c902b 100644 --- a/web/service/fetch.ts +++ b/web/service/fetch.ts @@ -69,12 +69,36 @@ const beforeErrorToast = (otherOptions: IOtherOptions): BeforeErrorHook => { } } +const SHARE_ROUTE_DENY_LIST = new Set(['webapp-signin', 'check-code', 'login']) + +const resolveShareCode = () => { + const pathnameSegments = globalThis.location.pathname.split('/').filter(Boolean) + const lastSegment = pathnameSegments.at(-1) || '' + if (lastSegment && !SHARE_ROUTE_DENY_LIST.has(lastSegment)) + return lastSegment + + const redirectParam = new URLSearchParams(globalThis.location.search).get('redirect_url') + if (!redirectParam) + return '' + try { + const redirectUrl = new URL(decodeURIComponent(redirectParam), globalThis.location.origin) + const redirectSegments = redirectUrl.pathname.split('/').filter(Boolean) + const redirectSegment = redirectSegments.at(-1) || '' + return SHARE_ROUTE_DENY_LIST.has(redirectSegment) ? '' : redirectSegment + } + catch { + return '' + } +} + const beforeRequestPublicWithCode = (request: Request) => { - request.headers.set('Authorization', `Bearer ${getWebAppAccessToken()}`) - const shareCode = globalThis.location.pathname.split('/').filter(Boolean).pop() || '' - // some pages does not end with share code, so we need to check it - // TODO: maybe find a better way to access app code? - if (shareCode === 'webapp-signin' || shareCode === 'check-code') + const accessToken = getWebAppAccessToken() + if (accessToken) + request.headers.set('Authorization', `Bearer ${accessToken}`) + else + request.headers.delete('Authorization') + const shareCode = resolveShareCode() + if (!shareCode) return request.headers.set(WEB_APP_SHARE_CODE_HEADER_NAME, shareCode) request.headers.set(PASSPORT_HEADER_NAME, getWebAppPassport(shareCode)) diff --git a/web/service/share.ts b/web/service/share.ts index ce03f508d1..b19dbc896d 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -291,7 +291,9 @@ export const textToAudioStream = (url: string, isPublicAPI: boolean, header: { c export const fetchAccessToken = async ({ userId, appCode }: { userId?: string, appCode: string }) => { const headers = new Headers() headers.append(WEB_APP_SHARE_CODE_HEADER_NAME, appCode) - headers.append('Authorization', `Bearer ${getWebAppAccessToken()}`) + const accessToken = getWebAppAccessToken() + if (accessToken) + headers.append('Authorization', `Bearer ${accessToken}`) const params = new URLSearchParams() userId && params.append('user_id', userId) const url = `/passport?${params.toString()}` From c61c2b0abdcec2f80db32aaf2cf73534cace6730 Mon Sep 17 00:00:00 2001 From: GuanMu Date: Wed, 22 Oct 2025 17:08:27 +0800 Subject: [PATCH 24/33] Fix type error (#27274) --- web/app/components/app/annotation/index.tsx | 8 --- web/app/components/app/annotation/list.tsx | 3 - .../config-prompt/advanced-prompt-input.tsx | 4 +- .../config-prompt/simple-prompt-input.tsx | 4 +- .../config-var/config-modal/index.tsx | 2 +- .../app/configuration/config-var/index.tsx | 4 +- .../config/agent/prompt-editor.tsx | 4 +- .../app/configuration/debug/index.tsx | 2 +- .../app/configuration/tools/index.tsx | 6 +- .../app/create-from-dsl-modal/index.tsx | 4 +- web/app/components/base/confirm/index.tsx | 2 +- web/app/components/tools/labels/constant.ts | 4 +- web/app/components/tools/types.ts | 1 + web/models/app.ts | 4 +- web/package.json | 2 + web/pnpm-lock.yaml | 55 +++++++++---------- 16 files changed, 55 insertions(+), 54 deletions(-) diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index 264b1ac727..bc63b85f6d 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -53,7 +53,6 @@ const Annotation: FC = (props) => { const [isShowViewModal, setIsShowViewModal] = useState(false) const [selectedIds, setSelectedIds] = useState([]) const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) - const [isBatchDeleting, setIsBatchDeleting] = useState(false) const fetchAnnotationConfig = async () => { const res = await doFetchAnnotationConfig(appDetail.id) @@ -108,9 +107,6 @@ const Annotation: FC = (props) => { } const handleBatchDelete = async () => { - if (isBatchDeleting) - return - setIsBatchDeleting(true) try { await delAnnotations(appDetail.id, selectedIds) Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' }) @@ -121,9 +117,6 @@ const Annotation: FC = (props) => { catch (e: any) { Toast.notify({ type: 'error', message: e.message || t('common.api.actionFailed') }) } - finally { - setIsBatchDeleting(false) - } } const handleView = (item: AnnotationItem) => { @@ -213,7 +206,6 @@ const Annotation: FC = (props) => { onSelectedIdsChange={setSelectedIds} onBatchDelete={handleBatchDelete} onCancel={() => setSelectedIds([])} - isBatchDeleting={isBatchDeleting} /> :
} diff --git a/web/app/components/app/annotation/list.tsx b/web/app/components/app/annotation/list.tsx index 6705ac5768..70ecedb869 100644 --- a/web/app/components/app/annotation/list.tsx +++ b/web/app/components/app/annotation/list.tsx @@ -19,7 +19,6 @@ type Props = { onSelectedIdsChange: (selectedIds: string[]) => void onBatchDelete: () => Promise onCancel: () => void - isBatchDeleting?: boolean } const List: FC = ({ @@ -30,7 +29,6 @@ const List: FC = ({ onSelectedIdsChange, onBatchDelete, onCancel, - isBatchDeleting, }) => { const { t } = useTranslation() const { formatTime } = useTimestamp() @@ -142,7 +140,6 @@ const List: FC = ({ selectedIds={selectedIds} onBatchDelete={onBatchDelete} onCancel={onCancel} - isBatchDeleting={isBatchDeleting} /> )}
diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx index 70e0334e98..aa8d0f65ca 100644 --- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx @@ -78,7 +78,9 @@ const AdvancedPromptInput: FC = ({ const handleOpenExternalDataToolModal = () => { setShowExternalDataToolModal({ payload: {}, - onSaveCallback: (newExternalDataTool: ExternalDataTool) => { + onSaveCallback: (newExternalDataTool?: ExternalDataTool) => { + if (!newExternalDataTool) + return eventEmitter?.emit({ type: ADD_EXTERNAL_DATA_TOOL, payload: newExternalDataTool, diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index 169e8a14a2..8634232b2b 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -76,7 +76,9 @@ const Prompt: FC = ({ const handleOpenExternalDataToolModal = () => { setShowExternalDataToolModal({ payload: {}, - onSaveCallback: (newExternalDataTool: ExternalDataTool) => { + onSaveCallback: (newExternalDataTool?: ExternalDataTool) => { + if (!newExternalDataTool) + return eventEmitter?.emit({ type: ADD_EXTERNAL_DATA_TOOL, payload: newExternalDataTool, diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index de7d2c9eac..3f32c9b0c7 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -320,7 +320,7 @@ const ConfigModal: FC = ({ {type === InputVarType.paragraph && (