Merge remote-tracking branch 'upstream/main' into feat/support-image-generate-for-gemini

pull/17372/head
QuantumGhost 1 year ago
commit 44c0e54683

@ -52,7 +52,6 @@ def initialize_extensions(app: DifyApp):
ext_mail, ext_mail,
ext_migrate, ext_migrate,
ext_otel, ext_otel,
ext_otel_patch,
ext_proxy_fix, ext_proxy_fix,
ext_redis, ext_redis,
ext_repositories, ext_repositories,
@ -85,7 +84,6 @@ def initialize_extensions(app: DifyApp):
ext_proxy_fix, ext_proxy_fix,
ext_blueprints, ext_blueprints,
ext_commands, ext_commands,
ext_otel_patch, # Apply patch before initializing OpenTelemetry
ext_otel, ext_otel,
] ]
for ext in extensions: for ext in extensions:

@ -818,8 +818,9 @@ def clear_free_plan_tenant_expired_logs(days: int, batch: int, tenant_ids: list[
click.echo(click.style("Clear free plan tenant expired logs completed.", fg="green")) click.echo(click.style("Clear free plan tenant expired logs completed.", fg="green"))
@click.option("-f", "--force", is_flag=True, help="Skip user confirmation and force the command to execute.")
@click.command("clear-orphaned-file-records", help="Clear orphaned file records.") @click.command("clear-orphaned-file-records", help="Clear orphaned file records.")
def clear_orphaned_file_records(): def clear_orphaned_file_records(force: bool):
""" """
Clear orphaned file records in the database. Clear orphaned file records in the database.
""" """
@ -845,7 +846,15 @@ def clear_orphaned_file_records():
# notify user and ask for confirmation # notify user and ask for confirmation
click.echo( click.echo(
click.style("This command will find and delete orphaned file records in the following tables:", fg="yellow") click.style(
"This command will first find and delete orphaned file records from the message_files table,", fg="yellow"
)
)
click.echo(
click.style(
"and then it will find and delete orphaned file records in the following tables:",
fg="yellow",
)
) )
for files_table in files_tables: for files_table in files_tables:
click.echo(click.style(f"- {files_table['table']}", fg="yellow")) click.echo(click.style(f"- {files_table['table']}", fg="yellow"))
@ -878,11 +887,55 @@ def clear_orphaned_file_records():
fg="yellow", fg="yellow",
) )
) )
if not force:
click.confirm("Do you want to proceed?", abort=True) click.confirm("Do you want to proceed?", abort=True)
# start the cleanup process # start the cleanup process
click.echo(click.style("Starting orphaned file records cleanup.", fg="white")) click.echo(click.style("Starting orphaned file records cleanup.", fg="white"))
# clean up the orphaned records in the message_files table where message_id doesn't exist in messages table
try:
click.echo(
click.style("- Listing message_files records where message_id doesn't exist in messages table", fg="white")
)
query = (
"SELECT mf.id, mf.message_id "
"FROM message_files mf LEFT JOIN messages m ON mf.message_id = m.id "
"WHERE m.id IS NULL"
)
orphaned_message_files = []
with db.engine.begin() as conn:
rs = conn.execute(db.text(query))
for i in rs:
orphaned_message_files.append({"id": str(i[0]), "message_id": str(i[1])})
if orphaned_message_files:
click.echo(click.style(f"Found {len(orphaned_message_files)} orphaned message_files records:", fg="white"))
for record in orphaned_message_files:
click.echo(click.style(f" - id: {record['id']}, message_id: {record['message_id']}", fg="black"))
if not force:
click.confirm(
(
f"Do you want to proceed "
f"to delete all {len(orphaned_message_files)} orphaned message_files records?"
),
abort=True,
)
click.echo(click.style("- Deleting orphaned message_files records", fg="white"))
query = "DELETE FROM message_files WHERE id IN :ids"
with db.engine.begin() as conn:
conn.execute(db.text(query), {"ids": tuple([record["id"] for record in orphaned_message_files])})
click.echo(
click.style(f"Removed {len(orphaned_message_files)} orphaned message_files records.", fg="green")
)
else:
click.echo(click.style("No orphaned message_files records found. There is nothing to delete.", fg="green"))
except Exception as e:
click.echo(click.style(f"Error deleting orphaned message_files records: {str(e)}", fg="red"))
# clean up the orphaned records in the rest of the *_files tables
try: try:
# fetch file id and keys from each table # fetch file id and keys from each table
all_files_in_tables = [] all_files_in_tables = []
@ -964,6 +1017,7 @@ def clear_orphaned_file_records():
click.echo(click.style(f"Found {len(orphaned_files)} orphaned file records.", fg="white")) click.echo(click.style(f"Found {len(orphaned_files)} orphaned file records.", fg="white"))
for file in orphaned_files: for file in orphaned_files:
click.echo(click.style(f"- orphaned file id: {file}", fg="black")) click.echo(click.style(f"- orphaned file id: {file}", fg="black"))
if not force:
click.confirm(f"Do you want to proceed to delete all {len(orphaned_files)} orphaned file records?", abort=True) click.confirm(f"Do you want to proceed to delete all {len(orphaned_files)} orphaned file records?", abort=True)
# delete orphaned records for each file # delete orphaned records for each file
@ -979,8 +1033,9 @@ def clear_orphaned_file_records():
click.echo(click.style(f"Removed {len(orphaned_files)} orphaned file records.", fg="green")) click.echo(click.style(f"Removed {len(orphaned_files)} orphaned file records.", fg="green"))
@click.option("-f", "--force", is_flag=True, help="Skip user confirmation and force the command to execute.")
@click.command("remove-orphaned-files-on-storage", help="Remove orphaned files on the storage.") @click.command("remove-orphaned-files-on-storage", help="Remove orphaned files on the storage.")
def remove_orphaned_files_on_storage(): def remove_orphaned_files_on_storage(force: bool):
""" """
Remove orphaned files on the storage. Remove orphaned files on the storage.
""" """
@ -1028,6 +1083,7 @@ def remove_orphaned_files_on_storage():
fg="yellow", fg="yellow",
) )
) )
if not force:
click.confirm("Do you want to proceed?", abort=True) click.confirm("Do you want to proceed?", abort=True)
# start the cleanup process # start the cleanup process
@ -1069,6 +1125,7 @@ def remove_orphaned_files_on_storage():
click.echo(click.style(f"Found {len(orphaned_files)} orphaned files.", fg="white")) click.echo(click.style(f"Found {len(orphaned_files)} orphaned files.", fg="white"))
for file in orphaned_files: for file in orphaned_files:
click.echo(click.style(f"- orphaned file: {file}", fg="black")) click.echo(click.style(f"- orphaned file: {file}", fg="black"))
if not force:
click.confirm(f"Do you want to proceed to remove all {len(orphaned_files)} orphaned files?", abort=True) click.confirm(f"Do you want to proceed to remove all {len(orphaned_files)} orphaned files?", abort=True)
# delete orphaned files # delete orphaned files

@ -1,4 +1,5 @@
from typing import Optional import enum
from typing import Literal, Optional
from pydantic import Field, PositiveInt from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@ -9,6 +10,14 @@ class OpenSearchConfig(BaseSettings):
Configuration settings for OpenSearch Configuration settings for OpenSearch
""" """
class AuthMethod(enum.StrEnum):
"""
Authentication method for OpenSearch
"""
BASIC = "basic"
AWS_MANAGED_IAM = "aws_managed_iam"
OPENSEARCH_HOST: Optional[str] = Field( OPENSEARCH_HOST: Optional[str] = Field(
description="Hostname or IP address of the OpenSearch server (e.g., 'localhost' or 'opensearch.example.com')", description="Hostname or IP address of the OpenSearch server (e.g., 'localhost' or 'opensearch.example.com')",
default=None, default=None,
@ -19,6 +28,16 @@ class OpenSearchConfig(BaseSettings):
default=9200, default=9200,
) )
OPENSEARCH_SECURE: bool = Field(
description="Whether to use SSL/TLS encrypted connection for OpenSearch (True for HTTPS, False for HTTP)",
default=False,
)
OPENSEARCH_AUTH_METHOD: AuthMethod = Field(
description="Authentication method for OpenSearch connection (default is 'basic')",
default=AuthMethod.BASIC,
)
OPENSEARCH_USER: Optional[str] = Field( OPENSEARCH_USER: Optional[str] = Field(
description="Username for authenticating with OpenSearch", description="Username for authenticating with OpenSearch",
default=None, default=None,
@ -29,7 +48,11 @@ class OpenSearchConfig(BaseSettings):
default=None, default=None,
) )
OPENSEARCH_SECURE: bool = Field( OPENSEARCH_AWS_REGION: Optional[str] = Field(
description="Whether to use SSL/TLS encrypted connection for OpenSearch (True for HTTPS, False for HTTP)", description="AWS region for OpenSearch (e.g. 'us-west-2')",
default=False, default=None,
)
OPENSEARCH_AWS_SERVICE: Optional[Literal["es", "aoss"]] = Field(
description="AWS service for OpenSearch (e.g. 'aoss' for OpenSearch Serverless)", default=None
) )

@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field( CURRENT_VERSION: str = Field(
description="Dify version", description="Dify version",
default="1.3.0", default="1.3.1",
) )
COMMIT_SHA: str = Field( COMMIT_SHA: str = Field(

@ -16,11 +16,25 @@ AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS])
if dify_config.ETL_TYPE == "Unstructured": if dify_config.ETL_TYPE == "Unstructured":
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls"] DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "vtt", "properties"]
DOCUMENT_EXTENSIONS.extend(("doc", "docx", "csv", "eml", "msg", "pptx", "xml", "epub")) DOCUMENT_EXTENSIONS.extend(("doc", "docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
if dify_config.UNSTRUCTURED_API_URL: if dify_config.UNSTRUCTURED_API_URL:
DOCUMENT_EXTENSIONS.append("ppt") DOCUMENT_EXTENSIONS.append("ppt")
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS]) DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
else: else:
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "docx", "csv"] DOCUMENT_EXTENSIONS = [
"txt",
"markdown",
"md",
"mdx",
"pdf",
"html",
"htm",
"xlsx",
"xls",
"docx",
"csv",
"vtt",
"properties",
]
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS]) DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])

@ -25,8 +25,8 @@ from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotA
from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager from core.ops.ops_trace_manager import TraceQueueManager
from core.prompt.utils.get_thread_messages_length import get_thread_messages_length from core.prompt.utils.get_thread_messages_length import get_thread_messages_length
from core.repository import RepositoryFactory from core.workflow.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db from extensions.ext_database import db
from factories import file_factory from factories import file_factory
from models.account import Account from models.account import Account

@ -62,10 +62,10 @@ from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
from core.model_runtime.entities.llm_entities import LLMUsage from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.ops_trace_manager import TraceQueueManager from core.ops.ops_trace_manager import TraceQueueManager
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.enums import SystemVariableKey from core.workflow.enums import SystemVariableKey
from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
from core.workflow.nodes import NodeType from core.workflow.nodes import NodeType
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from events.message_event import message_was_created from events.message_event import message_was_created
from extensions.ext_database import db from extensions.ext_database import db
from models import Conversation, EndUser, Message, MessageFile from models import Conversation, EndUser, Message, MessageFile

@ -23,8 +23,8 @@ from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerat
from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse
from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager from core.ops.ops_trace_manager import TraceQueueManager
from core.repository import RepositoryFactory from core.workflow.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db from extensions.ext_database import db
from factories import file_factory from factories import file_factory
from models import Account, App, EndUser, Workflow from models import Account, App, EndUser, Workflow

