Merge branch 'langgenius:main' into main

pull/19119/head
sondin 1 year ago committed by GitHub
commit e9599876a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,18 +5,35 @@ root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
indent_size = 4
indent_style = space
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.toml]
indent_size = 4
indent_style = space
# Markdown and MDX are whitespace sensitive languages.
# Do not remove trailing spaces.
[*.{md,mdx}]
trim_trailing_whitespace = false
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,tsx}]
charset = utf-8
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
# Matches the exact files package.json
[package.json]
indent_style = space
indent_size = 2

@ -0,0 +1,22 @@
{
"Verbose": false,
"Debug": false,
"IgnoreDefaults": false,
"SpacesAfterTabs": false,
"NoColor": false,
"Exclude": [
"^web/public/vs/",
"^web/public/pdf.worker.min.mjs$",
"web/app/components/base/icons/src/vender/"
],
"AllowedContentTypes": [],
"PassedFiles": [],
"Disable": {
"EndOfLine": false,
"Indentation": false,
"IndentSize": true,
"InsertFinalNewline": false,
"TrimTrailingWhitespace": false,
"MaxLineLength": false
}
}

@ -9,6 +9,12 @@ concurrency:
group: style-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
checks: write
statuses: write
contents: read
jobs:
python-style:
name: Python Style
@ -163,3 +169,14 @@ jobs:
VALIDATE_DOCKERFILE_HADOLINT: true
VALIDATE_XML: true
VALIDATE_YAML: true
- name: EditorConfig checks
uses: super-linter/super-linter/slim@v7
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IGNORE_GENERATED_FILES: true
IGNORE_GITIGNORED_FILES: true
# EditorConfig validation
VALIDATE_EDITORCONFIG: true
EDITORCONFIG_FILE_NAME: editorconfig-checker.json

@ -52,7 +52,6 @@ def initialize_extensions(app: DifyApp):
ext_mail,
ext_migrate,
ext_otel,
ext_otel_patch,
ext_proxy_fix,
ext_redis,
ext_repositories,
@ -85,7 +84,6 @@ def initialize_extensions(app: DifyApp):
ext_proxy_fix,
ext_blueprints,
ext_commands,
ext_otel_patch, # Apply patch before initializing OpenTelemetry
ext_otel,
]
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.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.")
def clear_orphaned_file_records():
def clear_orphaned_file_records(force: bool):
"""
Clear orphaned file records in the database.
"""
@ -845,7 +846,15 @@ def clear_orphaned_file_records():
# notify user and ask for confirmation
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:
click.echo(click.style(f"- {files_table['table']}", fg="yellow"))
@ -878,11 +887,55 @@ def clear_orphaned_file_records():
fg="yellow",
)
)
if not force:
click.confirm("Do you want to proceed?", abort=True)
# start the cleanup process
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:
# fetch file id and keys from each table
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"))
for file in orphaned_files:
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)
# 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.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.")
def remove_orphaned_files_on_storage():
def remove_orphaned_files_on_storage(force: bool):
"""
Remove orphaned files on the storage.
"""
@ -1028,6 +1083,7 @@ def remove_orphaned_files_on_storage():
fg="yellow",
)
)
if not force:
click.confirm("Do you want to proceed?", abort=True)
# 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"))
for file in orphaned_files:
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)
# 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_settings import BaseSettings
@ -9,6 +10,14 @@ class OpenSearchConfig(BaseSettings):
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(
description="Hostname or IP address of the OpenSearch server (e.g., 'localhost' or 'opensearch.example.com')",
default=None,
@ -19,6 +28,16 @@ class OpenSearchConfig(BaseSettings):
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(
description="Username for authenticating with OpenSearch",
default=None,
@ -29,7 +48,11 @@ class OpenSearchConfig(BaseSettings):
default=None,
)
OPENSEARCH_SECURE: bool = Field(
description="Whether to use SSL/TLS encrypted connection for OpenSearch (True for HTTPS, False for HTTP)",
default=False,
OPENSEARCH_AWS_REGION: Optional[str] = Field(
description="AWS region for OpenSearch (e.g. 'us-west-2')",
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(
description="Dify version",
default="1.3.0",
default="1.3.1",
)
COMMIT_SHA: str = Field(

@ -16,11 +16,25 @@ AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS])
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"))
if dify_config.UNSTRUCTURED_API_URL:
DOCUMENT_EXTENSIONS.append("ppt")
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
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])

@ -381,6 +381,8 @@ class WorkflowCycleManage:
workflow_node_execution.elapsed_time = elapsed_time
workflow_node_execution.execution_metadata = execution_metadata
self._workflow_node_execution_repository.update(workflow_node_execution)
return workflow_node_execution
def _handle_workflow_node_execution_retried(

@ -3,6 +3,8 @@ import logging
import re
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.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser
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": ""}
except InvokeError as e:

@ -72,7 +72,7 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation):
raise ValueError("missing query")
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)
elif app.mode == AppMode.COMPLETION:
return cls.invoke_completion_app(app, user, stream, inputs, files)

@ -27,8 +27,8 @@ class MilvusConfig(BaseModel):
uri: str # Milvus server URI
token: Optional[str] = None # Optional token for authentication
user: str # Username for authentication
password: str # Password for authentication
user: Optional[str] = None # Username for authentication
password: Optional[str] = None # Password for authentication
batch_size: int = 100 # Batch size for operations
database: str = "default" # Database name
enable_hybrid_search: bool = False # Flag to enable hybrid search
@ -43,6 +43,7 @@ class MilvusConfig(BaseModel):
"""
if not values.get("uri"):
raise ValueError("config MILVUS_URI is required")
if not values.get("token"):
if not values.get("user"):
raise ValueError("config MILVUS_USER is required")
if not values.get("password"):
@ -356,10 +357,13 @@ class MilvusVector(BaseVector):
)
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.
"""
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)
return client

