From f601093ccc07d1e8c1d3b91572ad9b4c70ef0b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 11 Jun 2025 15:38:51 +0800 Subject: [PATCH 1/8] fix: only enterprise version request app access mode (#20785) --- web/app/(shareLayout)/layout.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx index 8db336a17d..7de5d51edb 100644 --- a/web/app/(shareLayout)/layout.tsx +++ b/web/app/(shareLayout)/layout.tsx @@ -12,12 +12,18 @@ const Layout: FC<{ }> = ({ children }) => { const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending) const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode) + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const pathname = usePathname() const searchParams = useSearchParams() const redirectUrl = searchParams.get('redirect_url') const [isLoading, setIsLoading] = useState(true) useEffect(() => { (async () => { + if (!systemFeatures.webapp_auth.enabled) { + setIsLoading(false) + return + } + let appCode: string | null = null if (redirectUrl) appCode = redirectUrl?.split('/').pop() || null From d6d8cca053b074b17ea754821e9a52121102218d Mon Sep 17 00:00:00 2001 From: Yeuoly <45712896+Yeuoly@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:01:50 +0800 Subject: [PATCH 2/8] refactor: replace compact response generation with length-prefixed response for backwards invocation api (#20903) --- api/controllers/inner_api/plugin/plugin.py | 10 ++-- api/core/plugin/backwards_invocation/base.py | 8 ++- api/libs/helper.py | 56 ++++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/api/controllers/inner_api/plugin/plugin.py b/api/controllers/inner_api/plugin/plugin.py index f3a1bd8fa5..41063b35a5 100644 --- a/api/controllers/inner_api/plugin/plugin.py +++ b/api/controllers/inner_api/plugin/plugin.py @@ -29,7 +29,7 @@ from core.plugin.entities.request import ( RequestRequestUploadFile, ) from core.tools.entities.tool_entities import ToolProviderType -from libs.helper import compact_generate_response +from libs.helper import length_prefixed_response from models.account import Account, Tenant from models.model import EndUser @@ -44,7 +44,7 @@ class PluginInvokeLLMApi(Resource): response = PluginModelBackwardsInvocation.invoke_llm(user_model.id, tenant_model, payload) return PluginModelBackwardsInvocation.convert_to_event_stream(response) - return compact_generate_response(generator()) + return length_prefixed_response(0xF, generator()) class PluginInvokeTextEmbeddingApi(Resource): @@ -101,7 +101,7 @@ class PluginInvokeTTSApi(Resource): ) return PluginModelBackwardsInvocation.convert_to_event_stream(response) - return compact_generate_response(generator()) + return length_prefixed_response(0xF, generator()) class PluginInvokeSpeech2TextApi(Resource): @@ -162,7 +162,7 @@ class PluginInvokeToolApi(Resource): ), ) - return compact_generate_response(generator()) + return length_prefixed_response(0xF, generator()) class PluginInvokeParameterExtractorNodeApi(Resource): @@ -228,7 +228,7 @@ class PluginInvokeAppApi(Resource): files=payload.files, ) - return compact_generate_response(PluginAppBackwardsInvocation.convert_to_event_stream(response)) + return length_prefixed_response(0xF, PluginAppBackwardsInvocation.convert_to_event_stream(response)) class PluginInvokeEncryptApi(Resource): diff --git a/api/core/plugin/backwards_invocation/base.py b/api/core/plugin/backwards_invocation/base.py index 3214e07469..2a5f857576 100644 --- a/api/core/plugin/backwards_invocation/base.py +++ b/api/core/plugin/backwards_invocation/base.py @@ -11,14 +11,12 @@ class BaseBackwardsInvocation: try: for chunk in response: if isinstance(chunk, BaseModel | dict): - yield BaseBackwardsInvocationResponse(data=chunk).model_dump_json().encode() + b"\n\n" - elif isinstance(chunk, str): - yield f"event: {chunk}\n\n".encode() + yield BaseBackwardsInvocationResponse(data=chunk).model_dump_json().encode() except Exception as e: error_message = BaseBackwardsInvocationResponse(error=str(e)).model_dump_json() - yield f"{error_message}\n\n".encode() + yield error_message.encode() else: - yield BaseBackwardsInvocationResponse(data=response).model_dump_json().encode() + b"\n\n" + yield BaseBackwardsInvocationResponse(data=response).model_dump_json().encode() T = TypeVar("T", bound=dict | Mapping | str | bool | int | BaseModel) diff --git a/api/libs/helper.py b/api/libs/helper.py index e78a782fbe..070e575dbc 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -3,6 +3,7 @@ import logging import re import secrets import string +import struct import subprocess import time import uuid @@ -14,6 +15,7 @@ from zoneinfo import available_timezones from flask import Response, stream_with_context from flask_restful import fields +from pydantic import BaseModel from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator @@ -206,6 +208,60 @@ def compact_generate_response(response: Union[Mapping, Generator, RateLimitGener return Response(stream_with_context(generate()), status=200, mimetype="text/event-stream") +def length_prefixed_response(magic_number: int, response: Union[Mapping, Generator, RateLimitGenerator]) -> Response: + """ + This function is used to return a response with a length prefix. + Magic number is a one byte number that indicates the type of the response. + + For a compatibility with latest plugin daemon https://github.com/langgenius/dify-plugin-daemon/pull/341 + Avoid using line-based response, it leads a memory issue. + + We uses following format: + | Field | Size | Description | + |---------------|----------|---------------------------------| + | Magic Number | 1 byte | Magic number identifier | + | Reserved | 1 byte | Reserved field | + | Header Length | 2 bytes | Header length (usually 0xa) | + | Data Length | 4 bytes | Length of the data | + | Reserved | 6 bytes | Reserved fields | + | Data | Variable | Actual data content | + + | Reserved Fields | Header | Data | + |-----------------|----------|----------| + | 4 bytes total | Variable | Variable | + + all data is in little endian + """ + + def pack_response_with_length_prefix(response: bytes) -> bytes: + header_length = 0xA + data_length = len(response) + # | Magic Number 1byte | Reserved 1byte | Header Length 2bytes | Data Length 4bytes | Reserved 6bytes | Data + return struct.pack(" Generator: + for chunk in response: + if isinstance(chunk, str): + yield pack_response_with_length_prefix(chunk.encode("utf-8")) + else: + yield pack_response_with_length_prefix(chunk) + + return Response(stream_with_context(generate()), status=200, mimetype="text/event-stream") + + class TokenManager: @classmethod def generate_token( From acb2488fc8f9ba66cea5a4d99571a9f2eeb862b1 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 11 Jun 2025 16:28:36 +0800 Subject: [PATCH 3/8] chore(package): Bump version to 1.4.2 (#20897) Signed-off-by: -LAN- --- api/configs/packaging/__init__.py | 2 +- docker/docker-compose-template.yaml | 8 ++++---- docker/docker-compose.middleware.yaml | 2 +- docker/docker-compose.yaml | 8 ++++---- web/package.json | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index ae4875d591..5f209736a0 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings): CURRENT_VERSION: str = Field( description="Dify version", - default="1.4.1", + default="1.4.2", ) COMMIT_SHA: str = Field( diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 75bdab1a06..a409a729ce 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.1 + image: langgenius/dify-api:1.4.2 restart: always environment: # Use the shared environment variables. @@ -31,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.1 + image: langgenius/dify-api:1.4.2 restart: always environment: # Use the shared environment variables. @@ -57,7 +57,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.1 + image: langgenius/dify-web:1.4.2 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -142,7 +142,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.1-local + image: langgenius/dify-plugin-daemon:0.1.2-local restart: always environment: # Use the shared environment variables. diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index 8276e2977f..dceee484ca 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -71,7 +71,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.1-local + image: langgenius/dify-plugin-daemon:0.1.2-local restart: always env_file: - ./middleware.env diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e559021684..d927334118 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -508,7 +508,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.1 + image: langgenius/dify-api:1.4.2 restart: always environment: # Use the shared environment variables. @@ -537,7 +537,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.1 + image: langgenius/dify-api:1.4.2 restart: always environment: # Use the shared environment variables. @@ -563,7 +563,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.1 + image: langgenius/dify-web:1.4.2 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -648,7 +648,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.1-local + image: langgenius/dify-plugin-daemon:0.1.2-local restart: always environment: # Use the shared environment variables. diff --git a/web/package.json b/web/package.json index ff4214f966..f42233a5b3 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "1.4.1", + "version": "1.4.2", "private": true, "engines": { "node": ">=v22.11.0" From 41e3ecc8376125034a89914ad8f74dc695eb910f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E9=9C=B2=E5=85=88=E7=94=9F?= Date: Wed, 11 Jun 2025 16:57:24 +0800 Subject: [PATCH 4/8] fix remote ip header CF-Connecting-IP (#20846) --- api/libs/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/libs/helper.py b/api/libs/helper.py index 070e575dbc..3f2a630956 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -185,7 +185,7 @@ def generate_string(n): def extract_remote_ip(request) -> str: if request.headers.get("CF-Connecting-IP"): - return cast(str, request.headers.get("Cf-Connecting-Ip")) + return cast(str, request.headers.get("CF-Connecting-IP")) elif request.headers.getlist("X-Forwarded-For"): return cast(str, request.headers.getlist("X-Forwarded-For")[0]) else: From 1e03c97663a2df7d23831ff5cf2862d1ca8b2faa Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 11 Jun 2025 18:56:07 +0800 Subject: [PATCH 5/8] fix(llm_node): missing parameters for structure outputs (#20915) Signed-off-by: -LAN- --- api/core/workflow/nodes/llm/node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index ead929252c..d27124d62c 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -525,6 +525,8 @@ class LLMNode(BaseNode[LLMNodeData]): # Set appropriate response format based on model capabilities self._set_response_format(completion_params, model_schema.parameter_rules) model_config_with_cred.parameters = completion_params + # NOTE(-LAN-): This line modify the `self.node_data.model`, which is used in `_invoke_llm()`. + node_data_model.completion_params = completion_params return model, model_config_with_cred def _fetch_prompt_messages( From af83120832224f87e087c8152b66fa44077e1c3c Mon Sep 17 00:00:00 2001 From: Takuya Ono Date: Thu, 12 Jun 2025 01:49:38 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=90=9B=20Fix(Gemini=20LLM):=20Support?= =?UTF-8?q?=20Gemini=200.2.x=20plugin=20on=20agent=20app=20(#20794)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: QuantumGhost --- api/core/app/apps/base_app_runner.py | 21 +++++++++++++++---- .../easy_ui_based_generate_task_pipeline.py | 18 ++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/api/core/app/apps/base_app_runner.py b/api/core/app/apps/base_app_runner.py index c813dbb9d1..a3f0cf7f9f 100644 --- a/api/core/app/apps/base_app_runner.py +++ b/api/core/app/apps/base_app_runner.py @@ -1,3 +1,4 @@ +import logging import time from collections.abc import Generator, Mapping, Sequence from typing import TYPE_CHECKING, Any, Optional, Union @@ -33,6 +34,8 @@ from models.model import App, AppMode, Message, MessageAnnotation if TYPE_CHECKING: from core.file.models import File +_logger = logging.getLogger(__name__) + class AppRunner: def get_pre_calculate_rest_tokens( @@ -298,7 +301,7 @@ class AppRunner: ) def _handle_invoke_result_stream( - self, invoke_result: Generator, queue_manager: AppQueueManager, agent: bool + self, invoke_result: Generator[LLMResultChunk, None, None], queue_manager: AppQueueManager, agent: bool ) -> None: """ Handle invoke result @@ -317,18 +320,28 @@ class AppRunner: else: queue_manager.publish(QueueAgentMessageEvent(chunk=result), PublishFrom.APPLICATION_MANAGER) - text += result.delta.message.content + message = result.delta.message + if isinstance(message.content, str): + text += message.content + elif isinstance(message.content, list): + for content in message.content: + if not isinstance(content, str): + # TODO(QuantumGhost): Add multimodal output support for easy ui. + _logger.warning("received multimodal output, type=%s", type(content)) + text += content.data + else: + text += content # failback to str if not model: model = result.model if not prompt_messages: - prompt_messages = result.prompt_messages + prompt_messages = list(result.prompt_messages) if result.delta.usage: usage = result.delta.usage - if not usage: + if usage is None: usage = LLMUsage.empty_usage() llm_result = LLMResult( diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py index 1ea50a5778..d535e1f835 100644 --- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py @@ -48,6 +48,7 @@ from core.model_manager import ModelInstance from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage from core.model_runtime.entities.message_entities import ( AssistantPromptMessage, + TextPromptMessageContent, ) from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.ops.entities.trace_entity import TraceTaskName @@ -309,6 +310,23 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline): delta_text = chunk.delta.message.content if delta_text is None: continue + if isinstance(chunk.delta.message.content, list): + delta_text = "" + for content in chunk.delta.message.content: + logger.debug( + "The content type %s in LLM chunk delta message content.: %r", type(content), content + ) + if isinstance(content, TextPromptMessageContent): + delta_text += content.data + elif isinstance(content, str): + delta_text += content # failback to str + else: + logger.warning( + "Unsupported content type %s in LLM chunk delta message content.: %r", + type(content), + content, + ) + continue if not self._task_state.llm_result.prompt_messages: self._task_state.llm_result.prompt_messages = chunk.prompt_messages From b2ac11bc47b77e4a2dede3799fe34cc15ce9deda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 12 Jun 2025 14:18:15 +0800 Subject: [PATCH 7/8] fix: markdown button can't send message (#20933) --- web/app/components/base/markdown-blocks/button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/markdown-blocks/button.tsx b/web/app/components/base/markdown-blocks/button.tsx index 4646b12921..315653bcd0 100644 --- a/web/app/components/base/markdown-blocks/button.tsx +++ b/web/app/components/base/markdown-blocks/button.tsx @@ -14,7 +14,7 @@ const MarkdownButton = ({ node }: any) => { size={size} className={cn('!h-auto min-h-8 select-none whitespace-normal !px-3')} onClick={() => { - if (isValidUrl(link)) { + if (link && isValidUrl(link)) { window.open(link, '_blank') return } From c05e47ebc0017caba71f1787a10523a96070a53f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Fri, 13 Jun 2025 09:42:02 +0800 Subject: [PATCH 8/8] refactor(sqlalchemy_workflow_execution_repository): Use the max funtion for getting next_sequence_number. (#20966) --- .../sqlalchemy_workflow_execution_repository.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/core/repositories/sqlalchemy_workflow_execution_repository.py b/api/core/repositories/sqlalchemy_workflow_execution_repository.py index 19086cffff..e5ead9dc56 100644 --- a/api/core/repositories/sqlalchemy_workflow_execution_repository.py +++ b/api/core/repositories/sqlalchemy_workflow_execution_repository.py @@ -6,7 +6,7 @@ import json import logging from typing import Optional, Union -from sqlalchemy import select +from sqlalchemy import func, select from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker @@ -151,11 +151,11 @@ class SQLAlchemyWorkflowExecutionRepository(WorkflowExecutionRepository): existing = session.scalar(select(WorkflowRun).where(WorkflowRun.id == domain_model.id_)) if not existing: # For new records, get the next sequence number - stmt = select(WorkflowRun.sequence_number).where( + stmt = select(func.max(WorkflowRun.sequence_number)).where( WorkflowRun.app_id == self._app_id, WorkflowRun.tenant_id == self._tenant_id, ) - max_sequence = session.scalar(stmt.order_by(WorkflowRun.sequence_number.desc())) + max_sequence = session.scalar(stmt) db_model.sequence_number = (max_sequence or 0) + 1 else: # For updates, keep the existing sequence number