@ -54,8 +54,8 @@ from core.app.entities.task_entities import (
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
from core.ops.ops_trace_manager import TraceQueueManager from core.ops.ops_trace_manager import TraceQueueManager
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.enums import SystemVariableKey from core.workflow.enums import SystemVariableKey
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db from extensions.ext_database import db
from models.account import Account from models.account import Account
from models.enums import CreatedByRole from models.enums import CreatedByRole

@ -49,12 +49,12 @@ from core.file import FILE_MODEL_IDENTITY, File
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.entities.trace_entity import TraceTaskName from core.ops.entities.trace_entity import TraceTaskName
from core.ops.ops_trace_manager import TraceQueueManager, TraceTask from core.ops.ops_trace_manager import TraceQueueManager, TraceTask
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.tools.tool_manager import ToolManager from core.tools.tool_manager import ToolManager
from core.workflow.entities.node_entities import NodeRunMetadataKey from core.workflow.entities.node_entities import NodeRunMetadataKey
from core.workflow.enums import SystemVariableKey from core.workflow.enums import SystemVariableKey
from core.workflow.nodes import NodeType from core.workflow.nodes import NodeType
from core.workflow.nodes.tool.entities import ToolNodeData from core.workflow.nodes.tool.entities import ToolNodeData
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.workflow_entry import WorkflowEntry from core.workflow.workflow_entry import WorkflowEntry
from models.account import Account from models.account import Account
from models.enums import CreatedByRole, WorkflowRunTriggeredFrom from models.enums import CreatedByRole, WorkflowRunTriggeredFrom
@ -381,6 +381,8 @@ class WorkflowCycleManage:
workflow_node_execution.elapsed_time = elapsed_time workflow_node_execution.elapsed_time = elapsed_time
workflow_node_execution.execution_metadata = execution_metadata workflow_node_execution.execution_metadata = execution_metadata
self._workflow_node_execution_repository.update(workflow_node_execution)
return workflow_node_execution return workflow_node_execution
def _handle_workflow_node_execution_retried( def _handle_workflow_node_execution_retried(

@ -3,6 +3,8 @@ import logging
import re import re
from typing import Optional, cast from typing import Optional, cast
import json_repair
from core.llm_generator.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser from core.llm_generator.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser
from core.llm_generator.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser from core.llm_generator.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser
from core.llm_generator.prompts import ( from core.llm_generator.prompts import (
@ -366,7 +368,20 @@ class LLMGenerator:
), ),
) )
generated_json_schema = cast(str, response.message.content) raw_content = response.message.content
if not isinstance(raw_content, str):
raise ValueError(f"LLM response content must be a string, got: {type(raw_content)}")
try:
parsed_content = json.loads(raw_content)
except json.JSONDecodeError:
parsed_content = json_repair.loads(raw_content)
if not isinstance(parsed_content, dict | list):
raise ValueError(f"Failed to parse structured output from llm: {raw_content}")
generated_json_schema = json.dumps(parsed_content, indent=2, ensure_ascii=False)
return {"output": generated_json_schema, "error": ""} return {"output": generated_json_schema, "error": ""}
except InvokeError as e: except InvokeError as e:

@ -29,7 +29,7 @@ from core.ops.langfuse_trace.entities.langfuse_trace_entity import (
UnitEnum, UnitEnum,
) )
from core.ops.utils import filter_none_values from core.ops.utils import filter_none_values
from core.repository.repository_factory import RepositoryFactory from core.workflow.repository.repository_factory import RepositoryFactory
from extensions.ext_database import db from extensions.ext_database import db
from models.model import EndUser from models.model import EndUser

@ -28,7 +28,7 @@ from core.ops.langsmith_trace.entities.langsmith_trace_entity import (
LangSmithRunUpdateModel, LangSmithRunUpdateModel,
) )
from core.ops.utils import filter_none_values, generate_dotted_order from core.ops.utils import filter_none_values, generate_dotted_order
from core.repository.repository_factory import RepositoryFactory from core.workflow.repository.repository_factory import RepositoryFactory
from extensions.ext_database import db from extensions.ext_database import db
from models.model import EndUser, MessageFile from models.model import EndUser, MessageFile

@ -22,7 +22,7 @@ from core.ops.entities.trace_entity import (
TraceTaskName, TraceTaskName,
WorkflowTraceInfo, WorkflowTraceInfo,
) )
from core.repository.repository_factory import RepositoryFactory from core.workflow.repository.repository_factory import RepositoryFactory
from extensions.ext_database import db from extensions.ext_database import db
from models.model import EndUser, MessageFile from models.model import EndUser, MessageFile

@ -72,7 +72,7 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation):
raise ValueError("missing query") raise ValueError("missing query")
return cls.invoke_chat_app(app, user, conversation_id, query, stream, inputs, files) return cls.invoke_chat_app(app, user, conversation_id, query, stream, inputs, files)
elif app.mode == AppMode.WORKFLOW.value: elif app.mode == AppMode.WORKFLOW:
return cls.invoke_workflow_app(app, user, stream, inputs, files) return cls.invoke_workflow_app(app, user, stream, inputs, files)
elif app.mode == AppMode.COMPLETION: elif app.mode == AppMode.COMPLETION:
return cls.invoke_completion_app(app, user, stream, inputs, files) return cls.invoke_completion_app(app, user, stream, inputs, files)

@ -27,8 +27,8 @@ class MilvusConfig(BaseModel):
uri: str # Milvus server URI uri: str # Milvus server URI
token: Optional[str] = None # Optional token for authentication token: Optional[str] = None # Optional token for authentication
user: str # Username for authentication user: Optional[str] = None # Username for authentication
password: str # Password for authentication password: Optional[str] = None # Password for authentication
batch_size: int = 100 # Batch size for operations batch_size: int = 100 # Batch size for operations
database: str = "default" # Database name database: str = "default" # Database name
enable_hybrid_search: bool = False # Flag to enable hybrid search enable_hybrid_search: bool = False # Flag to enable hybrid search
@ -43,6 +43,7 @@ class MilvusConfig(BaseModel):
""" """
if not values.get("uri"): if not values.get("uri"):
raise ValueError("config MILVUS_URI is required") raise ValueError("config MILVUS_URI is required")
if not values.get("token"):
if not values.get("user"): if not values.get("user"):
raise ValueError("config MILVUS_USER is required") raise ValueError("config MILVUS_USER is required")
if not values.get("password"): if not values.get("password"):
@ -356,10 +357,13 @@ class MilvusVector(BaseVector):
) )
redis_client.set(collection_exist_cache_key, 1, ex=3600) redis_client.set(collection_exist_cache_key, 1, ex=3600)
def _init_client(self, config) -> MilvusClient: def _init_client(self, config: MilvusConfig) -> MilvusClient:
""" """
Initialize and return a Milvus client. Initialize and return a Milvus client.
""" """
if config.token:
client = MilvusClient(uri=config.uri, token=config.token, db_name=config.database)
else:
client = MilvusClient(uri=config.uri, user=config.user, password=config.password, db_name=config.database) client = MilvusClient(uri=config.uri, user=config.user, password=config.password, db_name=config.database)
return client return client

@ -1,10 +1,9 @@
import json import json
import logging import logging
import ssl from typing import Any, Literal, Optional
from typing import Any, Optional
from uuid import uuid4 from uuid import uuid4
from opensearchpy import OpenSearch, helpers from opensearchpy import OpenSearch, Urllib3AWSV4SignerAuth, Urllib3HttpConnection, helpers
from opensearchpy.helpers import BulkIndexError from opensearchpy.helpers import BulkIndexError
from pydantic import BaseModel, model_validator from pydantic import BaseModel, model_validator
@ -24,9 +23,12 @@ logger = logging.getLogger(__name__)
class OpenSearchConfig(BaseModel): class OpenSearchConfig(BaseModel):
host: str host: str
port: int port: int
secure: bool = False
auth_method: Literal["basic", "aws_managed_iam"] = "basic"
user: Optional[str] = None user: Optional[str] = None
password: Optional[str] = None password: Optional[str] = None
secure: bool = False aws_region: Optional[str] = None
aws_service: Optional[str] = None
@model_validator(mode="before") @model_validator(mode="before")
@classmethod @classmethod
@ -35,24 +37,40 @@ class OpenSearchConfig(BaseModel):
raise ValueError("config OPENSEARCH_HOST is required") raise ValueError("config OPENSEARCH_HOST is required")
if not values.get("port"): if not values.get("port"):
raise ValueError("config OPENSEARCH_PORT is required") raise ValueError("config OPENSEARCH_PORT is required")
if values.get("auth_method") == "aws_managed_iam":
if not values.get("aws_region"):
raise ValueError("config OPENSEARCH_AWS_REGION is required for AWS_MANAGED_IAM auth method")
if not values.get("aws_service"):
raise ValueError("config OPENSEARCH_AWS_SERVICE is required for AWS_MANAGED_IAM auth method")
return values return values
def create_ssl_context(self) -> ssl.SSLContext: def create_aws_managed_iam_auth(self) -> Urllib3AWSV4SignerAuth:
ssl_context = ssl.create_default_context() import boto3 # type: ignore
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE # Disable Certificate Validation return Urllib3AWSV4SignerAuth(
return ssl_context credentials=boto3.Session().get_credentials(),
region=self.aws_region,
service=self.aws_service, # type: ignore[arg-type]
)
def to_opensearch_params(self) -> dict[str, Any]: def to_opensearch_params(self) -> dict[str, Any]:
params = { params = {
"hosts": [{"host": self.host, "port": self.port}], "hosts": [{"host": self.host, "port": self.port}],
"use_ssl": self.secure, "use_ssl": self.secure,
"verify_certs": self.secure, "verify_certs": self.secure,
"connection_class": Urllib3HttpConnection,
"pool_maxsize": 20,
} }
if self.user and self.password:
if self.auth_method == "basic":
logger.info("Using basic authentication for OpenSearch Vector DB")
params["http_auth"] = (self.user, self.password) params["http_auth"] = (self.user, self.password)
if self.secure: elif self.auth_method == "aws_managed_iam":
params["ssl_context"] = self.create_ssl_context() logger.info("Using AWS managed IAM role for OpenSearch Vector DB")
params["http_auth"] = self.create_aws_managed_iam_auth()
return params return params
@ -76,16 +94,23 @@ class OpenSearchVector(BaseVector):
action = { action = {
"_op_type": "index", "_op_type": "index",
"_index": self._collection_name.lower(), "_index": self._collection_name.lower(),
"_id": uuid4().hex,
"_source": { "_source": {
Field.CONTENT_KEY.value: documents[i].page_content, Field.CONTENT_KEY.value: documents[i].page_content,
Field.VECTOR.value: embeddings[i], # Make sure you pass an array here Field.VECTOR.value: embeddings[i], # Make sure you pass an array here
Field.METADATA_KEY.value: documents[i].metadata, Field.METADATA_KEY.value: documents[i].metadata,
}, },
} }
# See https://github.com/langchain-ai/langchainjs/issues/4346#issuecomment-1935123377
if self._client_config.aws_service not in ["aoss"]:
action["_id"] = uuid4().hex
actions.append(action) actions.append(action)
helpers.bulk(self._client, actions) helpers.bulk(
client=self._client,
actions=actions,
timeout=30,
max_retries=3,
)
def get_ids_by_metadata_field(self, key: str, value: str): def get_ids_by_metadata_field(self, key: str, value: str):
query = {"query": {"term": {f"{Field.METADATA_KEY.value}.{key}": value}}} query = {"query": {"term": {f"{Field.METADATA_KEY.value}.{key}": value}}}
@ -234,6 +259,7 @@ class OpenSearchVector(BaseVector):
}, },
} }
logger.info(f"Creating OpenSearch index {self._collection_name.lower()}")
self._client.indices.create(index=self._collection_name.lower(), body=index_body) self._client.indices.create(index=self._collection_name.lower(), body=index_body)
redis_client.set(collection_exist_cache_key, 1, ex=3600) redis_client.set(collection_exist_cache_key, 1, ex=3600)
@ -252,9 +278,12 @@ class OpenSearchVectorFactory(AbstractVectorFactory):
open_search_config = OpenSearchConfig( open_search_config = OpenSearchConfig(
host=dify_config.OPENSEARCH_HOST or "localhost", host=dify_config.OPENSEARCH_HOST or "localhost",
port=dify_config.OPENSEARCH_PORT, port=dify_config.OPENSEARCH_PORT,
secure=dify_config.OPENSEARCH_SECURE,
auth_method=dify_config.OPENSEARCH_AUTH_METHOD.value,
user=dify_config.OPENSEARCH_USER, user=dify_config.OPENSEARCH_USER,
password=dify_config.OPENSEARCH_PASSWORD, password=dify_config.OPENSEARCH_PASSWORD,
secure=dify_config.OPENSEARCH_SECURE, aws_region=dify_config.OPENSEARCH_AWS_REGION,
aws_service=dify_config.OPENSEARCH_AWS_SERVICE,
) )
return OpenSearchVector(collection_name=collection_name, config=open_search_config) return OpenSearchVector(collection_name=collection_name, config=open_search_config)