@ -1,10 +1,9 @@
import json
import logging
import ssl
from typing import Any, Optional
from typing import Any, Literal, Optional
from uuid import uuid4
from opensearchpy import OpenSearch, helpers
from opensearchpy import OpenSearch, Urllib3AWSV4SignerAuth, Urllib3HttpConnection, helpers
from opensearchpy.helpers import BulkIndexError
from pydantic import BaseModel, model_validator
@ -24,9 +23,12 @@ logger = logging.getLogger(__name__)
class OpenSearchConfig(BaseModel):
host: str
port: int
secure: bool = False
auth_method: Literal["basic", "aws_managed_iam"] = "basic"
user: Optional[str] = None
password: Optional[str] = None
secure: bool = False
aws_region: Optional[str] = None
aws_service: Optional[str] = None
@model_validator(mode="before")
@classmethod
@ -35,24 +37,40 @@ class OpenSearchConfig(BaseModel):
raise ValueError("config OPENSEARCH_HOST is required")
if not values.get("port"):
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
def create_ssl_context(self) -> ssl.SSLContext:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE # Disable Certificate Validation
return ssl_context
def create_aws_managed_iam_auth(self) -> Urllib3AWSV4SignerAuth:
import boto3 # type: ignore
return Urllib3AWSV4SignerAuth(
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]:
params = {
"hosts": [{"host": self.host, "port": self.port}],
"use_ssl": 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)
if self.secure:
params["ssl_context"] = self.create_ssl_context()
elif self.auth_method == "aws_managed_iam":
logger.info("Using AWS managed IAM role for OpenSearch Vector DB")
params["http_auth"] = self.create_aws_managed_iam_auth()
return params
@ -76,16 +94,23 @@ class OpenSearchVector(BaseVector):
action = {
"_op_type": "index",
"_index": self._collection_name.lower(),
"_id": uuid4().hex,
"_source": {
Field.CONTENT_KEY.value: documents[i].page_content,
Field.VECTOR.value: embeddings[i], # Make sure you pass an array here
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)
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):
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)
redis_client.set(collection_exist_cache_key, 1, ex=3600)
@ -252,9 +278,12 @@ class OpenSearchVectorFactory(AbstractVectorFactory):
open_search_config = OpenSearchConfig(
host=dify_config.OPENSEARCH_HOST or "localhost",
port=dify_config.OPENSEARCH_PORT,
secure=dify_config.OPENSEARCH_SECURE,
auth_method=dify_config.OPENSEARCH_AUTH_METHOD.value,
user=dify_config.OPENSEARCH_USER,
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)

@ -52,6 +52,7 @@ class RerankModelRunner(BaseRerankRunner):
rerank_documents = []
for result in rerank_result.docs:
if score_threshold is None or result.score >= score_threshold:
# format document
rerank_document = Document(
page_content=result.text,
@ -62,4 +63,5 @@ class RerankModelRunner(BaseRerankRunner):
rerank_document.metadata["score"] = result.score
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

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

@ -11,6 +11,7 @@ import docx
import pandas as pd
import pypandoc # type: ignore
import pypdfium2 # type: ignore
import webvtt # type: ignore
import yaml # type: ignore
from docx.document import Document
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)
case "application/x-yaml" | "text/yaml":
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 _:
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:
"""Extract text from a file based on its 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)
case ".json":
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)
case ".msg":
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 _:
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
if not (dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY):
raise TextExtractionError("UNSTRUCTURED_API_URL and UNSTRUCTURED_API_KEY must be set")
if not dify_config.UNSTRUCTURED_API_URL:
raise TextExtractionError("UNSTRUCTURED_API_URL must be set")
try:
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,
metadata_filename=temp_file.name,
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)
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])
except Exception as 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

@ -9,6 +9,7 @@ from core.app.apps.base_app_queue_manager import GenerateTaskStoppedError
from core.app.entities.app_invoke_entities import InvokeFrom
from core.file.models import File
from core.workflow.callbacks import WorkflowCallback
from core.workflow.constants import ENVIRONMENT_VARIABLE_NODE_ID
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.errors import WorkflowNodeRunFailedError
from core.workflow.graph_engine.entities.event import GraphEngineEvent, GraphRunFailedEvent, InNodeEvent
@ -364,4 +365,5 @@ class WorkflowEntry:
input_value = file_factory.build_from_mappings(mappings=input_value, tenant_id=tenant_id)
# append variable and value to variable pool
if variable_node_id != ENVIRONMENT_VARIABLE_NODE_ID:
variable_pool.add([variable_node_id] + variable_key_list, input_value)

@ -20,7 +20,8 @@ if [[ "${MODE}" == "worker" ]]; then
CONCURRENCY_OPTION="-c ${CELERY_WORKER_AMOUNT:-1}"
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}
elif [[ "${MODE}" == "beat" ]]; then

@ -8,35 +8,77 @@ from typing import Union
from celery.signals import worker_init # 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 dify_app import DifyApp
class ExceptionLoggingHandler(logging.Handler):
@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):
"""Custom logging handler that creates spans for logging.exception() calls"""
def emit(self, record):
@ -60,19 +102,30 @@ class ExceptionLoggingHandler(logging.Handler):
except Exception:
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()
# Initialize OpenTelemetry
# Follow Semantic Convertions 1.32.0 to define resource attributes
@ -133,67 +186,19 @@ def init_app(app: DifyApp):
atexit.register(shutdown_tracer)
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 is_enabled():
return dify_config.ENABLE_OTEL
@worker_init.connect(weak=False)
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()
metric_provider = get_meter_provider()
if dify_config.DEBUG:
logging.info("Initializing OpenTelemetry for Celery worker")
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")

@ -1,6 +1,6 @@
[project]
name = "dify-api"
version = "1.3.0"
dynamic = ["version"]
requires-python = ">=3.11,<3.13"
dependencies = [
@ -81,15 +81,19 @@ dependencies = [
"tokenizers~=0.15.0",
"transformers~=4.35.0",
"unstructured[docx,epub,md,ppt,pptx]~=0.16.1",
"validators==0.21.0",
"weave~=0.51.34",
"yarl~=1.18.3",
"webvtt-py~=0.5.1",
]
# Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group.
[tool.setuptools]
packages = []
[tool.uv]
default-groups = ["storage", "tools", "vdb"]
package = false
[dependency-groups]
@ -191,6 +195,6 @@ vdb = [
"tidb-vector==0.0.9",
"upstash-vector==0.6.0",
"volcengine-compat~=1.0.156",
"weaviate-client~=3.21.0",
"weaviate-client~=3.24.0",
"xinference-client~=1.2.2",
]

@ -2,9 +2,9 @@ import json
from copy import deepcopy
from datetime import UTC, datetime
from typing import Any, Optional, Union, cast
from urllib.parse import urlparse
import httpx
import validators
from constants import HIDDEN_VALUE
from core.helper import ssrf_proxy
@ -72,7 +72,9 @@ class ExternalDatasetService:
endpoint = f"{settings['endpoint']}/retrieval"
api_key = settings["api_key"]
if not validators.url(endpoint, simple_host=True):
parsed_url = urlparse(endpoint)
if not all([parsed_url.scheme, parsed_url.netloc]):
if not endpoint.startswith("http://") and not endpoint.startswith("https://"):
raise ValueError(f"invalid endpoint: {endpoint} must start with http:// or https://")
else:

@ -23,13 +23,70 @@ def setup_mock_redis():
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:
def setup_method(self):
self.collection_name = "test_collection"
self.example_doc_id = "example_doc_id"
self.vector = OpenSearchVector(
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()

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

@ -1155,7 +1155,6 @@ wheels = [
[[package]]
name = "dify-api"
version = "1.3.0"
source = { virtual = "." }
dependencies = [
{ name = "authlib" },
@ -1233,8 +1232,8 @@ dependencies = [
{ name = "tokenizers" },
{ name = "transformers" },
{ name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] },
{ name = "validators" },
{ name = "weave" },
{ name = "webvtt-py" },
{ name = "yarl" },
]
@ -1403,8 +1402,8 @@ requires-dist = [
{ name = "tokenizers", specifier = "~=0.15.0" },
{ name = "transformers", specifier = "~=4.35.0" },
{ name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" },
{ name = "validators", specifier = "==0.21.0" },
{ name = "weave", specifier = "~=0.51.34" },
{ name = "webvtt-py", specifier = "~=0.5.1" },
{ name = "yarl", specifier = "~=1.18.3" },
]
@ -1492,7 +1491,7 @@ vdb = [
{ name = "tidb-vector", specifier = "==0.0.9" },
{ name = "upstash-vector", specifier = "==0.6.0" },
{ name = "volcengine-compat", specifier = "~=1.0.156" },
{ name = "weaviate-client", specifier = "~=3.21.0" },
{ name = "weaviate-client", specifier = "~=3.24.0" },
{ name = "xinference-client", specifier = "~=1.2.2" },
]
@ -6086,11 +6085,11 @@ wheels = [
[[package]]
name = "validators"
version = "0.21.0"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/c5/4095e7a5a6fecc2eca953ad058a3609135d833f986f84951f7e26790d651/validators-0.21.0.tar.gz", hash = "sha256:245b98ab778ed9352a7269c6a8f6c2a839bed5b2a7e3e60273ce399d247dd4b3", size = 20937 }
sdist = { url = "https://files.pythonhosted.org/packages/64/07/91582d69320f6f6daaf2d8072608a4ad8884683d4840e7e4f3a9dbdcc639/validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f", size = 70955 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/50/18dbf2ac594234ee6249bfe3425fa424c18eeb96f29dcd47f199ed6c51bc/validators-0.21.0-py3-none-any.whl", hash = "sha256:3470db6f2384c49727ee319afa2e97aec3f8fad736faa6067e0fd7f9eaf2c551", size = 27686 },
{ url = "https://files.pythonhosted.org/packages/6e/78/36828a4d857b25896f9774c875714ba4e9b3bc8a92d2debe3f4df3a83d4f/validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321", size = 43536 },
]
[[package]]
@ -6220,17 +6219,16 @@ wheels = [
[[package]]
name = "weaviate-client"
version = "3.21.0"
version = "3.24.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "authlib" },
{ name = "requests" },
{ name = "tqdm" },
{ name = "validators" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b4/a5/c6777a8507249d7a63f4f5d9696eb5f45beac87db0eddfa4438d408cc3b4/weaviate-client-3.21.0.tar.gz", hash = "sha256:ec94ac554883c765e94da8b2947c4f0fa4a0378ed3bbe9f3653df3a5b1745a6d", size = 186970 }
sdist = { url = "https://files.pythonhosted.org/packages/1f/c1/3285a21d8885f2b09aabb65edb9a8e062a35c2d7175e1bb024fa096582ab/weaviate-client-3.24.2.tar.gz", hash = "sha256:6914c48c9a7e5ad0be9399271f9cb85d6f59ab77476c6d4e56a3925bf149edaa", size = 199332 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/5b/57b55ad36eb071b57e79f1ea7fba5bfe6a2fe49702607f56726569665d60/weaviate_client-3.21.0-py3-none-any.whl", hash = "sha256:420444ded7106fb000f4f8b2321b5f5fa2387825aa7a303d702accf61026f9d2", size = 99944 },
{ url = "https://files.pythonhosted.org/packages/ab/98/3136d05f93e30cf29e1db280eaadf766df18d812dfe7994bcced653b2340/weaviate_client-3.24.2-py3-none-any.whl", hash = "sha256:bc50ca5fcebcd48de0d00f66700b0cf7c31a97c4cd3d29b4036d77c5d1d9479b", size = 107968 },
]
[[package]]
@ -6282,6 +6280,15 @@ wheels = [
{ 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]]
name = "werkzeug"
version = "3.1.3"

@ -39,6 +39,12 @@ APP_WEB_URL=
# File preview or download Url prefix.
# used to display File preview or download Url to the front-end or as Multi-model inputs;
# 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=
# ------------------------------
@ -520,9 +526,13 @@ RELYT_DATABASE=postgres
# open search configuration, only available when VECTOR_STORE is `opensearch`
OPENSEARCH_HOST=opensearch
OPENSEARCH_PORT=9200
OPENSEARCH_SECURE=true
OPENSEARCH_AUTH_METHOD=basic
OPENSEARCH_USER=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_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.
- **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`

@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.3.0
image: langgenius/dify-api:1.3.1
restart: always
environment:
# Use the shared environment variables.
@ -31,7 +31,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.3.0
image: langgenius/dify-api:1.3.1
restart: always
environment:
# Use the shared environment variables.
@ -57,7 +57,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.3.0
image: langgenius/dify-web:1.3.1
restart: always
environment:
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}
OPENSEARCH_HOST: ${OPENSEARCH_HOST:-opensearch}
OPENSEARCH_PORT: ${OPENSEARCH_PORT:-9200}
OPENSEARCH_SECURE: ${OPENSEARCH_SECURE:-true}
OPENSEARCH_AUTH_METHOD: ${OPENSEARCH_AUTH_METHOD:-basic}
OPENSEARCH_USER: ${OPENSEARCH_USER:-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_API_KEY: ${TENCENT_VECTOR_DB_API_KEY:-dify}
TENCENT_VECTOR_DB_TIMEOUT: ${TENCENT_VECTOR_DB_TIMEOUT:-30}
@ -488,7 +491,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.3.0
image: langgenius/dify-api:1.3.1
restart: always
environment:
# Use the shared environment variables.
@ -517,7 +520,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.3.0
image: langgenius/dify-api:1.3.1
restart: always
environment:
# Use the shared environment variables.
@ -543,7 +546,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.3.0
image: langgenius/dify-web:1.3.1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}

@ -1,6 +1,6 @@
import type { FC } from 'react'
import React from 'react'
import { RiResetLeftLine } from '@remixicon/react'
import React, { useCallback, useEffect, useState } from 'react'
import { RiCollapseDiagonal2Line, RiExpandDiagonal2Line, RiResetLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { Theme } from '../theme/theme-context'
import { CssTransform } from '../theme/utils'
@ -36,6 +36,44 @@ const Header: FC<IHeaderProps> = ({
currentConversationId,
inputsForms,
} = 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) {
return (
<div className='flex h-14 shrink-0 items-center justify-end p-3'>
@ -59,6 +97,21 @@ const Header: FC<IHeaderProps> = ({
{currentConversationId && (
<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 && (
<Tooltip
popupContent={t('share.chat.resetChat')}
@ -91,6 +144,21 @@ const Header: FC<IHeaderProps> = ({
</div>
</div>
<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 && (
<Tooltip
popupContent={t('share.chat.resetChat')}

@ -22,10 +22,6 @@ export function preprocessMermaidCode(code: string): string {
.replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}`)
// Fix common syntax issues
.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
.trim()
}

@ -37,4 +37,3 @@
white-space: pre-line;
word-break: break-all;
}

@ -196,22 +196,68 @@ const FileUploader = ({
e.stopPropagation()
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.stopPropagation()
setDragging(false)
if (!e.dataTransfer)
return
let files = [...e.dataTransfer.files] as File[]
if (notSupportBatchUpload)
files = files.slice(0, 1)
const validFiles = files.filter(isValid)
initialUpload(validFiles)
}, [initialUpload, isValid, notSupportBatchUpload])
if (!e.dataTransfer) return
const nested = await Promise.all(
Array.from(e.dataTransfer.items).map((it) => {
const entry = (it as any).webkitGetAsEntry?.()
if (entry) return traverseFileEntry(entry)
const f = it.getAsFile?.()
return f ? Promise.resolve([f]) : Promise.resolve([])
}),
)
let files = nested.flat()
if (notSupportBatchUpload) files = files.slice(0, 1)
const valid = files.filter(isValid)
initialUpload(valid)
},
[initialUpload, isValid, notSupportBatchUpload, traverseFileEntry],
)
const selectHandle = () => {
if (fileUploader.current)
fileUploader.current.click()

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

@ -49,7 +49,7 @@ const Header = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSegment])
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'>
{isMobile && <div
className='flex h-8 w-8 cursor-pointer items-center justify-center'
@ -59,7 +59,7 @@ const Header = () => {
</div>}
{
!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'>
<LogoSite className='object-contain' />
</Link>
@ -84,7 +84,7 @@ const Header = () => {
)}
{
!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 && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
@ -92,7 +92,7 @@ const Header = () => {
</div>
)
}
<div className='flex shrink-0 items-center'>
<div className='flex shrink-0 items-center pr-3'>
<EnvNav />
<div className='mr-2'>
<PluginsNav />

@ -6,7 +6,7 @@ import {
RiArrowDownSLine,
RiArrowRightSLine,
} 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 { debounce } from 'lodash-es'
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}>
{
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={() => {
if (curNav?.id === nav.id)
return
@ -112,12 +112,12 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
{nav.name}
</div>
</div>
</MenuItems>
</MenuItem>
))
}
</div>
{!isApp && isCurrentWorkspaceEditor && (
<MenuButton className='w-full p-1'>
<MenuItem as="div" className='w-full p-1'>
<div onClick={() => onCreate('')} className={cn(
'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 className='grow text-left text-[14px] font-normal text-gray-700'>{createText}</div>
</div>
</MenuButton>
</MenuItem>
)}
{isApp && isCurrentWorkspaceEditor && (
<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
}
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
curr = curr?.find((v: any) => v.variable === key)
if (isLast) {
curr = Array.isArray(curr) ? curr.find(v => v.variable === key) : []
if (isLast)
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) {
@ -631,7 +630,7 @@ const getLoopItemType = ({
}: {
valueSelector: ValueSelector
beforeNodesOutputVars: NodeOutPutVar[]
// eslint-disable-next-line sonarjs/no-identical-functions
}): VarType => {
const outputVarNodeId = valueSelector[0]
const isSystem = isSystemVar(valueSelector)

@ -63,7 +63,7 @@ const ClassList: FC<Props> = ({
return (
<Item
nodeId={nodeId}
key={index}
key={list[index].id}
payload={item}
onChange={handleClassChange(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_FIRECRAWL=${ENABLE_WEBSITE_FIRECRAWL:-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

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

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

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

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

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

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

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

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

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
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',
tip: 'Prend en charge {{supportTypes}}. Max {{size}}MB chacun.',
validation: {

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
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',
tip: 'Acceptă {{supportTypes}}. Maxim {{size}}MB fiecare.',
validation: {

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

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

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

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

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

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

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

@ -27,7 +27,7 @@ const translation = {
},
uploader: {
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',
tip: 'Destekler {{supportTypes}}. Her biri en fazla {{size}}MB.',
validation: {

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

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

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

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
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',
tip: 'Hỗ trợ {{supportTypes}}. Tối đa {{size}}MB mỗi tệp.',
validation: {

@ -26,6 +26,8 @@ const translation = {
},
tryToSolve: 'Thử giải quyết',
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: {
tabs: {

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

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

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

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

@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "1.3.0",
"version": "1.3.1",
"private": true,
"engines": {
"node": ">=v22.11.0"
@ -36,13 +36,13 @@
"@headlessui/react": "^2.2.0",
"@heroicons/react": "^2.0.16",
"@hookform/resolvers": "^3.9.0",
"@lexical/code": "^0.18.0",
"@lexical/link": "^0.18.0",
"@lexical/list": "^0.18.0",
"@lexical/react": "^0.18.0",
"@lexical/selection": "^0.18.0",
"@lexical/text": "^0.18.0",
"@lexical/utils": "^0.18.0",
"@lexical/code": "^0.30.0",
"@lexical/link": "^0.30.0",
"@lexical/list": "^0.30.0",
"@lexical/react": "^0.30.0",
"@lexical/selection": "^0.30.0",
"@lexical/text": "^0.30.0",
"@lexical/utils": "^0.30.0",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@monaco-editor/react": "^4.6.0",
@ -84,7 +84,7 @@
"katex": "^0.16.21",
"ky": "^1.7.2",
"lamejs": "^1.2.1",
"lexical": "^0.18.0",
"lexical": "^0.30.0",
"line-clamp": "^1.0.0",
"lodash-es": "^4.17.21",
"mermaid": "11.4.1",

@ -41,26 +41,26 @@ importers:
specifier: ^3.9.0
version: 3.10.0(react-hook-form@7.55.0(react@19.0.0))
'@lexical/code':
specifier: ^0.18.0
version: 0.18.0
specifier: ^0.30.0
version: 0.30.0
'@lexical/link':
specifier: ^0.18.0
version: 0.18.0
specifier: ^0.30.0
version: 0.30.0
'@lexical/list':
specifier: ^0.18.0
version: 0.18.0
specifier: ^0.30.0
version: 0.30.0
'@lexical/react':
specifier: ^0.18.0
version: 0.18.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.24)
specifier: ^0.30.0
version: 0.30.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.24)
'@lexical/selection':
specifier: ^0.18.0
version: 0.18.0
specifier: ^0.30.0
version: 0.30.0
'@lexical/text':
specifier: ^0.18.0
version: 0.18.0
specifier: ^0.30.0
version: 0.30.0
'@lexical/utils':
specifier: ^0.18.0
version: 0.18.0
specifier: ^0.30.0
version: 0.30.0
'@mdx-js/loader':
specifier: ^3.1.0
version: 3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.24.2)(uglify-js@3.19.3))
@ -185,8 +185,8 @@ importers:
specifier: ^1.2.1
version: 1.2.1
lexical:
specifier: ^0.18.0
version: 0.18.0
specifier: ^0.30.0
version: 0.30.0
line-clamp:
specifier: ^1.0.0
version: 1.0.0
@ -1977,74 +1977,74 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@lexical/clipboard@0.18.0':
resolution: {integrity: sha512-ybc+hx14wj0n2ZjdOkLcZ02MRB3UprXjpLDXlByFIuVcZpUxVcp3NzA0UBPOKXYKvdt0bmgjnAsFWM5OSbwS0w==}
'@lexical/clipboard@0.30.0':
resolution: {integrity: sha512-taWQURtE6xF4Jy4I8teQw3+nVBVNO1r+9N9voXeivgwxSrAM40rjqQ/aZEKxWbwZtfkABDkCEArbVrqP0SkWcQ==}
'@lexical/code@0.18.0':
resolution: {integrity: sha512-VB8fRHIrB8QTqyZUvGBMVWP2tpKe3ArOjPdWAqgrS8MVFldqUhuTHcW+XJFkVxcEBYCXynNT29YRYtQhfQ+vDQ==}
'@lexical/code@0.30.0':
resolution: {integrity: sha512-OmA6Bmp3w9SMV25Hae1dLXtPNOdCgnzo1xy84K19U+dPP5iqXagwFq5oY/9PVOOI2wgaQHrz3C+7B4phDb9xaA==}
'@lexical/devtools-core@0.18.0':
resolution: {integrity: sha512-gVgtEkLwGjz1frOmDpFJzDPFxPgAcC9n5ZaaZWHo5GLcptnQmkuLm1t+UInQWujXhFmcyJzfiqDaMJ8EIcb2Ww==}
'@lexical/devtools-core@0.30.0':
resolution: {integrity: sha512-6vKEEIUym8pQ+tWt4VfRMOGE/dtfyPr9e1zPrAAV7Y/EdzK0AJYPPlw2Dt5Uqq9rposcIriqF4MkuFvy4UcZiQ==}
peerDependencies:
react: '>=17.x'
react-dom: '>=17.x'
'@lexical/dragon@0.18.0':
resolution: {integrity: sha512-toD/y2/TgtG+eFVKXf65kDk/Mv02FwgmcGH18nyAabZnO1TLBaMYPkGFdTTZ8hVmQxqIu9nZuLWUbdIBMs8UWw==}
'@lexical/dragon@0.30.0':
resolution: {integrity: sha512-eikVYw1pIcFIOojn2mGlps59YcyT9ATd6UMIx/ivuscakrZeU7SZM/F6c75QPJXNOu1b2koOo+4Bb1GT6jixGQ==}
'@lexical/hashtag@0.18.0':
resolution: {integrity: sha512-bm+Sv7keguVYbUY0ngd+iAv2Owd3dePzdVkzkmw9Al8GPXkE5ll8fjq6Xjw2u3OVhf+9pTnesIo/AS7H+h0exw==}
'@lexical/hashtag@0.30.0':
resolution: {integrity: sha512-gB3DobSdAc0YZUhlTT7ZAUr+6RRREQ3UWVC1twdtFvXXw1vyTUXH2gWTDp/ParwBZ16Lnrg8mxET8Nu/qD1PSw==}
'@lexical/history@0.18.0':
resolution: {integrity: sha512-c87J4ke1Sae03coElJay2Ikac/4OcA2OmhtNbt2gAi/XBtcsP4mPuz1yZfZf9XIe+weekObgjinvZekQ2AFw0g==}
'@lexical/history@0.30.0':
resolution: {integrity: sha512-dxudthi94vSLQKXVq3LSwcOVkOmb2lvxoy7sCma513yJbrsn3fPLppR2Ynhl6aB9oPw675wSDrfsE6BG3U3+CA==}
'@lexical/html@0.18.0':
resolution: {integrity: sha512-8lhba1DFnnobXgYm4Rk5Gr2tZedD4Gl6A/NKCt7whO/CET63vT3UnK2ggcVVgtIJG530Cv0bdZoJbJu5DauI5w==}
'@lexical/html@0.30.0':
resolution: {integrity: sha512-GdegWO6RjJ7eE+yD3Z0X/OpT88SZjOs3DyQ0rgrZy3z7RPaFCbEEcq0M/NssJbKAB1XOFUsUFrnS7kZs1vJzGg==}
'@lexical/link@0.18.0':
resolution: {integrity: sha512-GCYcbNTSTwJk0lr+GMc8nn6Meq44BZs3QL2d1B0skpZAspd8yI53sRS6HDy5P+jW5P0dzyZr/XJAU4U+7zsEEg==}
'@lexical/link@0.30.0':
resolution: {integrity: sha512-isD3PC0ywQIwbtekHYEvh7hDxcPz/cEr/AspYntYs08u5J0czhw3rpqnXWGauWaav5V9ExIkf1ZkGUFUI6bw5w==}
'@lexical/list@0.18.0':
resolution: {integrity: sha512-DEWs9Scbg3+STZeE2O0OoG8SWnKnxQccObBzyeHRjn4GAN6JA7lgcAzfrdgp0fNWTbMM/ku876MmXKGnqhvg9Q==}
'@lexical/list@0.30.0':
resolution: {integrity: sha512-WKnwH+Cg+j2I0EbaEyPHo8MPNyrqQV3W1NmH5Mf/iRxCq42z7NJxemhmRUxbqv8vsugACwBkh2RlkhekRXmUQQ==}
'@lexical/mark@0.18.0':
resolution: {integrity: sha512-QA4YWfTP5WWnCnoH/RmfcsSZyhhd7oeFWDpfP7S8Bbmhz6kiPwGcsVr+uRQBBT56AqEX167xX2rX8JR6FiYZqA==}
'@lexical/mark@0.30.0':
resolution: {integrity: sha512-dLFH6tJ2WQUSdo1Y2Jp81vRT8j48FjF75K5YLRsKD/UFxWEy+RFgRXsd0H/BuFkx/jPTXt6xe8CaIrZvek8mLg==}
'@lexical/markdown@0.18.0':
resolution: {integrity: sha512-uSWwcK8eJw5C+waEhU5WoX8W+JxNZbKuFnZwsn5nsp+iQgqMj4qY6g0yJub4sq8vvh6jjl4vVXhXTq2up9aykw==}
'@lexical/markdown@0.30.0':
resolution: {integrity: sha512-GGddZs63k0wb3/fdL7JyBjiy8L1AIHuRKT68riWbKAcNL7rfMl3Uy5VnMkgV/5bN/2eUQijkGjxG+VxsR8RWbw==}
'@lexical/offset@0.18.0':
resolution: {integrity: sha512-KGlboyLSxQAH5PMOlJmyvHlbYXZneVnKiHpfyBV5IUX5kuyB/eZbQEYcJP9saekfQ5Xb1FWXWmsZEo+sWtrrZA==}
'@lexical/offset@0.30.0':
resolution: {integrity: sha512-sZFbZt5dVdtrdoYk79i13xBDs8/MHXw6CqmZNht85L7UdwiuzVqA3KTyaMe60Vrg6mfsKIVjghbpMOhspcuCrw==}
'@lexical/overflow@0.18.0':
resolution: {integrity: sha512-3ATTwttVgZtVLq60ZUWbpbXBbpuMa3PZD5CxSP3nulviL+2I4phvacV4WUN+8wMeq+PGmuarl+cYfrFL02ii3g==}
'@lexical/overflow@0.30.0':
resolution: {integrity: sha512-fvjWnhtPZLMS3qJ6HC6tZTOMmcfNmeRUkgXTas9bvWT8Yul+WLJ/fWjzwvBcqpKlvPQjRFOcDcrW8T/Rp7KPrg==}
'@lexical/plain-text@0.18.0':
resolution: {integrity: sha512-L6yQpiwW0ZacY1oNwvRBxSuW2TZaUcveZLheJc8JzGcZoVxzII/CAbLZG8691VbNuKsbOURiNXZIsgwujKmo4Q==}
'@lexical/plain-text@0.30.0':
resolution: {integrity: sha512-jvxMMxFO3Yuj7evWsc33IGWfigU5A1KrJaIf6zv6GmYj0a7ZRkR1x6vJyc7AlgUM70sld+dozLdoynguQIlmrQ==}
'@lexical/react@0.18.0':
resolution: {integrity: sha512-DLvIbTsjvFIFqm+9zvAjEwuZHAbSxzZf1AGqf1lLctlL/Ran0f+8EZOv5jttELTe7xISZ2+xSXTLRfyxhNwGXQ==}
'@lexical/react@0.30.0':
resolution: {integrity: sha512-fsb6voXzxHyP55lXdmnGhHMfxe6g/f+0NpmfPCkutOXYnY8UqKa86LLYl4Nrsi8HX8BRZfh1H0IjkzDG6EzVPw==}
peerDependencies:
react: '>=17.x'
react-dom: '>=17.x'
'@lexical/rich-text@0.18.0':
resolution: {integrity: sha512-xMANCB7WueMsmWK8qxik5FZN4ApyaHWHQILS9r4FTbdv/DlNepsR7Pt8kg2317xZ56NAueQLIdyyKYXG1nBrHw==}
'@lexical/rich-text@0.30.0':
resolution: {integrity: sha512-oitOh5u68E5DBZt5VBZIaIeM/iNdt3mIDkGp2C259x81V/9KlSNB9c3rqdTKcs/A+Msw4j60FRhdmZcKQ9uYUA==}
'@lexical/selection@0.18.0':
resolution: {integrity: sha512-mJoMhmxeZLfM9K2JMYETs9u179IkHQUlgtYG5GZJHjKx2iUn+9KvJ9RVssq+Lusi7C/N42wWPGNHDPdUvFtxXg==}
'@lexical/selection@0.30.0':
resolution: {integrity: sha512-Ys2XfSmIV/Irg6Xo663YtR4jozIv/7sDemArkEGHT0fxZn2py5qftowPF5IBqFYxKTigAdv5vVPwusBvAnLIEg==}
'@lexical/table@0.18.0':
resolution: {integrity: sha512-TeTAnuFAAgVjm1QE8adRB3GFWN+DUUiS4vzGq+ynPRCtNdpmW27NmTkRMyxKsetUtt7nIFfj4DvLvor4RwqIpA==}
'@lexical/table@0.30.0':
resolution: {integrity: sha512-XPCIMIGnZLKTa5/4cP16bXbmzvMndPR273HNl7ZaF35ky7UjZxdj42HBbE7q9zw2zbRPDiO77EyhYA0p20cbdw==}
'@lexical/text@0.18.0':
resolution: {integrity: sha512-MTHSBeq3K0+lqSsP5oysBMnY4tPVhB8kAa2xBnEc3dYgXFxEEvJwZahbHNX93EPObtJkxXfUuI63Al4G3lYK8A==}
'@lexical/text@0.30.0':
resolution: {integrity: sha512-P0ptriFwwP/hoDpz/MoBbzHxrFHqh0kCGzASWUdRZ1zrU0yPvJ9vV/UNMhyolH7xx+eAGI1Yl+m74NlpGmXqTg==}
'@lexical/utils@0.18.0':
resolution: {integrity: sha512-4s9dVpBZjqIaA/1q2GtfWFjKsv2Wqhjer0Zw2mcl1TIVN0zreXxcTKN316QppAWmSQJxVGvkWHjjaZJwl6/TSw==}
'@lexical/utils@0.30.0':
resolution: {integrity: sha512-VJlAUhupCZmnbYYX3zMWovd4viu2guR01sAqKGbbOMbP+4rlaymixFbinvNPaRKDBloOARi+fpiveQFxnyr/Ew==}
'@lexical/yjs@0.18.0':
resolution: {integrity: sha512-rl7Rl9XIb3ygQEEHOFtACdXs3BE+UUUmdyNqB6kK9A6IRGz+w4Azp+qzt8It/t+c0oaSYHpAtcLNXg1amJz+kA==}
'@lexical/yjs@0.30.0':
resolution: {integrity: sha512-mWGFAGpUPz4JoSV+Y0cZOzOZJoMLbVb/enldxEbV0xX71BBVzD0c0vjPxuaIJ9MtNkRZdK3eOubj+B45iOECtw==}
peerDependencies:
yjs: '>=13.5.22'
@ -6100,8 +6100,8 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lexical@0.18.0:
resolution: {integrity: sha512-3K/B0RpzjoW+Wj2E455wWXxkqxqK8UgdIiuqkOqdOsoSSo5mCkHOU6eVw7Nlmlr1MFvAMzGmz4RPn8NZaLQ2Mw==}
lexical@0.30.0:
resolution: {integrity: sha512-6gxYeXaJiAcreJD0whCofvO0MuJmnWoIgIl1w7L5FTigfhnEohuCx2SoI/oywzfzXE9gzZnyr3rVvZrMItPL8A==}
lib0@0.2.102:
resolution: {integrity: sha512-g70kydI0I1sZU0ChO8mBbhw0oUW/8U0GHzygpvEIx8k+jgOpqnTSb/E+70toYVqHxBhrERD21TwD5QcZJQ40ZQ==}
@ -10329,151 +10329,149 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@lexical/clipboard@0.18.0':
'@lexical/clipboard@0.30.0':
dependencies:
'@lexical/html': 0.18.0
'@lexical/list': 0.18.0
'@lexical/selection': 0.18.0
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/html': 0.30.0
'@lexical/list': 0.30.0
'@lexical/selection': 0.30.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/code@0.18.0':
'@lexical/code@0.30.0':
dependencies:
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
prismjs: 1.30.0
'@lexical/devtools-core@0.18.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
'@lexical/devtools-core@0.30.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@lexical/html': 0.18.0
'@lexical/link': 0.18.0
'@lexical/mark': 0.18.0
'@lexical/table': 0.18.0
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/html': 0.30.0
'@lexical/link': 0.30.0
'@lexical/mark': 0.30.0
'@lexical/table': 0.30.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
'@lexical/dragon@0.18.0':
'@lexical/dragon@0.30.0':
dependencies:
lexical: 0.18.0
lexical: 0.30.0
'@lexical/hashtag@0.18.0':
'@lexical/hashtag@0.30.0':
dependencies:
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/history@0.18.0':
'@lexical/history@0.30.0':
dependencies:
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/html@0.18.0':
'@lexical/html@0.30.0':
dependencies:
'@lexical/selection': 0.18.0
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/selection': 0.30.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/link@0.18.0':
'@lexical/link@0.30.0':
dependencies:
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/list@0.18.0':
'@lexical/list@0.30.0':
dependencies:
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/selection': 0.30.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/mark@0.18.0':
'@lexical/mark@0.30.0':
dependencies:
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/markdown@0.18.0':
'@lexical/markdown@0.30.0':
dependencies:
'@lexical/code': 0.18.0
'@lexical/link': 0.18.0
'@lexical/list': 0.18.0
'@lexical/rich-text': 0.18.0
'@lexical/text': 0.18.0
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/code': 0.30.0
'@lexical/link': 0.30.0
'@lexical/list': 0.30.0
'@lexical/rich-text': 0.30.0
'@lexical/text': 0.30.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/offset@0.18.0':
'@lexical/offset@0.30.0':
dependencies:
lexical: 0.18.0
lexical: 0.30.0
'@lexical/overflow@0.18.0':
'@lexical/overflow@0.30.0':
dependencies:
lexical: 0.18.0
lexical: 0.30.0
'@lexical/plain-text@0.18.0':
'@lexical/plain-text@0.30.0':
dependencies:
'@lexical/clipboard': 0.18.0
'@lexical/selection': 0.18.0
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/clipboard': 0.30.0
'@lexical/selection': 0.30.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/react@0.18.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.24)':
'@lexical/react@0.30.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.24)':
dependencies:
'@lexical/clipboard': 0.18.0
'@lexical/code': 0.18.0
'@lexical/devtools-core': 0.18.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@lexical/dragon': 0.18.0
'@lexical/hashtag': 0.18.0
'@lexical/history': 0.18.0
'@lexical/link': 0.18.0
'@lexical/list': 0.18.0
'@lexical/mark': 0.18.0
'@lexical/markdown': 0.18.0
'@lexical/overflow': 0.18.0
'@lexical/plain-text': 0.18.0
'@lexical/rich-text': 0.18.0
'@lexical/selection': 0.18.0
'@lexical/table': 0.18.0
'@lexical/text': 0.18.0
'@lexical/utils': 0.18.0
'@lexical/yjs': 0.18.0(yjs@13.6.24)
lexical: 0.18.0
'@lexical/devtools-core': 0.30.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@lexical/dragon': 0.30.0
'@lexical/hashtag': 0.30.0
'@lexical/history': 0.30.0
'@lexical/link': 0.30.0
'@lexical/list': 0.30.0
'@lexical/mark': 0.30.0
'@lexical/markdown': 0.30.0
'@lexical/overflow': 0.30.0
'@lexical/plain-text': 0.30.0
'@lexical/rich-text': 0.30.0
'@lexical/table': 0.30.0
'@lexical/text': 0.30.0
'@lexical/utils': 0.30.0
'@lexical/yjs': 0.30.0(yjs@13.6.24)
lexical: 0.30.0
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-error-boundary: 3.1.4(react@19.0.0)
transitivePeerDependencies:
- yjs
'@lexical/rich-text@0.18.0':
'@lexical/rich-text@0.30.0':
dependencies:
'@lexical/clipboard': 0.18.0
'@lexical/selection': 0.18.0
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/clipboard': 0.30.0
'@lexical/selection': 0.30.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/selection@0.18.0':
'@lexical/selection@0.30.0':
dependencies:
lexical: 0.18.0
lexical: 0.30.0
'@lexical/table@0.18.0':
'@lexical/table@0.30.0':
dependencies:
'@lexical/clipboard': 0.18.0
'@lexical/utils': 0.18.0
lexical: 0.18.0
'@lexical/clipboard': 0.30.0
'@lexical/utils': 0.30.0
lexical: 0.30.0
'@lexical/text@0.18.0':
'@lexical/text@0.30.0':
dependencies:
lexical: 0.18.0
lexical: 0.30.0
'@lexical/utils@0.18.0':
'@lexical/utils@0.30.0':
dependencies:
'@lexical/list': 0.18.0
'@lexical/selection': 0.18.0
'@lexical/table': 0.18.0
lexical: 0.18.0
'@lexical/list': 0.30.0
'@lexical/selection': 0.30.0
'@lexical/table': 0.30.0
lexical: 0.30.0
'@lexical/yjs@0.18.0(yjs@13.6.24)':
'@lexical/yjs@0.30.0(yjs@13.6.24)':
dependencies:
'@lexical/offset': 0.18.0
'@lexical/selection': 0.18.0
lexical: 0.18.0
'@lexical/offset': 0.30.0
'@lexical/selection': 0.30.0
lexical: 0.30.0
yjs: 13.6.24
'@mapbox/node-pre-gyp@1.0.11':
@ -15557,7 +15555,7 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lexical@0.18.0: {}
lexical@0.30.0: {}
lib0@0.2.102:
dependencies:

@ -12,6 +12,7 @@
const buttonId = "dify-chatbot-bubble-button";
const iframeId = "dify-chatbot-bubble-window";
const config = window[configKey];
let isExpanded = false;
// 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">
@ -22,6 +23,53 @@
</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
async function embedChatbot() {
let isDragging = false
@ -71,6 +119,7 @@
const baseUrl =
config.baseUrl || `https://${config.isDev ? "dev." : ""}udify.app`;
const targetOrigin = new URL(baseUrl).origin;
// pre-check the length of the URL
const iframeUrl = `${baseUrl}/chatbot/${config.token}?${params}`;
@ -92,23 +141,7 @@
iframe.title = "dify chatbot bubble window";
iframe.id = iframeId;
iframe.src = iframeUrl;
iframe.style.cssText = `
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;
`;
iframe.style.cssText = originalIframeStyleText;
return iframe;
}
@ -121,28 +154,69 @@
const targetButton = document.getElementById(buttonId);
if (targetIframe && targetButton) {
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) {
targetIframe.style.bottom = "0px";
targetIframe.style.top = "unset";
if (buttonCenterX < viewportCenterX) {
targetIframe.style.left = `var(--${buttonId}-right, 1rem)`;
targetIframe.style.right = 'unset';
} else {
targetIframe.style.bottom = "unset";
targetIframe.style.top = "0px";
targetIframe.style.right = `var(--${buttonId}-right, 1rem)`;
targetIframe.style.left = 'unset';
}
}
}
const buttonInRight = buttonRect.right > targetIframe.clientWidth;
function toggleExpand() {
isExpanded = !isExpanded;
if (buttonInRight) {
targetIframe.style.right = "0";
targetIframe.style.left = "unset";
const targetIframe = document.getElementById(iframeId);
if (!targetIframe) return;
if (isExpanded) {
targetIframe.style.cssText = expandedIframeStyleText;
} else {
targetIframe.style.right = "unset";
targetIframe.style.left = 0;
targetIframe.style.cssText = originalIframeStyleText;
}
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 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;
display: flex;
flex-direction: column;
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;
right: 0;
bottom: 0;
width: 24rem;
max-width: calc(100vw - 2rem);
height: 43.75rem;
@ -14,7 +15,10 @@
z-index: 2147483640;
overflow: hidden;
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} {
position: fixed;
bottom: var(--${n.id}-bottom, 1rem);
@ -29,10 +33,10 @@
cursor: pointer;
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"/>
</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"/>
</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