@ -52,6 +52,7 @@ class RerankModelRunner(BaseRerankRunner):
rerank_documents = [] rerank_documents = []
for result in rerank_result.docs: for result in rerank_result.docs:
if score_threshold is None or result.score >= score_threshold:
# format document # format document
rerank_document = Document( rerank_document = Document(
page_content=result.text, page_content=result.text,
@ -62,4 +63,5 @@ class RerankModelRunner(BaseRerankRunner):
rerank_document.metadata["score"] = result.score rerank_document.metadata["score"] = result.score
rerank_documents.append(rerank_document) rerank_documents.append(rerank_document)
return rerank_documents rerank_documents.sort(key=lambda x: x.metadata.get("score", 0.0), reverse=True)
return rerank_documents[:top_n] if top_n else rerank_documents

@ -2,5 +2,5 @@
Repository implementations for data access. Repository implementations for data access.
This package contains concrete implementations of the repository interfaces This package contains concrete implementations of the repository interfaces
defined in the core.repository package. defined in the core.workflow.repository package.
""" """

@ -11,9 +11,9 @@ from typing import Any
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from configs import dify_config from configs import dify_config
from core.repository.repository_factory import RepositoryFactory from core.repositories.workflow_node_execution import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.repository.repository_factory import RepositoryFactory
from extensions.ext_database import db from extensions.ext_database import db
from repositories.workflow_node_execution import SQLAlchemyWorkflowNodeExecutionRepository
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -2,7 +2,7 @@
WorkflowNodeExecution repository implementations. WorkflowNodeExecution repository implementations.
""" """
from repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository from core.repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
__all__ = [ __all__ = [
"SQLAlchemyWorkflowNodeExecutionRepository", "SQLAlchemyWorkflowNodeExecutionRepository",

@ -10,7 +10,7 @@ from sqlalchemy import UnaryExpression, asc, delete, desc, select
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from core.repository.workflow_node_execution_repository import OrderConfig from core.workflow.repository.workflow_node_execution_repository import OrderConfig
from models.workflow import WorkflowNodeExecution, WorkflowNodeExecutionStatus, WorkflowNodeExecutionTriggeredFrom from models.workflow import WorkflowNodeExecution, WorkflowNodeExecutionStatus, WorkflowNodeExecutionTriggeredFrom
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -246,7 +246,7 @@ class ToolEngine:
+ "you do not need to create it, just tell the user to check it now." + "you do not need to create it, just tell the user to check it now."
) )
elif response.type == ToolInvokeMessage.MessageType.JSON: elif response.type == ToolInvokeMessage.MessageType.JSON:
result = json.dumps( result += json.dumps(
cast(ToolInvokeMessage.JsonMessage, response.message).json_object, ensure_ascii=False cast(ToolInvokeMessage.JsonMessage, response.message).json_object, ensure_ascii=False
) )
else: else:

@ -11,6 +11,7 @@ import docx
import pandas as pd import pandas as pd
import pypandoc # type: ignore import pypandoc # type: ignore
import pypdfium2 # type: ignore import pypdfium2 # type: ignore
import webvtt # type: ignore
import yaml # type: ignore import yaml # type: ignore
from docx.document import Document from docx.document import Document
from docx.oxml.table import CT_Tbl from docx.oxml.table import CT_Tbl
@ -132,6 +133,10 @@ def _extract_text_by_mime_type(*, file_content: bytes, mime_type: str) -> str:
return _extract_text_from_json(file_content) return _extract_text_from_json(file_content)
case "application/x-yaml" | "text/yaml": case "application/x-yaml" | "text/yaml":
return _extract_text_from_yaml(file_content) return _extract_text_from_yaml(file_content)
case "text/vtt":
return _extract_text_from_vtt(file_content)
case "text/properties":
return _extract_text_from_properties(file_content)
case _: case _:
raise UnsupportedFileTypeError(f"Unsupported MIME type: {mime_type}") raise UnsupportedFileTypeError(f"Unsupported MIME type: {mime_type}")
@ -139,7 +144,7 @@ def _extract_text_by_mime_type(*, file_content: bytes, mime_type: str) -> str:
def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str) -> str: def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str) -> str:
"""Extract text from a file based on its file extension.""" """Extract text from a file based on its file extension."""
match file_extension: match file_extension:
case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml" | ".vtt": case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml":
return _extract_text_from_plain_text(file_content) return _extract_text_from_plain_text(file_content)
case ".json": case ".json":
return _extract_text_from_json(file_content) return _extract_text_from_json(file_content)
@ -165,6 +170,10 @@ def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str)
return _extract_text_from_eml(file_content) return _extract_text_from_eml(file_content)
case ".msg": case ".msg":
return _extract_text_from_msg(file_content) return _extract_text_from_msg(file_content)
case ".vtt":
return _extract_text_from_vtt(file_content)
case ".properties":
return _extract_text_from_properties(file_content)
case _: case _:
raise UnsupportedFileTypeError(f"Unsupported Extension Type: {file_extension}") raise UnsupportedFileTypeError(f"Unsupported Extension Type: {file_extension}")
@ -214,8 +223,8 @@ def _extract_text_from_doc(file_content: bytes) -> str:
""" """
from unstructured.partition.api import partition_via_api from unstructured.partition.api import partition_via_api
if not (dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY): if not dify_config.UNSTRUCTURED_API_URL:
raise TextExtractionError("UNSTRUCTURED_API_URL and UNSTRUCTURED_API_KEY must be set") raise TextExtractionError("UNSTRUCTURED_API_URL must be set")
try: try:
with tempfile.NamedTemporaryFile(suffix=".doc", delete=False) as temp_file: with tempfile.NamedTemporaryFile(suffix=".doc", delete=False) as temp_file:
@ -226,7 +235,7 @@ def _extract_text_from_doc(file_content: bytes) -> str:
file=file, file=file,
metadata_filename=temp_file.name, metadata_filename=temp_file.name,
api_url=dify_config.UNSTRUCTURED_API_URL, api_url=dify_config.UNSTRUCTURED_API_URL,
api_key=dify_config.UNSTRUCTURED_API_KEY, api_key=dify_config.UNSTRUCTURED_API_KEY, # type: ignore
) )
os.unlink(temp_file.name) os.unlink(temp_file.name)
return "\n".join([getattr(element, "text", "") for element in elements]) return "\n".join([getattr(element, "text", "") for element in elements])
@ -462,3 +471,68 @@ def _extract_text_from_msg(file_content: bytes) -> str:
return "\n".join([str(element) for element in elements]) return "\n".join([str(element) for element in elements])
except Exception as e: except Exception as e:
raise TextExtractionError(f"Failed to extract text from MSG: {str(e)}") from e raise TextExtractionError(f"Failed to extract text from MSG: {str(e)}") from e
def _extract_text_from_vtt(vtt_bytes: bytes) -> str:
text = _extract_text_from_plain_text(vtt_bytes)
# remove bom
text = text.lstrip("\ufeff")
raw_results = []
for caption in webvtt.from_string(text):
raw_results.append((caption.voice, caption.text))
# Merge consecutive utterances by the same speaker
merged_results = []
if raw_results:
current_speaker, current_text = raw_results[0]
for i in range(1, len(raw_results)):
spk, txt = raw_results[i]
if spk == None:
merged_results.append((None, current_text))
continue
if spk == current_speaker:
# If it is the same speaker, merge the utterances (joined by space)
current_text += " " + txt
else:
# If the speaker changes, register the utterance so far and move on
merged_results.append((current_speaker, current_text))
current_speaker, current_text = spk, txt
# Add the last element
merged_results.append((current_speaker, current_text))
else:
merged_results = raw_results
# Return the result in the specified format: Speaker "text" style
formatted = [f'{spk or ""} "{txt}"' for spk, txt in merged_results]
return "\n".join(formatted)
def _extract_text_from_properties(file_content: bytes) -> str:
try:
text = _extract_text_from_plain_text(file_content)
lines = text.splitlines()
result = []
for line in lines:
line = line.strip()
# Preserve comments and empty lines
if not line or line.startswith("#") or line.startswith("!"):
result.append(line)
continue
if "=" in line:
key, value = line.split("=", 1)
elif ":" in line:
key, value = line.split(":", 1)
else:
key, value = line, ""
result.append(f"{key.strip()}: {value.strip()}")
return "\n".join(result)
except Exception as e:
raise TextExtractionError(f"Failed to extract text from properties file: {str(e)}") from e

@ -6,8 +6,8 @@ for accessing and manipulating data, regardless of the underlying
storage mechanism. storage mechanism.
""" """
from core.repository.repository_factory import RepositoryFactory from core.workflow.repository.repository_factory import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
__all__ = [ __all__ = [
"RepositoryFactory", "RepositoryFactory",

@ -8,7 +8,7 @@ It does not contain any implementation details or dependencies on specific repos
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from typing import Any, Literal, Optional, cast from typing import Any, Literal, Optional, cast
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
# Type for factory functions - takes a dict of parameters and returns any repository type # Type for factory functions - takes a dict of parameters and returns any repository type
RepositoryFactoryFunc = Callable[[Mapping[str, Any]], Any] RepositoryFactoryFunc = Callable[[Mapping[str, Any]], Any]

@ -20,7 +20,8 @@ if [[ "${MODE}" == "worker" ]]; then
CONCURRENCY_OPTION="-c ${CELERY_WORKER_AMOUNT:-1}" CONCURRENCY_OPTION="-c ${CELERY_WORKER_AMOUNT:-1}"
fi fi
exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION --loglevel ${LOG_LEVEL:-INFO} \ exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION \
--max-tasks-per-child ${MAX_TASK_PRE_CHILD:-50} --loglevel ${LOG_LEVEL:-INFO} \
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion} -Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion}
elif [[ "${MODE}" == "beat" ]]; then elif [[ "${MODE}" == "beat" ]]; then

@ -8,34 +8,76 @@ from typing import Union
from celery.signals import worker_init # type: ignore from celery.signals import worker_init # type: ignore
from flask_login import user_loaded_from_request, user_logged_in # type: ignore from flask_login import user_loaded_from_request, user_logged_in # type: ignore
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.metrics import get_meter, get_meter_provider, set_meter_provider
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3Format
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
ConsoleSpanExporter,
)
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import Span, get_current_span, get_tracer_provider, set_tracer_provider
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.trace.status import StatusCode
from configs import dify_config from configs import dify_config
from dify_app import DifyApp from dify_app import DifyApp
@user_logged_in.connect
@user_loaded_from_request.connect
def on_user_loaded(_sender, user):
if dify_config.ENABLE_OTEL:
from opentelemetry.trace import get_current_span
if user:
current_span = get_current_span()
if current_span:
current_span.set_attribute("service.tenant.id", user.current_tenant_id)
current_span.set_attribute("service.user.id", user.id)
def init_app(app: DifyApp):
def is_celery_worker():
return "celery" in sys.argv[0].lower()
def instrument_exception_logging():
exception_handler = ExceptionLoggingHandler()
logging.getLogger().addHandler(exception_handler)
def init_flask_instrumentor(app: DifyApp):
meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION)
_http_response_counter = meter.create_counter(
"http.server.response.count", description="Total number of HTTP responses by status code", unit="{response}"
)
def response_hook(span: Span, status: str, response_headers: list):
if span and span.is_recording():
if status.startswith("2"):
span.set_status(StatusCode.OK)
else:
span.set_status(StatusCode.ERROR, status)
status = status.split(" ")[0]
status_code = int(status)
status_class = f"{status_code // 100}xx"
_http_response_counter.add(1, {"status_code": status_code, "status_class": status_class})
instrumentor = FlaskInstrumentor()
if dify_config.DEBUG:
logging.info("Initializing Flask instrumentor")
instrumentor.instrument_app(app, response_hook=response_hook)
def init_sqlalchemy_instrumentor(app: DifyApp):
with app.app_context():
engines = list(app.extensions["sqlalchemy"].engines.values())
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
def setup_context_propagation():
# Configure propagators
set_global_textmap(
CompositePropagator(
[
TraceContextTextMapPropagator(), # W3C trace context
B3Format(), # B3 propagation (used by many systems)
]
)
)
def shutdown_tracer():
provider = trace.get_tracer_provider()
if hasattr(provider, "force_flush"):
provider.force_flush()
class ExceptionLoggingHandler(logging.Handler): class ExceptionLoggingHandler(logging.Handler):
"""Custom logging handler that creates spans for logging.exception() calls""" """Custom logging handler that creates spans for logging.exception() calls"""
@ -60,19 +102,30 @@ class ExceptionLoggingHandler(logging.Handler):
except Exception: except Exception:
pass pass
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.metrics import get_meter, get_meter_provider, set_meter_provider
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3Format
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
ConsoleSpanExporter,
)
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import Span, get_tracer_provider, set_tracer_provider
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.trace.status import StatusCode
@user_logged_in.connect
@user_loaded_from_request.connect
def on_user_loaded(_sender, user):
if user:
current_span = get_current_span()
if current_span:
current_span.set_attribute("service.tenant.id", user.current_tenant_id)
current_span.set_attribute("service.user.id", user.id)
def init_app(app: DifyApp):
if dify_config.ENABLE_OTEL:
setup_context_propagation() setup_context_propagation()
# Initialize OpenTelemetry # Initialize OpenTelemetry
# Follow Semantic Convertions 1.32.0 to define resource attributes # Follow Semantic Convertions 1.32.0 to define resource attributes
@ -133,67 +186,19 @@ def init_app(app: DifyApp):
atexit.register(shutdown_tracer) atexit.register(shutdown_tracer)
def is_celery_worker(): def is_enabled():
return "celery" in sys.argv[0].lower() return dify_config.ENABLE_OTEL
def instrument_exception_logging():
exception_handler = ExceptionLoggingHandler()
logging.getLogger().addHandler(exception_handler)
def init_flask_instrumentor(app: DifyApp):
meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION)
_http_response_counter = meter.create_counter(
"http.server.response.count", description="Total number of HTTP responses by status code", unit="{response}"
)
def response_hook(span: Span, status: str, response_headers: list):
if span and span.is_recording():
if status.startswith("2"):
span.set_status(StatusCode.OK)
else:
span.set_status(StatusCode.ERROR, status)
status = status.split(" ")[0]
status_code = int(status)
status_class = f"{status_code // 100}xx"
_http_response_counter.add(1, {"status_code": status_code, "status_class": status_class})
instrumentor = FlaskInstrumentor()
if dify_config.DEBUG:
logging.info("Initializing Flask instrumentor")
instrumentor.instrument_app(app, response_hook=response_hook)
def init_sqlalchemy_instrumentor(app: DifyApp):
with app.app_context():
engines = list(app.extensions["sqlalchemy"].engines.values())
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
def setup_context_propagation():
# Configure propagators
set_global_textmap(
CompositePropagator(
[
TraceContextTextMapPropagator(), # W3C trace context
B3Format(), # B3 propagation (used by many systems)
]
)
)
@worker_init.connect(weak=False) @worker_init.connect(weak=False)
def init_celery_worker(*args, **kwargs): def init_celery_worker(*args, **kwargs):
if dify_config.ENABLE_OTEL:
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.metrics import get_meter_provider
from opentelemetry.trace import get_tracer_provider
tracer_provider = get_tracer_provider() tracer_provider = get_tracer_provider()
metric_provider = get_meter_provider() metric_provider = get_meter_provider()
if dify_config.DEBUG: if dify_config.DEBUG:
logging.info("Initializing OpenTelemetry for Celery worker") logging.info("Initializing OpenTelemetry for Celery worker")
CeleryInstrumentor(tracer_provider=tracer_provider, meter_provider=metric_provider).instrument() CeleryInstrumentor(tracer_provider=tracer_provider, meter_provider=metric_provider).instrument()
def shutdown_tracer():
provider = trace.get_tracer_provider()
if hasattr(provider, "force_flush"):
provider.force_flush()

@ -1,63 +0,0 @@
"""
Patch for OpenTelemetry context detach method to handle None tokens gracefully.
This patch addresses the issue where OpenTelemetry's context.detach() method raises a TypeError
when called with a None token. The error occurs in the contextvars_context.py file where it tries
to call reset() on a None token.
Related GitHub issue: https://github.com/langgenius/dify/issues/18496
Error being fixed:
```
Traceback (most recent call last):
File "opentelemetry/context/__init__.py", line 154, in detach
_RUNTIME_CONTEXT.detach(token)
File "opentelemetry/context/contextvars_context.py", line 50, in detach
self._current_context.reset(token) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: expected an instance of Token, got None
```
Instead of modifying the third-party package directly, this patch monkey-patches the
context.detach method to gracefully handle None tokens.
"""
import logging
from functools import wraps
from opentelemetry import context
logger = logging.getLogger(__name__)
# Store the original detach method
original_detach = context.detach
# Create a patched version that handles None tokens
@wraps(original_detach)
def patched_detach(token):
"""
A patched version of context.detach that handles None tokens gracefully.
"""
if token is None:
logger.debug("Attempted to detach a None token, skipping")
return
return original_detach(token)
def is_enabled():
"""
Check if the extension is enabled.
Always enable this patch to prevent errors even when OpenTelemetry is disabled.
"""
return True
def init_app(app):
"""
Initialize the OpenTelemetry context patch.
"""
# Replace the original detach method with our patched version
context.detach = patched_detach
logger.info("OpenTelemetry context.detach patched to handle None tokens")

@ -4,8 +4,8 @@ Extension for initializing repositories.
This extension registers repository implementations with the RepositoryFactory. This extension registers repository implementations with the RepositoryFactory.
""" """
from core.repositories.repository_registry import register_repositories
from dify_app import DifyApp from dify_app import DifyApp
from repositories.repository_registry import register_repositories
def init_app(_app: DifyApp) -> None: def init_app(_app: DifyApp) -> None:

@ -1010,7 +1010,9 @@ class Message(db.Model): # type: ignore[name-defined]
sign_url = file_helpers.get_signed_file_url(upload_file_id) sign_url = file_helpers.get_signed_file_url(upload_file_id)
else: else:
continue continue
# if as_attachment is in the url, add it to the sign_url.
if "as_attachment" in url:
sign_url += "&as_attachment=true"
re_sign_file_url_answer = re_sign_file_url_answer.replace(url, sign_url) re_sign_file_url_answer = re_sign_file_url_answer.replace(url, sign_url)
return re_sign_file_url_answer return re_sign_file_url_answer

@ -1,6 +1,6 @@
[project] [project]
name = "dify-api" name = "dify-api"
version = "1.3.0" dynamic = ["version"]
requires-python = ">=3.11,<3.13" requires-python = ">=3.11,<3.13"
dependencies = [ dependencies = [
@ -84,12 +84,17 @@ dependencies = [
"validators==0.21.0", "validators==0.21.0",
"weave~=0.51.34", "weave~=0.51.34",
"yarl~=1.18.3", "yarl~=1.18.3",
"webvtt-py~=0.5.1",
] ]
# Before adding new dependency, consider place it in # Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group. # alphabet order (a-z) and suitable group.
[tool.setuptools]
packages = []
[tool.uv] [tool.uv]
default-groups = ["storage", "tools", "vdb"] default-groups = ["storage", "tools", "vdb"]
package = false
[dependency-groups] [dependency-groups]

@ -2,8 +2,8 @@ import threading
from typing import Optional from typing import Optional
import contexts import contexts
from core.repository import RepositoryFactory from core.workflow.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import OrderConfig from core.workflow.repository.workflow_node_execution_repository import OrderConfig
from extensions.ext_database import db from extensions.ext_database import db
from libs.infinite_scroll_pagination import InfiniteScrollPagination from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models.enums import WorkflowRunTriggeredFrom from models.enums import WorkflowRunTriggeredFrom

@ -11,7 +11,6 @@ from sqlalchemy.orm import Session
from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager
from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.repository import RepositoryFactory
from core.variables import Variable from core.variables import Variable
from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.errors import WorkflowNodeRunFailedError from core.workflow.errors import WorkflowNodeRunFailedError
@ -22,6 +21,7 @@ from core.workflow.nodes.enums import ErrorStrategy
from core.workflow.nodes.event import RunCompletedEvent from core.workflow.nodes.event import RunCompletedEvent
from core.workflow.nodes.event.types import NodeEvent from core.workflow.nodes.event.types import NodeEvent
from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING
from core.workflow.repository import RepositoryFactory
from core.workflow.workflow_entry import WorkflowEntry from core.workflow.workflow_entry import WorkflowEntry
from events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated from events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated
from extensions.ext_database import db from extensions.ext_database import db

@ -7,7 +7,7 @@ from celery import shared_task # type: ignore
from sqlalchemy import delete from sqlalchemy import delete
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from core.repository import RepositoryFactory from core.workflow.repository import RepositoryFactory
from extensions.ext_database import db from extensions.ext_database import db
from models.dataset import AppDatasetJoin from models.dataset import AppDatasetJoin
from models.model import ( from models.model import (

@ -23,13 +23,70 @@ def setup_mock_redis():
ext_redis.redis_client.lock = MagicMock(return_value=mock_redis_lock) ext_redis.redis_client.lock = MagicMock(return_value=mock_redis_lock)
class TestOpenSearchConfig:
def test_to_opensearch_params(self):
config = OpenSearchConfig(
host="localhost",
port=9200,
secure=True,
user="admin",
password="password",
)
params = config.to_opensearch_params()
assert params["hosts"] == [{"host": "localhost", "port": 9200}]
assert params["use_ssl"] is True
assert params["verify_certs"] is True
assert params["connection_class"].__name__ == "Urllib3HttpConnection"
assert params["http_auth"] == ("admin", "password")
@patch("boto3.Session")
@patch("core.rag.datasource.vdb.opensearch.opensearch_vector.Urllib3AWSV4SignerAuth")
def test_to_opensearch_params_with_aws_managed_iam(
self, mock_aws_signer_auth: MagicMock, mock_boto_session: MagicMock
):
mock_credentials = MagicMock()
mock_boto_session.return_value.get_credentials.return_value = mock_credentials
mock_auth_instance = MagicMock()
mock_aws_signer_auth.return_value = mock_auth_instance
aws_region = "ap-southeast-2"
aws_service = "aoss"
host = f"aoss-endpoint.{aws_region}.aoss.amazonaws.com"
port = 9201
config = OpenSearchConfig(
host=host,
port=port,
secure=True,
auth_method="aws_managed_iam",
aws_region=aws_region,
aws_service=aws_service,
)
params = config.to_opensearch_params()
assert params["hosts"] == [{"host": host, "port": port}]
assert params["use_ssl"] is True
assert params["verify_certs"] is True
assert params["connection_class"].__name__ == "Urllib3HttpConnection"
assert params["http_auth"] is mock_auth_instance
mock_aws_signer_auth.assert_called_once_with(
credentials=mock_credentials, region=aws_region, service=aws_service
)
assert mock_boto_session.return_value.get_credentials.called
class TestOpenSearchVector: class TestOpenSearchVector:
def setup_method(self): def setup_method(self):
self.collection_name = "test_collection" self.collection_name = "test_collection"
self.example_doc_id = "example_doc_id" self.example_doc_id = "example_doc_id"
self.vector = OpenSearchVector( self.vector = OpenSearchVector(
collection_name=self.collection_name, collection_name=self.collection_name,
config=OpenSearchConfig(host="localhost", port=9200, user="admin", password="password", secure=False), config=OpenSearchConfig(host="localhost", port=9200, secure=False, user="admin", password="password"),
) )
self.vector._client = MagicMock() self.vector._client = MagicMock()

@ -864,10 +864,11 @@ def test_condition_parallel_correct_output(mock_close, mock_remove, app):
with patch.object(CodeNode, "_run", new=code_generator): with patch.object(CodeNode, "_run", new=code_generator):
generator = graph_engine.run() generator = graph_engine.run()
stream_content = "" stream_content = ""
res_content = "VAT:\ndify 123" wrong_content = ["Stamp Duty", "other"]
for item in generator: for item in generator:
if isinstance(item, NodeRunStreamChunkEvent): if isinstance(item, NodeRunStreamChunkEvent):
stream_content += f"{item.chunk_content}\n" stream_content += f"{item.chunk_content}\n"
if isinstance(item, GraphRunSucceededEvent): if isinstance(item, GraphRunSucceededEvent):
assert item.outputs == {"answer": res_content} assert item.outputs is not None
assert stream_content == res_content + "\n" answer = item.outputs["answer"]
assert all(rc not in answer for rc in wrong_content)

@ -8,9 +8,9 @@ import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.orm import Session, sessionmaker
from core.repository.workflow_node_execution_repository import OrderConfig from core.repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.repository.workflow_node_execution_repository import OrderConfig
from models.workflow import WorkflowNodeExecution from models.workflow import WorkflowNodeExecution
from repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
@pytest.fixture @pytest.fixture
@ -80,7 +80,7 @@ def test_get_by_node_execution_id(repository, session, mocker: MockerFixture):
"""Test get_by_node_execution_id method.""" """Test get_by_node_execution_id method."""
session_obj, _ = session session_obj, _ = session
# Set up mock # Set up mock
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select") mock_select = mocker.patch("core.repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_stmt = mocker.MagicMock() mock_stmt = mocker.MagicMock()
mock_select.return_value = mock_stmt mock_select.return_value = mock_stmt
mock_stmt.where.return_value = mock_stmt mock_stmt.where.return_value = mock_stmt
@ -99,7 +99,7 @@ def test_get_by_workflow_run(repository, session, mocker: MockerFixture):
"""Test get_by_workflow_run method.""" """Test get_by_workflow_run method."""
session_obj, _ = session session_obj, _ = session
# Set up mock # Set up mock
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select") mock_select = mocker.patch("core.repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_stmt = mocker.MagicMock() mock_stmt = mocker.MagicMock()
mock_select.return_value = mock_stmt mock_select.return_value = mock_stmt
mock_stmt.where.return_value = mock_stmt mock_stmt.where.return_value = mock_stmt
@ -120,7 +120,7 @@ def test_get_running_executions(repository, session, mocker: MockerFixture):
"""Test get_running_executions method.""" """Test get_running_executions method."""
session_obj, _ = session session_obj, _ = session
# Set up mock # Set up mock
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select") mock_select = mocker.patch("core.repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_stmt = mocker.MagicMock() mock_stmt = mocker.MagicMock()
mock_select.return_value = mock_stmt mock_select.return_value = mock_stmt
mock_stmt.where.return_value = mock_stmt mock_stmt.where.return_value = mock_stmt
@ -158,7 +158,7 @@ def test_clear(repository, session, mocker: MockerFixture):
"""Test clear method.""" """Test clear method."""
session_obj, _ = session session_obj, _ = session
# Set up mock # Set up mock
mock_delete = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.delete") mock_delete = mocker.patch("core.repositories.workflow_node_execution.sqlalchemy_repository.delete")
mock_stmt = mocker.MagicMock() mock_stmt = mocker.MagicMock()
mock_delete.return_value = mock_stmt mock_delete.return_value = mock_stmt
mock_stmt.where.return_value = mock_stmt mock_stmt.where.return_value = mock_stmt

@ -1155,7 +1155,6 @@ wheels = [
[[package]] [[package]]
name = "dify-api" name = "dify-api"
version = "1.3.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "authlib" }, { name = "authlib" },
@ -1235,6 +1234,7 @@ dependencies = [
{ name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] }, { name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] },
{ name = "validators" }, { name = "validators" },
{ name = "weave" }, { name = "weave" },
{ name = "webvtt-py" },
{ name = "yarl" }, { name = "yarl" },
] ]
@ -1405,6 +1405,7 @@ requires-dist = [
{ name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" }, { name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" },
{ name = "validators", specifier = "==0.21.0" }, { name = "validators", specifier = "==0.21.0" },
{ name = "weave", specifier = "~=0.51.34" }, { name = "weave", specifier = "~=0.51.34" },
{ name = "webvtt-py", specifier = "~=0.5.1" },
{ name = "yarl", specifier = "~=1.18.3" }, { name = "yarl", specifier = "~=1.18.3" },
] ]
@ -6282,6 +6283,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 }, { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 },
] ]
[[package]]
name = "webvtt-py"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/f6/7c9c964681fb148e0293e6860108d378e09ccab2218f9063fd3eb87f840a/webvtt-py-0.5.1.tar.gz", hash = "sha256:2040dd325277ddadc1e0c6cc66cbc4a1d9b6b49b24c57a0c3364374c3e8a3dc1", size = 55128 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/ed/aad7e0f5a462d679f7b4d2e0d8502c3096740c883b5bbed5103146480937/webvtt_py-0.5.1-py3-none-any.whl", hash = "sha256:9d517d286cfe7fc7825e9d4e2079647ce32f5678eb58e39ef544ffbb932610b7", size = 19802 },
]
[[package]] [[package]]
name = "werkzeug" name = "werkzeug"
version = "3.1.3" version = "3.1.3"

@ -39,6 +39,12 @@ APP_WEB_URL=
# File preview or download Url prefix. # File preview or download Url prefix.
# used to display File preview or download Url to the front-end or as Multi-model inputs; # used to display File preview or download Url to the front-end or as Multi-model inputs;
# Url is signed and has expiration time. # Url is signed and has expiration time.
# Setting FILES_URL is required for file processing plugins.
# - For https://example.com, use FILES_URL=https://example.com
# - For http://example.com, use FILES_URL=http://example.com
# Recommendation: use a dedicated domain (e.g., https://upload.example.com).
# Alternatively, use http://<your-ip>:5001 or http://api:5001,
# ensuring port 5001 is externally accessible (see docker-compose.yaml).
FILES_URL= FILES_URL=
# ------------------------------ # ------------------------------
@ -520,9 +526,13 @@ RELYT_DATABASE=postgres
# open search configuration, only available when VECTOR_STORE is `opensearch` # open search configuration, only available when VECTOR_STORE is `opensearch`
OPENSEARCH_HOST=opensearch OPENSEARCH_HOST=opensearch
OPENSEARCH_PORT=9200 OPENSEARCH_PORT=9200
OPENSEARCH_SECURE=true
OPENSEARCH_AUTH_METHOD=basic
OPENSEARCH_USER=admin OPENSEARCH_USER=admin
OPENSEARCH_PASSWORD=admin OPENSEARCH_PASSWORD=admin
OPENSEARCH_SECURE=true # If using AWS managed IAM, e.g. Managed Cluster or OpenSearch Serverless
OPENSEARCH_AWS_REGION=ap-southeast-1
OPENSEARCH_AWS_SERVICE=aoss
# tencent vector configurations, only available when VECTOR_STORE is `tencent` # tencent vector configurations, only available when VECTOR_STORE is `tencent`
TENCENT_VECTOR_DB_URL=http://127.0.0.1 TENCENT_VECTOR_DB_URL=http://127.0.0.1

@ -14,7 +14,6 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
- **Unified Vector Database Services**: All vector database services are now managed from a single Docker Compose file `docker-compose.yaml`. You can switch between different vector databases by setting the `VECTOR_STORE` environment variable in your `.env` file. - **Unified Vector Database Services**: All vector database services are now managed from a single Docker Compose file `docker-compose.yaml`. You can switch between different vector databases by setting the `VECTOR_STORE` environment variable in your `.env` file.
- **Mandatory .env File**: A `.env` file is now required to run `docker compose up`. This file is crucial for configuring your deployment and for any custom settings to persist through upgrades. - **Mandatory .env File**: A `.env` file is now required to run `docker compose up`. This file is crucial for configuring your deployment and for any custom settings to persist through upgrades.
- **Legacy Support**: Previous deployment files are now located in the `docker-legacy` directory and will no longer be maintained.
### How to Deploy Dify with `docker-compose.yaml` ### How to Deploy Dify with `docker-compose.yaml`

@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services: services:
# API service # API service
api: api:
image: langgenius/dify-api:1.3.0 image: langgenius/dify-api:1.3.1
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@ -31,7 +31,7 @@ services:
# worker service # worker service
# The Celery worker for processing the queue. # The Celery worker for processing the queue.
worker: worker:
image: langgenius/dify-api:1.3.0 image: langgenius/dify-api:1.3.1
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@ -57,7 +57,7 @@ services:
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:1.3.0 image: langgenius/dify-web:1.3.1
restart: always restart: always
environment: environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-} CONSOLE_API_URL: ${CONSOLE_API_URL:-}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 170 KiB

@ -225,9 +225,12 @@ x-shared-env: &shared-api-worker-env
RELYT_DATABASE: ${RELYT_DATABASE:-postgres} RELYT_DATABASE: ${RELYT_DATABASE:-postgres}
OPENSEARCH_HOST: ${OPENSEARCH_HOST:-opensearch} OPENSEARCH_HOST: ${OPENSEARCH_HOST:-opensearch}
OPENSEARCH_PORT: ${OPENSEARCH_PORT:-9200} OPENSEARCH_PORT: ${OPENSEARCH_PORT:-9200}
OPENSEARCH_SECURE: ${OPENSEARCH_SECURE:-true}
OPENSEARCH_AUTH_METHOD: ${OPENSEARCH_AUTH_METHOD:-basic}
OPENSEARCH_USER: ${OPENSEARCH_USER:-admin} OPENSEARCH_USER: ${OPENSEARCH_USER:-admin}
OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD:-admin} OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD:-admin}
OPENSEARCH_SECURE: ${OPENSEARCH_SECURE:-true} OPENSEARCH_AWS_REGION: ${OPENSEARCH_AWS_REGION:-ap-southeast-1}
OPENSEARCH_AWS_SERVICE: ${OPENSEARCH_AWS_SERVICE:-aoss}
TENCENT_VECTOR_DB_URL: ${TENCENT_VECTOR_DB_URL:-http://127.0.0.1} TENCENT_VECTOR_DB_URL: ${TENCENT_VECTOR_DB_URL:-http://127.0.0.1}
TENCENT_VECTOR_DB_API_KEY: ${TENCENT_VECTOR_DB_API_KEY:-dify} TENCENT_VECTOR_DB_API_KEY: ${TENCENT_VECTOR_DB_API_KEY:-dify}
TENCENT_VECTOR_DB_TIMEOUT: ${TENCENT_VECTOR_DB_TIMEOUT:-30} TENCENT_VECTOR_DB_TIMEOUT: ${TENCENT_VECTOR_DB_TIMEOUT:-30}
@ -488,7 +491,7 @@ x-shared-env: &shared-api-worker-env
services: services:
# API service # API service
api: api:
image: langgenius/dify-api:1.3.0 image: langgenius/dify-api:1.3.1
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@ -517,7 +520,7 @@ services:
# worker service # worker service
# The Celery worker for processing the queue. # The Celery worker for processing the queue.
worker: worker:
image: langgenius/dify-api:1.3.0 image: langgenius/dify-api:1.3.1
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@ -543,7 +546,7 @@ services:
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:1.3.0 image: langgenius/dify-web:1.3.1
restart: always restart: always
environment: environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-} CONSOLE_API_URL: ${CONSOLE_API_URL:-}

@ -1,6 +1,6 @@
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { RiResetLeftLine } from '@remixicon/react' import { RiCollapseDiagonal2Line, RiExpandDiagonal2Line, RiResetLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { Theme } from '../theme/theme-context' import type { Theme } from '../theme/theme-context'
import { CssTransform } from '../theme/utils' import { CssTransform } from '../theme/utils'
@ -36,6 +36,44 @@ const Header: FC<IHeaderProps> = ({
currentConversationId, currentConversationId,
inputsForms, inputsForms,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()
const isClient = typeof window !== 'undefined'
const isIframe = isClient ? window.self !== window.top : false
const [parentOrigin, setParentOrigin] = useState('')
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)
const [expanded, setExpanded] = useState(false)
const handleMessageReceived = useCallback((event: MessageEvent) => {
let currentParentOrigin = parentOrigin
if (!currentParentOrigin && event.data.type === 'dify-chatbot-config') {
currentParentOrigin = event.origin
setParentOrigin(event.origin)
}
if (event.origin !== currentParentOrigin)
return
if (event.data.type === 'dify-chatbot-config')
setShowToggleExpandButton(event.data.payload.isToggledByButton && !event.data.payload.isDraggable)
}, [parentOrigin])
useEffect(() => {
if (!isIframe) return
const listener = (event: MessageEvent) => handleMessageReceived(event)
window.addEventListener('message', listener)
window.parent.postMessage({ type: 'dify-chatbot-iframe-ready' }, '*')
return () => window.removeEventListener('message', listener)
}, [isIframe, handleMessageReceived])
const handleToggleExpand = useCallback(() => {
if (!isIframe || !showToggleExpandButton) return
setExpanded(!expanded)
window.parent.postMessage({
type: 'dify-chatbot-expand-change',
}, parentOrigin)
}, [isIframe, parentOrigin, showToggleExpandButton, expanded])
if (!isMobile) { if (!isMobile) {
return ( return (
<div className='flex h-14 shrink-0 items-center justify-end p-3'> <div className='flex h-14 shrink-0 items-center justify-end p-3'>
@ -59,6 +97,21 @@ const Header: FC<IHeaderProps> = ({
{currentConversationId && ( {currentConversationId && (
<Divider type='vertical' className='h-3.5' /> <Divider type='vertical' className='h-3.5' />
)} )}
{
showToggleExpandButton && (
<Tooltip
popupContent={expanded ? t('share.chat.collapse') : t('share.chat.expand')}
>
<ActionButton size='l' onClick={handleToggleExpand}>
{
expanded
? <RiCollapseDiagonal2Line className='h-[18px] w-[18px]' />
: <RiExpandDiagonal2Line className='h-[18px] w-[18px]' />
}
</ActionButton>
</Tooltip>
)
}
{currentConversationId && allowResetChat && ( {currentConversationId && allowResetChat && (
<Tooltip <Tooltip
popupContent={t('share.chat.resetChat')} popupContent={t('share.chat.resetChat')}
@ -91,6 +144,21 @@ const Header: FC<IHeaderProps> = ({
</div> </div>
</div> </div>
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
{
showToggleExpandButton && (
<Tooltip
popupContent={expanded ? t('share.chat.collapse') : t('share.chat.expand')}
>
<ActionButton size='l' onClick={handleToggleExpand}>
{
expanded
? <RiCollapseDiagonal2Line className={cn('h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
: <RiExpandDiagonal2Line className={cn('h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
}
</ActionButton>
</Tooltip>
)
}
{currentConversationId && allowResetChat && ( {currentConversationId && allowResetChat && (
<Tooltip <Tooltip
popupContent={t('share.chat.resetChat')} popupContent={t('share.chat.resetChat')}

@ -22,10 +22,6 @@ export function preprocessMermaidCode(code: string): string {
.replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}`) .replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}`)
// Fix common syntax issues // Fix common syntax issues
.replace(/fifopacket/g, 'rect') .replace(/fifopacket/g, 'rect')
// Ensure graph has direction
.replace(/^graph\s+((?:TB|BT|RL|LR)*)/, (match, direction) => {
return direction ? match : 'graph TD'
})
// Clean up empty lines and extra spaces // Clean up empty lines and extra spaces
.trim() .trim()
} }

@ -196,22 +196,68 @@ const FileUploader = ({
e.stopPropagation() e.stopPropagation()
e.target === dragRef.current && setDragging(false) e.target === dragRef.current && setDragging(false)
} }
type FileWithPath = {
relativePath?: string
} & File
const traverseFileEntry = useCallback(
(entry: any, prefix = ''): Promise<FileWithPath[]> => {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file((file: FileWithPath) => {
file.relativePath = `${prefix}${file.name}`
resolve([file])
})
}
else if (entry.isDirectory) {
const reader = entry.createReader()
const entries: any[] = []
const read = () => {
reader.readEntries(async (results: FileSystemEntry[]) => {
if (!results.length) {
const files = await Promise.all(
entries.map(ent =>
traverseFileEntry(ent, `${prefix}${entry.name}/`),
),
)
resolve(files.flat())
}
else {
entries.push(...results)
read()
}
})
}
read()
}
else {
resolve([])
}
})
},
[],
)
const handleDrop = useCallback((e: DragEvent) => { const handleDrop = useCallback(
async (e: DragEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setDragging(false) setDragging(false)
if (!e.dataTransfer) if (!e.dataTransfer) return
return const nested = await Promise.all(
Array.from(e.dataTransfer.items).map((it) => {
let files = [...e.dataTransfer.files] as File[] const entry = (it as any).webkitGetAsEntry?.()
if (notSupportBatchUpload) if (entry) return traverseFileEntry(entry)
files = files.slice(0, 1) const f = it.getAsFile?.()
return f ? Promise.resolve([f]) : Promise.resolve([])
const validFiles = files.filter(isValid) }),
initialUpload(validFiles) )
}, [initialUpload, isValid, notSupportBatchUpload]) let files = nested.flat()
if (notSupportBatchUpload) files = files.slice(0, 1)
const valid = files.filter(isValid)
initialUpload(valid)
},
[initialUpload, isValid, notSupportBatchUpload, traverseFileEntry],
)
const selectHandle = () => { const selectHandle = () => {
if (fileUploader.current) if (fileUploader.current)
fileUploader.current.click() fileUploader.current.click()

@ -17,6 +17,7 @@ type Props = {
const NoData: FC<Props> = ({ const NoData: FC<Props> = ({
onConfig, onConfig,
provider,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -38,7 +39,7 @@ const NoData: FC<Props> = ({
} : null, } : null,
} }
const currentProvider = Object.values(providerConfig).find(provider => provider !== null) || providerConfig[DataSourceProvider.jinaReader] const currentProvider = providerConfig[provider] || providerConfig[DataSourceProvider.jinaReader]
if (!currentProvider) return null if (!currentProvider) return null

@ -49,7 +49,7 @@ const Header = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSegment]) }, [selectedSegment])
return ( return (
<div className='flex flex-1 items-center justify-between bg-background-body px-4'> <div className='relative flex flex-1 items-center justify-between bg-background-body'>
<div className='flex items-center'> <div className='flex items-center'>
{isMobile && <div {isMobile && <div
className='flex h-8 w-8 cursor-pointer items-center justify-center' className='flex h-8 w-8 cursor-pointer items-center justify-center'
@ -59,7 +59,7 @@ const Header = () => {
</div>} </div>}
{ {
!isMobile !isMobile
&& <div className='flex w-64 shrink-0 items-center gap-1.5 self-stretch p-2 pl-3'> && <div className='flex shrink-0 items-center gap-1.5 self-stretch pl-3'>
<Link href="/apps" className='flex h-8 w-8 shrink-0 items-center justify-center gap-2'> <Link href="/apps" className='flex h-8 w-8 shrink-0 items-center justify-center gap-2'>
<LogoSite className='object-contain' /> <LogoSite className='object-contain' />
</Link> </Link>
@ -84,7 +84,7 @@ const Header = () => {
)} )}
{ {
!isMobile && ( !isMobile && (
<div className='flex items-center'> <div className='absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center'>
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />} {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />} {!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />} {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
@ -92,7 +92,7 @@ const Header = () => {
</div> </div>
) )
} }
<div className='flex shrink-0 items-center'> <div className='flex shrink-0 items-center pr-3'>
<EnvNav /> <EnvNav />
<div className='mr-2'> <div className='mr-2'>
<PluginsNav /> <PluginsNav />

@ -6,7 +6,7 @@ import {
RiArrowDownSLine, RiArrowDownSLine,
RiArrowRightSLine, RiArrowRightSLine,
} from '@remixicon/react' } from '@remixicon/react'
import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react' import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -77,7 +77,7 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
<div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}> <div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}>
{ {
navs.map(nav => ( navs.map(nav => (
<MenuItems key={nav.id}> <MenuItem key={nav.id}>
<div className='flex w-full cursor-pointer items-center truncate rounded-lg px-3 py-[6px] text-[14px] font-normal text-gray-700 hover:bg-gray-100' onClick={() => { <div className='flex w-full cursor-pointer items-center truncate rounded-lg px-3 py-[6px] text-[14px] font-normal text-gray-700 hover:bg-gray-100' onClick={() => {
if (curNav?.id === nav.id) if (curNav?.id === nav.id)
return return
@ -112,12 +112,12 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
{nav.name} {nav.name}
</div> </div>
</div> </div>
</MenuItems> </MenuItem>
)) ))
} }
</div> </div>
{!isApp && isCurrentWorkspaceEditor && ( {!isApp && isCurrentWorkspaceEditor && (
<MenuButton className='w-full p-1'> <MenuItem as="div" className='w-full p-1'>
<div onClick={() => onCreate('')} className={cn( <div onClick={() => onCreate('')} className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-gray-100', 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-gray-100',
)}> )}>
@ -126,7 +126,7 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
</div> </div>
<div className='grow text-left text-[14px] font-normal text-gray-700'>{createText}</div> <div className='grow text-left text-[14px] font-normal text-gray-700'>{createText}</div>
</div> </div>
</MenuButton> </MenuItem>
)} )}
{isApp && isCurrentWorkspaceEditor && ( {isApp && isCurrentWorkspaceEditor && (
<Menu as="div" className="relative h-full w-full"> <Menu as="div" className="relative h-full w-full">

@ -596,17 +596,16 @@ const getIterationItemType = ({
arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type
} }
else { else {
(valueSelector).slice(1).forEach((key, i) => { for (let i = 1; i < valueSelector.length - 1; i++) {
const key = valueSelector[i]
const isLast = i === valueSelector.length - 2 const isLast = i === valueSelector.length - 2
curr = curr?.find((v: any) => v.variable === key) curr = Array.isArray(curr) ? curr.find(v => v.variable === key) : []
if (isLast) {
if (isLast)
arrayType = curr?.type arrayType = curr?.type
else if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children || []
} }
else {
if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children
}
})
} }
switch (arrayType as VarType) { switch (arrayType as VarType) {
@ -631,7 +630,7 @@ const getLoopItemType = ({
}: { }: {
valueSelector: ValueSelector valueSelector: ValueSelector
beforeNodesOutputVars: NodeOutPutVar[] beforeNodesOutputVars: NodeOutPutVar[]
// eslint-disable-next-line sonarjs/no-identical-functions
}): VarType => { }): VarType => {
const outputVarNodeId = valueSelector[0] const outputVarNodeId = valueSelector[0]
const isSystem = isSystemVar(valueSelector) const isSystem = isSystemVar(valueSelector)

@ -296,7 +296,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
onCollapse={setStructuredOutputCollapsed} onCollapse={setStructuredOutputCollapsed}
operations={ operations={
<div className='mr-4 flex shrink-0 items-center'> <div className='mr-4 flex shrink-0 items-center'>
{!isModelSupportStructuredOutput && ( {(!isModelSupportStructuredOutput && !!inputs.structured_output_enabled) && (
<Tooltip noDecoration popupContent={ <Tooltip noDecoration popupContent={
<div className='w-[232px] rounded-xl border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-4 py-3.5 shadow-lg backdrop-blur-[5px]'> <div className='w-[232px] rounded-xl border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-4 py-3.5 shadow-lg backdrop-blur-[5px]'>
<div className='title-xs-semi-bold text-text-primary'>{t('app.structOutput.modelNotSupported')}</div> <div className='title-xs-semi-bold text-text-primary'>{t('app.structOutput.modelNotSupported')}</div>

@ -63,7 +63,7 @@ const ClassList: FC<Props> = ({
return ( return (
<Item <Item
nodeId={nodeId} nodeId={nodeId}
key={index} key={list[index].id}
payload={item} payload={item}
onChange={handleClassChange(index)} onChange={handleClassChange(index)}
onRemove={handleRemoveClass(index)} onRemove={handleRemoveClass(index)}

@ -32,4 +32,7 @@ export NEXT_PUBLIC_MAX_TOOLS_NUM=${MAX_TOOLS_NUM}
export NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER=${ENABLE_WEBSITE_JINAREADER:-true} export NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER=${ENABLE_WEBSITE_JINAREADER:-true}
export NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL=${ENABLE_WEBSITE_FIRECRAWL:-true} export NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL=${ENABLE_WEBSITE_FIRECRAWL:-true}
export NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL=${ENABLE_WEBSITE_WATERCRAWL:-true} export NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL=${ENABLE_WEBSITE_WATERCRAWL:-true}
export NEXT_PUBLIC_LOOP_NODE_MAX_COUNT=${LOOP_NODE_MAX_COUNT}
export NEXT_PUBLIC_MAX_PARALLEL_LIMIT=${MAX_PARALLEL_LIMIT}
export NEXT_PUBLIC_MAX_ITERATIONS_NUM=${MAX_ITERATIONS_NUM}
pm2 start /app/web/server.js --name dify-web --cwd /app/web -i ${PM2_INSTANCES} --no-daemon pm2 start /app/web/server.js --name dify-web --cwd /app/web -i ${PM2_INSTANCES} --no-daemon

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Textdatei hochladen', title: 'Textdatei hochladen',
button: 'Datei hierher ziehen oder', button: 'Dateien und Ordner hierher ziehen oder klicken',
browse: 'Durchsuchen', browse: 'Durchsuchen',
tip: 'Unterstützt {{supportTypes}}. Maximal {{size}}MB pro Datei.', tip: 'Unterstützt {{supportTypes}}. Maximal {{size}}MB pro Datei.',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: 'Versuchen zu lösen', tryToSolve: 'Versuchen zu lösen',
temporarySystemIssue: 'Entschuldigung, vorübergehendes Systemproblem.', temporarySystemIssue: 'Entschuldigung, vorübergehendes Systemproblem.',
expand: 'Erweitern',
collapse: 'Reduzieren',
}, },
generation: { generation: {
tabs: { tabs: {

@ -35,7 +35,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Upload file', title: 'Upload file',
button: 'Drag and drop file, or', button: 'Drag and drop file or folder, or',
browse: 'Browse', browse: 'Browse',
tip: 'Supports {{supportTypes}}. Max {{size}}MB each.', tip: 'Supports {{supportTypes}}. Max {{size}}MB each.',
validation: { validation: {

@ -34,6 +34,8 @@ const translation = {
}, },
tryToSolve: 'Try to solve', tryToSolve: 'Try to solve',
temporarySystemIssue: 'Sorry, temporary system issue.', temporarySystemIssue: 'Sorry, temporary system issue.',
expand: 'Expand',
collapse: 'Collapse',
}, },
generation: { generation: {
tabs: { tabs: {

@ -27,7 +27,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Cargar archivo', title: 'Cargar archivo',
button: 'Arrastra y suelta el archivo, o', button: 'Arrastre y suelte archivos o carpetas, o',
browse: 'Buscar', browse: 'Buscar',
tip: 'Soporta {{supportTypes}}. Máximo {{size}}MB cada uno.', tip: 'Soporta {{supportTypes}}. Máximo {{size}}MB cada uno.',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: 'Intentar resolver', tryToSolve: 'Intentar resolver',
temporarySystemIssue: 'Lo sentimos, hay un problema temporal del sistema.', temporarySystemIssue: 'Lo sentimos, hay un problema temporal del sistema.',
expand: 'Ampliar',
collapse: 'Contraer',
}, },
generation: { generation: {
tabs: { tabs: {

@ -27,7 +27,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'بارگذاری فایل', title: 'بارگذاری فایل',
button: 'کشیدن و رها کردن فایل، یا', button: 'فایل ها یا پوشه ها را بکشید و رها کنید یا',
browse: 'مرور', browse: 'مرور',
tip: 'پشتیبانی از {{supportTypes}}. حداکثر {{size}}MB هر کدام.', tip: 'پشتیبانی از {{supportTypes}}. حداکثر {{size}}MB هر کدام.',
validation: { validation: {

@ -26,6 +26,8 @@ const translation = {
}, },
tryToSolve: 'سعی کنید حل کنید', tryToSolve: 'سعی کنید حل کنید',
temporarySystemIssue: 'ببخشید، مشکل موقت سیستمی.', temporarySystemIssue: 'ببخشید، مشکل موقت سیستمی.',
expand: 'باز کردن',
collapse: 'بستن',
}, },
generation: { generation: {
tabs: { tabs: {

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Télécharger le fichier texte', title: 'Télécharger le fichier texte',
button: 'Glisser et déposer le fichier, ou', button: 'Faites glisser et déposez des fichiers ou des dossiers, ou',
browse: 'Parcourir', browse: 'Parcourir',
tip: 'Prend en charge {{supportTypes}}. Max {{size}}MB chacun.', tip: 'Prend en charge {{supportTypes}}. Max {{size}}MB chacun.',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: 'Essayez de résoudre', tryToSolve: 'Essayez de résoudre',
temporarySystemIssue: 'Désolé, problème temporaire du système.', temporarySystemIssue: 'Désolé, problème temporaire du système.',
expand: 'Développer',
collapse: 'Réduire',
}, },
generation: { generation: {
tabs: { tabs: {

@ -27,7 +27,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'फ़ाइल अपलोड करें', title: 'फ़ाइल अपलोड करें',
button: 'फ़ाइल खींचें और छोड़ें, या', button: 'फ़ाइलों या फ़ोल्डरों को खींचें और छोड़ें, या',
browse: 'ब्राउज़ करें', browse: 'ब्राउज़ करें',
tip: 'समर्थित {{supportTypes}}। प्रत्येक अधिकतम {{size}}MB।', tip: 'समर्थित {{supportTypes}}। प्रत्येक अधिकतम {{size}}MB।',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: 'समाधान करने का प्रयास करें', tryToSolve: 'समाधान करने का प्रयास करें',
temporarySystemIssue: 'अभी सिस्टम में समस्या है, कृपया पुनः प्रयास करें।', temporarySystemIssue: 'अभी सिस्टम में समस्या है, कृपया पुनः प्रयास करें।',
expand: 'विस्तार करें',
collapse: 'संकुचित करें',
}, },
generation: { generation: {
tabs: { tabs: {

@ -27,7 +27,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Carica file', title: 'Carica file',
button: 'Trascina e rilascia il file, o', button: 'Trascina e rilascia file o cartelle, oppure',
browse: 'Sfoglia', browse: 'Sfoglia',
tip: 'Supporta {{supportTypes}}. Max {{size}}MB ciascuno.', tip: 'Supporta {{supportTypes}}. Max {{size}}MB ciascuno.',
validation: { validation: {

@ -28,6 +28,8 @@ const translation = {
}, },
tryToSolve: 'Prova a risolvere', tryToSolve: 'Prova a risolvere',
temporarySystemIssue: 'Spiacente, problema temporaneo del sistema.', temporarySystemIssue: 'Spiacente, problema temporaneo del sistema.',
expand: 'Espandi',
collapse: 'Riduci',
}, },
generation: { generation: {
tabs: { tabs: {

@ -30,7 +30,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'テキストファイルをアップロード', title: 'テキストファイルをアップロード',
button: 'ファイルをドラッグ&ドロップするか', button: 'ファイルまたはフォルダをドラッグアンドドロップする',
browse: '参照', browse: '参照',
tip: '{{supportTypes}}をサポートしています。1つあたりの最大サイズは{{size}}MBです。', tip: '{{supportTypes}}をサポートしています。1つあたりの最大サイズは{{size}}MBです。',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: '問題を解決する', tryToSolve: '問題を解決する',
temporarySystemIssue: 'システムに一時的な問題が発生しています', temporarySystemIssue: 'システムに一時的な問題が発生しています',
expand: '拡大',
collapse: '縮小',
}, },
generation: { generation: {
tabs: { tabs: {

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: '텍스트 파일 업로드', title: '텍스트 파일 업로드',
button: '파일을 끌어다 놓거나', button: '파일이나 폴더를 끌어서 놓기',
browse: '찾아보기', browse: '찾아보기',
tip: '{{supportTypes}}을(를) 지원합니다. 파일당 최대 크기는 {{size}}MB입니다.', tip: '{{supportTypes}}을(를) 지원합니다. 파일당 최대 크기는 {{size}}MB입니다.',
validation: { validation: {

@ -26,6 +26,8 @@ const translation = {
}, },
tryToSolve: '해결하려고 합니다', tryToSolve: '해결하려고 합니다',
temporarySystemIssue: '죄송합니다. 일시적인 시스템 문제가 발생했습니다.', temporarySystemIssue: '죄송합니다. 일시적인 시스템 문제가 발생했습니다.',
expand: '확장',
collapse: '축소',
}, },
generation: { generation: {
tabs: { tabs: {

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Prześlij plik tekstowy', title: 'Prześlij plik tekstowy',
button: 'Przeciągnij i upuść plik lub', button: 'Przeciągnij i upuść pliki lub foldery lub',
browse: 'Przeglądaj', browse: 'Przeglądaj',
tip: 'Obsługuje {{supportTypes}}. Maksymalnie {{size}}MB każdy.', tip: 'Obsługuje {{supportTypes}}. Maksymalnie {{size}}MB każdy.',
validation: { validation: {

@ -27,6 +27,8 @@ const translation = {
}, },
tryToSolve: 'Spróbuj rozwiązać', tryToSolve: 'Spróbuj rozwiązać',
temporarySystemIssue: 'Przepraszamy, tymczasowy problem systemowy.', temporarySystemIssue: 'Przepraszamy, tymczasowy problem systemowy.',
expand: 'Rozwiń',
collapse: 'Zwiń',
}, },
generation: { generation: {
tabs: { tabs: {

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Enviar arquivo de texto', title: 'Enviar arquivo de texto',
button: 'Arraste e solte o arquivo, ou', button: 'Arraste e solte arquivos ou pastas, ou',
browse: 'Navegar', browse: 'Navegar',
tip: 'Suporta {{supportTypes}}. Máximo de {{size}}MB cada.', tip: 'Suporta {{supportTypes}}. Máximo de {{size}}MB cada.',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: 'Tente resolver', tryToSolve: 'Tente resolver',
temporarySystemIssue: 'Desculpe, problema temporário do sistema.', temporarySystemIssue: 'Desculpe, problema temporário do sistema.',
expand: 'Expandir',
collapse: 'Contrair',
}, },
generation: { generation: {
tabs: { tabs: {

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Încărcați fișier text', title: 'Încărcați fișier text',
button: 'Trageți și fixați fișierul, sau', button: 'Trageți și plasați fișiere sau foldere sau',
browse: 'Răsfoire', browse: 'Răsfoire',
tip: 'Acceptă {{supportTypes}}. Maxim {{size}}MB fiecare.', tip: 'Acceptă {{supportTypes}}. Maxim {{size}}MB fiecare.',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: 'Încercați să rezolvați', tryToSolve: 'Încercați să rezolvați',
temporarySystemIssue: 'Ne pare rău, problemă temporară a sistemului.', temporarySystemIssue: 'Ne pare rău, problemă temporară a sistemului.',
expand: 'Extinde',
collapse: 'Restrânge',
}, },
generation: { generation: {
tabs: { tabs: {

@ -27,7 +27,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Загрузить файл', title: 'Загрузить файл',
button: 'Перетащите файл или', button: 'Перетащите файлы или папки или',
browse: 'Обзор', browse: 'Обзор',
tip: 'Поддерживаются {{supportTypes}}. Максимум {{size}} МБ каждый.', tip: 'Поддерживаются {{supportTypes}}. Максимум {{size}} МБ каждый.',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: 'Попробуйте решить', tryToSolve: 'Попробуйте решить',
temporarySystemIssue: 'Извините, временная проблема с системой.', temporarySystemIssue: 'Извините, временная проблема с системой.',
expand: 'Развернуть',
collapse: 'Свернуть',
}, },
generation: { generation: {
tabs: { tabs: {

@ -32,7 +32,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Naloži datoteko', title: 'Naloži datoteko',
button: 'Povleci in spusti datoteko ali', button: 'Povleci in spusti datoteke ali mape oz',
browse: 'Prebrskaj', browse: 'Prebrskaj',
tip: 'Podprti tipi datotek: {{supportTypes}}. Največ {{size}}MB na datoteko.', tip: 'Podprti tipi datotek: {{supportTypes}}. Največ {{size}}MB na datoteko.',
validation: { validation: {

@ -27,6 +27,8 @@ const translation = {
}, },
tryToSolve: 'Poskusite rešiti', tryToSolve: 'Poskusite rešiti',
temporarySystemIssue: 'Oprostite, začasna težava s sistemom.', temporarySystemIssue: 'Oprostite, začasna težava s sistemom.',
expand: 'Razširi',
collapse: 'Skrči',
}, },
generation: { generation: {
tabs: { tabs: {

@ -32,7 +32,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'อัปโหลดไฟล์', title: 'อัปโหลดไฟล์',
button: 'ลากและวางไฟล์ หรือ', button: 'ลากและวางไฟล์หรือโฟลเดอร์หรือ',
browse: 'เล็ม', browse: 'เล็ม',
tip: 'รองรับ {{supportTypes}} สูงสุด {{size}}MB แต่ละตัว', tip: 'รองรับ {{supportTypes}} สูงสุด {{size}}MB แต่ละตัว',
validation: { validation: {

@ -26,6 +26,8 @@ const translation = {
}, },
tryToSolve: 'พยายามแก้', tryToSolve: 'พยายามแก้',
temporarySystemIssue: 'ขออภัย ปัญหาระบบชั่วคราว', temporarySystemIssue: 'ขออภัย ปัญหาระบบชั่วคราว',
expand: 'ขยาย',
collapse: 'ย่อ',
}, },
generation: { generation: {
tabs: { tabs: {

@ -27,7 +27,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Dosya yükle', title: 'Dosya yükle',
button: 'Dosyayı sürükleyip bırakın veya', button: 'Dosyaları veya klasörleri sürükleyip bırakın veya',
browse: 'Göz atın', browse: 'Göz atın',
tip: 'Destekler {{supportTypes}}. Her biri en fazla {{size}}MB.', tip: 'Destekler {{supportTypes}}. Her biri en fazla {{size}}MB.',
validation: { validation: {

@ -26,6 +26,8 @@ const translation = {
}, },
tryToSolve: 'Çözmeyi Dene', tryToSolve: 'Çözmeyi Dene',
temporarySystemIssue: 'Üzgünüz, geçici sistem sorunu.', temporarySystemIssue: 'Üzgünüz, geçici sistem sorunu.',
expand: 'Genişlet',
collapse: 'Kısıtla',
}, },
generation: { generation: {
tabs: { tabs: {

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Завантажити текстовий файл', title: 'Завантажити текстовий файл',
button: 'Перетягніть файл або', button: 'Перетягніть файли або папки або',
browse: 'Оберіть', browse: 'Оберіть',
tip: 'Підтримуються {{supportTypes}}. Максимум {{size}} МБ кожен.', tip: 'Підтримуються {{supportTypes}}. Максимум {{size}} МБ кожен.',
validation: { validation: {

@ -26,6 +26,8 @@ const translation = {
}, },
tryToSolve: 'Спробувати вирішити', tryToSolve: 'Спробувати вирішити',
temporarySystemIssue: 'Вибачте, тимчасова системна проблема.', temporarySystemIssue: 'Вибачте, тимчасова системна проблема.',
expand: 'Розгорнути',
collapse: 'Згорнути',
}, },
generation: { generation: {
tabs: { tabs: {

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: 'Tải lên tệp văn bản', title: 'Tải lên tệp văn bản',
button: 'Kéo và thả tệp, hoặc', button: 'Kéo và thả các tập tin hoặc thư mục, hoặc',
browse: 'Chọn tệp', browse: 'Chọn tệp',
tip: 'Hỗ trợ {{supportTypes}}. Tối đa {{size}}MB mỗi tệp.', tip: 'Hỗ trợ {{supportTypes}}. Tối đa {{size}}MB mỗi tệp.',
validation: { validation: {

@ -26,6 +26,8 @@ const translation = {
}, },
tryToSolve: 'Thử giải quyết', tryToSolve: 'Thử giải quyết',
temporarySystemIssue: 'Xin lỗi, hệ thống đang gặp sự cố tạm thời.', temporarySystemIssue: 'Xin lỗi, hệ thống đang gặp sự cố tạm thời.',
expand: 'Mở rộng',
collapse: 'Thu gọn',
}, },
generation: { generation: {
tabs: { tabs: {

@ -35,7 +35,7 @@ const translation = {
}, },
uploader: { uploader: {
title: '上传文本文件', title: '上传文本文件',
button: '拖拽文件至此,或者', button: '拖拽文件或文件夹至此,或者',
browse: '选择文件', browse: '选择文件',
tip: '已支持 {{supportTypes}},每个文件不超过 {{size}}MB。', tip: '已支持 {{supportTypes}},每个文件不超过 {{size}}MB。',
validation: { validation: {

@ -30,6 +30,8 @@ const translation = {
}, },
tryToSolve: '尝试解决', tryToSolve: '尝试解决',
temporarySystemIssue: '抱歉,临时系统问题。', temporarySystemIssue: '抱歉,临时系统问题。',
expand: '展开',
collapse: '折叠',
}, },
generation: { generation: {
tabs: { tabs: {

@ -22,7 +22,7 @@ const translation = {
}, },
uploader: { uploader: {
title: '上傳文字檔案', title: '上傳文字檔案',
button: '拖拽檔案至此,或者', button: '拖拽檔案或檔案夾至此,或者',
browse: '選擇檔案', browse: '選擇檔案',
tip: '已支援 {{supportTypes}},每個檔案不超過 {{size}}MB。', tip: '已支援 {{supportTypes}},每個檔案不超過 {{size}}MB。',
validation: { validation: {

@ -26,6 +26,8 @@ const translation = {
}, },
tryToSolve: '嘗試解決', tryToSolve: '嘗試解決',
temporarySystemIssue: '抱歉,臨時系統問題。', temporarySystemIssue: '抱歉,臨時系統問題。',
expand: '展開',
collapse: '摺疊',
}, },
generation: { generation: {
tabs: { tabs: {

@ -1,6 +1,6 @@
{ {
"name": "dify-web", "name": "dify-web",
"version": "1.3.0", "version": "1.3.1",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=v22.11.0" "node": ">=v22.11.0"

@ -12,6 +12,7 @@
const buttonId = "dify-chatbot-bubble-button"; const buttonId = "dify-chatbot-bubble-button";
const iframeId = "dify-chatbot-bubble-window"; const iframeId = "dify-chatbot-bubble-window";
const config = window[configKey]; const config = window[configKey];
let isExpanded = false;
// SVG icons for open and close states // SVG icons for open and close states
const svgIcons = `<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> const svgIcons = `<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -22,6 +23,53 @@
</svg> </svg>
`; `;
const originalIframeStyleText = `
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
top: unset;
right: var(--${buttonId}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
bottom: var(--${buttonId}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
left: unset;
width: 24rem;
max-width: calc(100vw - 2rem);
height: 43.75rem;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
transition-property: width, height;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
`
const expandedIframeStyleText = `
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
top: unset;
right: var(--${buttonId}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
bottom: var(--${buttonId}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
left: unset;
min-width: 24rem;
width: 48%;
max-width: 40rem; /* Match mobile breakpoint*/
min-height: 43.75rem;
height: 88%;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
transition-property: width, height;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
`
// Main function to embed the chatbot // Main function to embed the chatbot
async function embedChatbot() { async function embedChatbot() {
let isDragging = false let isDragging = false
@ -71,6 +119,7 @@
const baseUrl = const baseUrl =
config.baseUrl || `https://${config.isDev ? "dev." : ""}udify.app`; config.baseUrl || `https://${config.isDev ? "dev." : ""}udify.app`;
const targetOrigin = new URL(baseUrl).origin;
// pre-check the length of the URL // pre-check the length of the URL
const iframeUrl = `${baseUrl}/chatbot/${config.token}?${params}`; const iframeUrl = `${baseUrl}/chatbot/${config.token}?${params}`;
@ -92,23 +141,7 @@
iframe.title = "dify chatbot bubble window"; iframe.title = "dify chatbot bubble window";
iframe.id = iframeId; iframe.id = iframeId;
iframe.src = iframeUrl; iframe.src = iframeUrl;
iframe.style.cssText = ` iframe.style.cssText = originalIframeStyleText;
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
left: unset;
right: 0;
bottom: 0;
width: 24rem;
max-width: calc(100vw - 2rem);
height: 43.75rem;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
`;
return iframe; return iframe;
} }
@ -121,28 +154,69 @@
const targetButton = document.getElementById(buttonId); const targetButton = document.getElementById(buttonId);
if (targetIframe && targetButton) { if (targetIframe && targetButton) {
const buttonRect = targetButton.getBoundingClientRect(); const buttonRect = targetButton.getBoundingClientRect();
// We don't necessarily need iframeRect anymore with the center logic
const viewportCenterY = window.innerHeight / 2;
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
if (buttonCenterY < viewportCenterY) {
targetIframe.style.top = `var(--${buttonId}-bottom, 1rem)`;
targetIframe.style.bottom = 'unset';
} else {
targetIframe.style.bottom = `var(--${buttonId}-bottom, 1rem)`;
targetIframe.style.top = 'unset';
}
const buttonInBottom = buttonRect.top - 5 > targetIframe.clientHeight; const viewportCenterX = window.innerWidth / 2;
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
if (buttonInBottom) { if (buttonCenterX < viewportCenterX) {
targetIframe.style.bottom = "0px"; targetIframe.style.left = `var(--${buttonId}-right, 1rem)`;
targetIframe.style.top = "unset"; targetIframe.style.right = 'unset';
} else { } else {
targetIframe.style.bottom = "unset"; targetIframe.style.right = `var(--${buttonId}-right, 1rem)`;
targetIframe.style.top = "0px"; targetIframe.style.left = 'unset';
}
}
} }
const buttonInRight = buttonRect.right > targetIframe.clientWidth; function toggleExpand() {
isExpanded = !isExpanded;
if (buttonInRight) { const targetIframe = document.getElementById(iframeId);
targetIframe.style.right = "0"; if (!targetIframe) return;
targetIframe.style.left = "unset";
if (isExpanded) {
targetIframe.style.cssText = expandedIframeStyleText;
} else { } else {
targetIframe.style.right = "unset"; targetIframe.style.cssText = originalIframeStyleText;
targetIframe.style.left = 0; }
resetIframePosition();
} }
window.addEventListener('message', (event) => {
if (event.origin !== targetOrigin) return;
const targetIframe = document.getElementById(iframeId);
if (!targetIframe || event.source !== targetIframe.contentWindow) return;
if (event.data.type === 'dify-chatbot-iframe-ready') {
targetIframe.contentWindow?.postMessage(
{
type: 'dify-chatbot-config',
payload: {
isToggledByButton: true,
isDraggable: !!config.draggable,
},
},
targetOrigin
);
} }
if (event.data.type === 'dify-chatbot-expand-change') {
toggleExpand();
} }
});
// Function to create the chat button // Function to create the chat button
function createButton() { function createButton() {

@ -1,11 +1,12 @@
(()=>{let t="difyChatbotConfig",m="dify-chatbot-bubble-button",h="dify-chatbot-bubble-window",p=window[t];async function e(){let u=!1;if(p&&p.token){var e=new URLSearchParams({...await(async()=>{var e=p?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await o(t)})),n})(),...await(async()=>{var e=p?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await o(t)})),n})()});let t=`${p.baseUrl||`https://${p.isDev?"dev.":""}udify.app`}/chatbot/${p.token}?`+e;e=s();async function o(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=h,e.src=t,e.style.cssText=` (()=>{let t="difyChatbotConfig",h="dify-chatbot-bubble-button",m="dify-chatbot-bubble-window",y=window[t],a=!1,l=`
position: absolute; position: absolute;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
top: unset;
right: var(--${h}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
bottom: var(--${h}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
left: unset; left: unset;
right: 0;
bottom: 0;
width: 24rem; width: 24rem;
max-width: calc(100vw - 2rem); max-width: calc(100vw - 2rem);
height: 43.75rem; height: 43.75rem;
@ -14,7 +15,10 @@
z-index: 2147483640; z-index: 2147483640;
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
`,e}function d(){var e,t;window.innerWidth<=640||(e=document.getElementById(h),t=document.getElementById(m),e&&t&&((t=t.getBoundingClientRect()).top-5>e.clientHeight?(e.style.bottom="0px",e.style.top="unset"):(e.style.bottom="unset",e.style.top="0px"),t.right>e.clientWidth?(e.style.right="0",e.style.left="unset"):(e.style.right="unset",e.style.left=0)))}function n(){let n=document.createElement("div");Object.entries(p.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=m;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(` transition-property: width, height;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
`;async function e(){let u=!1;if(y&&y.token){var e=new URLSearchParams({...await(async()=>{var e=y?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await i(t)})),n})(),...await(async()=>{var e=y?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await i(t)})),n})()}),n=y.baseUrl||`https://${y.isDev?"dev.":""}udify.app`;let o=new URL(n).origin,t=`${n}/chatbot/${y.token}?`+e;n=s();async function i(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=m,e.src=t,e.style.cssText=l,e}function d(){var e,t,n;window.innerWidth<=640||(e=document.getElementById(m),t=document.getElementById(h),e&&t&&(t=t.getBoundingClientRect(),n=window.innerHeight/2,t.top+t.height/2<n?(e.style.top=`var(--${h}-bottom, 1rem)`,e.style.bottom="unset"):(e.style.bottom=`var(--${h}-bottom, 1rem)`,e.style.top="unset"),t.left+t.width/2<window.innerWidth/2?(e.style.left=`var(--${h}-right, 1rem)`,e.style.right="unset"):(e.style.right=`var(--${h}-right, 1rem)`,e.style.left="unset")))}function r(){let n=document.createElement("div");Object.entries(y.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=h;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(`
#${n.id} { #${n.id} {
position: fixed; position: fixed;
bottom: var(--${n.id}-bottom, 1rem); bottom: var(--${n.id}-bottom, 1rem);
@ -29,10 +33,10 @@
cursor: pointer; cursor: pointer;
z-index: 2147483647; z-index: 2147483647;
} }
`),document.createElement("div"));function t(){var e;u||((e=document.getElementById(h))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?y("open"):y("close"),"none"===e.style.display?document.removeEventListener("keydown",l):document.addEventListener("keydown",l),d()):(n.appendChild(s()),d(),this.title="Exit (ESC)",y("close"),document.addEventListener("keydown",l)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> `),document.createElement("div"));function t(){var e;u||((e=document.getElementById(m))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?p("open"):p("close"),"none"===e.style.display?document.removeEventListener("keydown",b):document.addEventListener("keydown",b),d()):(n.appendChild(s()),d(),this.title="Exit (ESC)",p("close"),document.addEventListener("keydown",b)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/>
</svg> </svg>
<svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
`,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",t),p.draggable){var r=n;var a=p.dragAxis||"both";let s,d,t,l;function o(e){u=!1,l=("touchstart"===e.type?(s=e.touches[0].clientX-r.offsetLeft,d=e.touches[0].clientY-r.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-r.offsetLeft,d=e.clientY-r.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-l;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){r.style.transition="none",r.style.cursor="grabbing";i=document.getElementById(h);i&&(i.style.display="none",y("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-d):(e=n.clientX-s,window.innerHeight-n.clientY-d);o=r.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==a&&"both"!==a||r.style.setProperty(`--${m}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==a&&"both"!==a||r.style.setProperty(`--${m}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),r.style.transition="",r.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}r.addEventListener("mousedown",o),r.addEventListener("touchstart",o)}}e.style.display="none",document.body.appendChild(e),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),document.getElementById(m)||n()}else console.error(t+" is empty or token is not provided")}function y(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function l(e){"Escape"===e.key&&(e=document.getElementById(h))&&"none"!==e.style.display&&(e.style.display="none",y("open"))}document.addEventListener("keydown",l),p?.dynamicScript?e():document.body.onload=e})(); `,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",t),y.draggable){var a=n;var l=y.dragAxis||"both";let s,d,t,r;function o(e){u=!1,r=("touchstart"===e.type?(s=e.touches[0].clientX-a.offsetLeft,d=e.touches[0].clientY-a.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-a.offsetLeft,d=e.clientY-a.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-r;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){a.style.transition="none",a.style.cursor="grabbing";i=document.getElementById(m);i&&(i.style.display="none",p("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-d):(e=n.clientX-s,window.innerHeight-n.clientY-d);o=a.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==l&&"both"!==l||a.style.setProperty(`--${h}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==l&&"both"!==l||a.style.setProperty(`--${h}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),a.style.transition="",a.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}a.addEventListener("mousedown",o),a.addEventListener("touchstart",o)}}n.style.display="none",document.body.appendChild(n),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),window.addEventListener("message",e=>{var t,n;e.origin===o&&(t=document.getElementById(m))&&e.source===t.contentWindow&&("dify-chatbot-iframe-ready"===e.data.type&&t.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:!0,isDraggable:!!y.draggable}},o),"dify-chatbot-expand-change"===e.data.type)&&(a=!a,n=document.getElementById(m))&&(a?n.style.cssText="\n position: absolute;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n top: unset;\n right: var(--dify-chatbot-bubble-button-right, 1rem); /* Align with dify-chatbot-bubble-button. */\n bottom: var(--dify-chatbot-bubble-button-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */\n left: unset;\n min-width: 24rem;\n width: 48%;\n max-width: 40rem; /* Match mobile breakpoint*/\n min-height: 43.75rem;\n height: 88%;\n max-height: calc(100vh - 6rem);\n border: none;\n z-index: 2147483640;\n overflow: hidden;\n user-select: none;\n transition-property: width, height;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n ":n.style.cssText=l,d())}),document.getElementById(h)||r()}else console.error(t+" is empty or token is not provided")}function p(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function b(e){"Escape"===e.key&&(e=document.getElementById(m))&&"none"!==e.style.display&&(e.style.display="none",p("open"))}h,h,document.addEventListener("keydown",b),y?.dynamicScript?e():document.body.onload=e})();
Loading…
Cancel
Save