diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md
index 0478d2e1fa..69ae7071bb 100644
--- a/CONTRIBUTING_CN.md
+++ b/CONTRIBUTING_CN.md
@@ -6,7 +6,7 @@
本指南和 Dify 一样在不断完善中。如果有任何滞后于项目实际情况的地方,恳请谅解,我们也欢迎任何改进建议。
-关于许可证,请花一分钟阅读我们简短的[许可和贡献者协议](./LICENSE)。社区同时也遵循[行为准则](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md)。
+关于许可证,请花一分钟阅读我们简短的[许可和贡献者协议](./LICENSE)。同时也请遵循社区[行为准则](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md)。
## 开始之前
diff --git a/README.md b/README.md
index 87ebc9bafc..65e8001dd2 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
Dify Cloud ·
Self-hosting ·
Documentation ·
- Enterprise inquiry
+ Dify edition overview
diff --git a/README_AR.md b/README_AR.md
index e58f59da5d..4f93802fda 100644
--- a/README_AR.md
+++ b/README_AR.md
@@ -4,7 +4,7 @@
Dify Cloud ·
الاستضافة الذاتية ·
التوثيق ·
- استفسار الشركات (للإنجليزية فقط)
+ نظرة عامة على منتجات Dify
diff --git a/README_BN.md b/README_BN.md
index 3ebc81af5d..7599fae9ff 100644
--- a/README_BN.md
+++ b/README_BN.md
@@ -8,7 +8,7 @@
ডিফাই ক্লাউড ·
সেল্ফ-হোস্টিং ·
ডকুমেন্টেশন ·
- ব্যাবসায়িক অনুসন্ধান
+ Dify পণ্যের রূপভেদ
diff --git a/README_CN.md b/README_CN.md
index 6d3c601100..973629f459 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -4,7 +4,7 @@
Dify 云服务 ·
自托管 ·
文档 ·
- (需用英文)常见问题解答 / 联系团队
+ Dify 产品形态总览
diff --git a/README_DE.md b/README_DE.md
index b3b9bf3221..738c0e3b67 100644
--- a/README_DE.md
+++ b/README_DE.md
@@ -8,7 +8,7 @@
Dify Cloud ·
Selbstgehostetes ·
Dokumentation ·
- Anfrage an Unternehmen
+ Überblick über die Dify-Produkte
diff --git a/README_ES.md b/README_ES.md
index d14afdd2eb..212268b73d 100644
--- a/README_ES.md
+++ b/README_ES.md
@@ -4,7 +4,7 @@
Dify Cloud ·
Auto-alojamiento ·
Documentación ·
- Consultas empresariales (en inglés)
+ Resumen de las ediciones de Dify
diff --git a/README_FR.md b/README_FR.md
index 031196303e..89eea7d058 100644
--- a/README_FR.md
+++ b/README_FR.md
@@ -4,7 +4,7 @@
Dify Cloud ·
Auto-hébergement ·
Documentation ·
- Demande d’entreprise (en anglais seulement)
+ Présentation des différentes offres Dify
diff --git a/README_JA.md b/README_JA.md
index 3b7a6f50db..adca219753 100644
--- a/README_JA.md
+++ b/README_JA.md
@@ -4,7 +4,7 @@
Dify Cloud ·
セルフホスティング ·
ドキュメント ·
- 企業のお問い合わせ(英語のみ)
+ Difyの各種エディションについて
diff --git a/README_KL.md b/README_KL.md
index ccadb77274..17e6c9d509 100644
--- a/README_KL.md
+++ b/README_KL.md
@@ -4,7 +4,7 @@
Dify Cloud ·
Self-hosting ·
Documentation ·
- Commercial enquiries
+ Dify product editions
diff --git a/README_KR.md b/README_KR.md
index c1a98f8b68..d44723f9b6 100644
--- a/README_KR.md
+++ b/README_KR.md
@@ -4,7 +4,7 @@
Dify 클라우드 ·
셀프-호스팅 ·
문서 ·
- 기업 문의 (영어만 가능)
+ Dify 제품 에디션 안내
diff --git a/README_PT.md b/README_PT.md
index 5b3c782645..9dc2207279 100644
--- a/README_PT.md
+++ b/README_PT.md
@@ -8,7 +8,7 @@
Dify Cloud ·
Auto-hospedagem ·
Documentação ·
- Consultas empresariais
+ Visão geral das edições do Dify
diff --git a/README_SI.md b/README_SI.md
index 7c0867c776..caa5975973 100644
--- a/README_SI.md
+++ b/README_SI.md
@@ -8,7 +8,7 @@
Dify Cloud ·
Samostojno gostovanje ·
Dokumentacija ·
- Povpraševanje za podjetja
+ Pregled ponudb izdelkov Dify
diff --git a/README_TR.md b/README_TR.md
index f8890b00ef..ab2853a019 100644
--- a/README_TR.md
+++ b/README_TR.md
@@ -4,7 +4,7 @@
Dify Bulut ·
Kendi Sunucunuzda Barındırma ·
Dokümantasyon ·
- Yalnızca İngilizce: Kurumsal Sorgulama
+ Dify ürün seçeneklerine genel bakış
diff --git a/README_TW.md b/README_TW.md
index 260f1e80ac..8263a22b64 100644
--- a/README_TW.md
+++ b/README_TW.md
@@ -8,7 +8,7 @@
Dify 雲端服務 ·
自行託管 ·
說明文件 ·
- 企業諮詢
+ 產品方案概覽
diff --git a/README_VI.md b/README_VI.md
index 15d2d5ae80..852ed7aaa0 100644
--- a/README_VI.md
+++ b/README_VI.md
@@ -4,7 +4,7 @@
Dify Cloud ·
Tự triển khai ·
Tài liệu ·
- Yêu cầu doanh nghiệp
+ Tổng quan các lựa chọn sản phẩm Dify
diff --git a/api/.env.example b/api/.env.example
index 01ddb4adfd..b5820fcdc2 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -482,4 +482,7 @@ OTEL_MAX_QUEUE_SIZE=2048
OTEL_MAX_EXPORT_BATCH_SIZE=512
OTEL_METRIC_EXPORT_INTERVAL=60000
OTEL_BATCH_EXPORT_TIMEOUT=10000
-OTEL_METRIC_EXPORT_TIMEOUT=30000
\ No newline at end of file
+OTEL_METRIC_EXPORT_TIMEOUT=30000
+
+# Prevent Clickjacking
+ALLOW_EMBED=false
diff --git a/api/app_factory.py b/api/app_factory.py
index 586f2ded9e..9648d770ab 100644
--- a/api/app_factory.py
+++ b/api/app_factory.py
@@ -52,6 +52,7 @@ def initialize_extensions(app: DifyApp):
ext_mail,
ext_migrate,
ext_otel,
+ ext_otel_patch,
ext_proxy_fix,
ext_redis,
ext_repositories,
@@ -84,6 +85,7 @@ 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:
diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py
index c7aedc5b8a..a33c7727dc 100644
--- a/api/configs/packaging/__init__.py
+++ b/api/configs/packaging/__init__.py
@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field(
description="Dify version",
- default="1.2.0",
+ default="1.3.0",
)
COMMIT_SHA: str = Field(
diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py
index 12d9157dda..7519ae96c0 100644
--- a/api/controllers/console/app/audio.py
+++ b/api/controllers/console/app/audio.py
@@ -80,8 +80,6 @@ class ChatMessageTextApi(Resource):
@account_initialization_required
@get_app_model
def post(self, app_model: App):
- from werkzeug.exceptions import InternalServerError
-
try:
parser = reqparse.RequestParser()
parser.add_argument("message_id", type=str, location="json")
diff --git a/api/controllers/console/workspace/endpoint.py b/api/controllers/console/workspace/endpoint.py
index a5bd2a4bcf..46dee20f8b 100644
--- a/api/controllers/console/workspace/endpoint.py
+++ b/api/controllers/console/workspace/endpoint.py
@@ -5,6 +5,7 @@ from werkzeug.exceptions import Forbidden
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required
from core.model_runtime.utils.encoders import jsonable_encoder
+from core.plugin.manager.exc import PluginPermissionDeniedError
from libs.login import login_required
from services.plugin.endpoint_service import EndpointService
@@ -28,15 +29,18 @@ class EndpointCreateApi(Resource):
settings = args["settings"]
name = args["name"]
- return {
- "success": EndpointService.create_endpoint(
- tenant_id=user.current_tenant_id,
- user_id=user.id,
- plugin_unique_identifier=plugin_unique_identifier,
- name=name,
- settings=settings,
- )
- }
+ try:
+ return {
+ "success": EndpointService.create_endpoint(
+ tenant_id=user.current_tenant_id,
+ user_id=user.id,
+ plugin_unique_identifier=plugin_unique_identifier,
+ name=name,
+ settings=settings,
+ )
+ }
+ except PluginPermissionDeniedError as e:
+ raise ValueError(e.description) from e
class EndpointListApi(Resource):
diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py
index 48c92ea2db..e648613605 100644
--- a/api/core/agent/base_agent_runner.py
+++ b/api/core/agent/base_agent_runner.py
@@ -21,14 +21,13 @@ from core.model_runtime.entities import (
AssistantPromptMessage,
LLMUsage,
PromptMessage,
- PromptMessageContent,
PromptMessageTool,
SystemPromptMessage,
TextPromptMessageContent,
ToolPromptMessage,
UserPromptMessage,
)
-from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.model_runtime.entities.model_entities import ModelFeature
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
from core.prompt.utils.extract_thread_messages import extract_thread_messages
@@ -501,7 +500,7 @@ class BaseAgentRunner(AppRunner):
)
if not file_objs:
return UserPromptMessage(content=message.query)
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=message.query))
for file in file_objs:
prompt_message_contents.append(
diff --git a/api/core/agent/cot_chat_agent_runner.py b/api/core/agent/cot_chat_agent_runner.py
index 7d407a4976..5ff89bdacb 100644
--- a/api/core/agent/cot_chat_agent_runner.py
+++ b/api/core/agent/cot_chat_agent_runner.py
@@ -5,12 +5,11 @@ from core.file import file_manager
from core.model_runtime.entities import (
AssistantPromptMessage,
PromptMessage,
- PromptMessageContent,
SystemPromptMessage,
TextPromptMessageContent,
UserPromptMessage,
)
-from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.model_runtime.utils.encoders import jsonable_encoder
@@ -40,7 +39,7 @@ class CotChatAgentRunner(CotAgentRunner):
Organize user query
"""
if self.files:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=query))
# get image detail config
diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py
index f45fa5c66e..a1110e7709 100644
--- a/api/core/agent/fc_agent_runner.py
+++ b/api/core/agent/fc_agent_runner.py
@@ -15,14 +15,13 @@ from core.model_runtime.entities import (
LLMResultChunkDelta,
LLMUsage,
PromptMessage,
- PromptMessageContent,
PromptMessageContentType,
SystemPromptMessage,
TextPromptMessageContent,
ToolPromptMessage,
UserPromptMessage,
)
-from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform
from core.tools.entities.tool_entities import ToolInvokeMeta
from core.tools.tool_engine import ToolEngine
@@ -395,7 +394,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
Organize user query
"""
if self.files:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=query))
# get image detail config
diff --git a/api/core/app/apps/message_based_app_generator.py b/api/core/app/apps/message_based_app_generator.py
index b1f527c0f2..995082b79d 100644
--- a/api/core/app/apps/message_based_app_generator.py
+++ b/api/core/app/apps/message_based_app_generator.py
@@ -153,6 +153,8 @@ class MessageBasedAppGenerator(BaseAppGenerator):
query = application_generate_entity.query or "New conversation"
else:
query = next(iter(application_generate_entity.inputs.values()), "New conversation")
+ if isinstance(query, int):
+ query = str(query)
query = query or "New conversation"
conversation_name = (query[:20] + "…") if len(query) > 20 else query
diff --git a/api/core/file/file_manager.py b/api/core/file/file_manager.py
index 4ebe997ac5..9a204e9ff6 100644
--- a/api/core/file/file_manager.py
+++ b/api/core/file/file_manager.py
@@ -7,9 +7,9 @@ from core.model_runtime.entities import (
AudioPromptMessageContent,
DocumentPromptMessageContent,
ImagePromptMessageContent,
- MultiModalPromptMessageContent,
VideoPromptMessageContent,
)
+from core.model_runtime.entities.message_entities import PromptMessageContentUnionTypes
from extensions.ext_storage import storage
from . import helpers
@@ -43,7 +43,7 @@ def to_prompt_message_content(
/,
*,
image_detail_config: ImagePromptMessageContent.DETAIL | None = None,
-) -> MultiModalPromptMessageContent:
+) -> PromptMessageContentUnionTypes:
if f.extension is None:
raise ValueError("Missing file extension")
if f.mime_type is None:
@@ -58,7 +58,7 @@ def to_prompt_message_content(
if f.type == FileType.IMAGE:
params["detail"] = image_detail_config or ImagePromptMessageContent.DETAIL.LOW
- prompt_class_map: Mapping[FileType, type[MultiModalPromptMessageContent]] = {
+ prompt_class_map: Mapping[FileType, type[PromptMessageContentUnionTypes]] = {
FileType.IMAGE: ImagePromptMessageContent,
FileType.AUDIO: AudioPromptMessageContent,
FileType.VIDEO: VideoPromptMessageContent,
diff --git a/api/core/llm_generator/prompts.py b/api/core/llm_generator/prompts.py
index 82d22d7f89..fad7cea01c 100644
--- a/api/core/llm_generator/prompts.py
+++ b/api/core/llm_generator/prompts.py
@@ -1,7 +1,7 @@
# Written by YORKI MINAKO🤡, Edited by Xiaoyi
CONVERSATION_TITLE_PROMPT = """You need to decompose the user's input into "subject" and "intention" in order to accurately figure out what the user's input language actually is.
-Notice: the language type user use could be diverse, which can be English, Chinese, Italian, Español, Arabic, Japanese, French, and etc.
-MAKE SURE your output is the SAME language as the user's input!
+Notice: the language type user uses could be diverse, which can be English, Chinese, Italian, Español, Arabic, Japanese, French, and etc.
+ENSURE your output is in the SAME language as the user's input!
Your output is restricted only to: (Input language) Intention + Subject(short as possible)
Your output MUST be a valid JSON.
@@ -19,7 +19,7 @@ User Input: hi, yesterday i had some burgers.
example 2:
User Input: hello
{
- "Language Type": "The user's input is written in pure English",
+ "Language Type": "The user's input is pure English",
"Your Reasoning": "The language of my output must be pure English.",
"Your Output": "Greeting myself☺️"
}
@@ -46,7 +46,7 @@ example 5:
User Input: why小红的年龄is老than小明?
{
"Language Type": "The user's input is English-Chinese mixed",
- "Your Reasoning": "The English parts are subjective particles, the main intention is written in Chinese, besides, Chinese occupies a greater \"actual meaning\" than English, so the language of my output must be using Chinese.",
+ "Your Reasoning": "The English parts are filler words, the main intention is written in Chinese, besides, Chinese occupies a greater \"actual meaning\" than English, so the language of my output must be using Chinese.",
"Your Output": "询问小红和小明的年龄"
}
@@ -114,6 +114,13 @@ JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE = (
"4. The returned object should contain at least one key-value pair.\n\n"
"5. The returned object should always be in the format: {result: ...}\n\n"
"Example:\n"
+ "/**\n"
+ " * Multiplies two numbers together.\n"
+ " *\n"
+ " * @param {number} arg1 - The first number to multiply.\n"
+ " * @param {number} arg2 - The second number to multiply.\n"
+ " * @returns {{ result: number }} The result of the multiplication.\n"
+ " */\n"
"function main(arg1, arg2) {\n"
" return {\n"
" result: arg1 * arg2\n"
@@ -130,7 +137,7 @@ JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE = (
SUGGESTED_QUESTIONS_AFTER_ANSWER_INSTRUCTION_PROMPT = (
"Please help me predict the three most likely questions that human would ask, "
- "and keeping each question under 20 characters.\n"
+ "and keep each question under 20 characters.\n"
"MAKE SURE your output is the SAME language as the Assistant's latest response. "
"The output must be an array in JSON format following the specified schema:\n"
'["question1","question2","question3"]\n'
@@ -157,9 +164,9 @@ Here is a task description for which I would like you to create a high-quality p
Based on task description, please create a well-structured prompt template that another AI could use to consistently complete the task. The prompt template should include:
- Do not include or section and variables in the prompt, assume user will add them at their own will.
-- Clear instructions for the AI that will be using this prompt, demarcated with tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
+- Clear instructions for the AI that will be using this prompt, demarcated with tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
- Relevant examples if needed to clarify the task further, demarcated with tags. Do not include variables in the prompt. Give three pairs of input and output examples.
-- Include other relevant sections demarcated with appropriate XML tags like , .
+- Include other relevant sections demarcated with appropriate XML tags like , .
- Use the same language as task description.
- Output in ``` xml ``` and start with
Please generate the full prompt template with at least 300 words and output only the prompt template.
@@ -172,7 +179,7 @@ Here is a task description for which I would like you to create a high-quality p
Based on task description, please create a well-structured prompt template that another AI could use to consistently complete the task. The prompt template should include:
- Descriptive variable names surrounded by {{ }} (two curly brackets) to indicate where the actual values will be substituted in. Choose variable names that clearly indicate the type of value expected. Variable names have to be composed of number, english alphabets and underline and nothing else.
-- Clear instructions for the AI that will be using this prompt, demarcated with tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
+- Clear instructions for the AI that will be using this prompt, demarcated with tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
- Relevant examples if needed to clarify the task further, demarcated with tags. Do not use curly brackets any other than in section.
- Any other relevant sections demarcated with appropriate XML tags like , , etc.
- Use the same language as task description.
@@ -291,32 +298,30 @@ Your task is to convert simple user descriptions into properly formatted JSON Sc
{
"type": "object",
"properties": {
- "properties": {
- "songs": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "id": {
- "type": "string"
- },
- "duration": {
- "type": "string"
- },
- "aritst": {
- "type": "string"
- }
+ "songs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "duration": {
+ "type": "string"
},
- "required": [
- "name",
- "id",
- "duration",
- "aritst"
- ]
- }
+ "aritst": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "id",
+ "duration",
+ "aritst"
+ ]
}
}
},
diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py
index 3c90dd22a2..2254b3d4d5 100644
--- a/api/core/memory/token_buffer_memory.py
+++ b/api/core/memory/token_buffer_memory.py
@@ -8,11 +8,11 @@ from core.model_runtime.entities import (
AssistantPromptMessage,
ImagePromptMessageContent,
PromptMessage,
- PromptMessageContent,
PromptMessageRole,
TextPromptMessageContent,
UserPromptMessage,
)
+from core.model_runtime.entities.message_entities import PromptMessageContentUnionTypes
from core.prompt.utils.extract_thread_messages import extract_thread_messages
from extensions.ext_database import db
from factories import file_factory
@@ -100,7 +100,7 @@ class TokenBufferMemory:
if not file_objs:
prompt_messages.append(UserPromptMessage(content=message.query))
else:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=message.query))
for file in file_objs:
prompt_message = file_manager.to_prompt_message_content(
diff --git a/api/core/model_runtime/entities/message_entities.py b/api/core/model_runtime/entities/message_entities.py
index 3bed2460dd..b1c43d1455 100644
--- a/api/core/model_runtime/entities/message_entities.py
+++ b/api/core/model_runtime/entities/message_entities.py
@@ -1,6 +1,6 @@
from collections.abc import Sequence
from enum import Enum, StrEnum
-from typing import Any, Optional, Union
+from typing import Annotated, Any, Literal, Optional, Union
from pydantic import BaseModel, Field, field_serializer, field_validator
@@ -61,11 +61,7 @@ class PromptMessageContentType(StrEnum):
class PromptMessageContent(BaseModel):
- """
- Model class for prompt message content.
- """
-
- type: PromptMessageContentType
+ pass
class TextPromptMessageContent(PromptMessageContent):
@@ -73,7 +69,7 @@ class TextPromptMessageContent(PromptMessageContent):
Model class for text prompt message content.
"""
- type: PromptMessageContentType = PromptMessageContentType.TEXT
+ type: Literal[PromptMessageContentType.TEXT] = PromptMessageContentType.TEXT
data: str
@@ -82,7 +78,6 @@ class MultiModalPromptMessageContent(PromptMessageContent):
Model class for multi-modal prompt message content.
"""
- type: PromptMessageContentType
format: str = Field(default=..., description="the format of multi-modal file")
base64_data: str = Field(default="", description="the base64 data of multi-modal file")
url: str = Field(default="", description="the url of multi-modal file")
@@ -94,11 +89,11 @@ class MultiModalPromptMessageContent(PromptMessageContent):
class VideoPromptMessageContent(MultiModalPromptMessageContent):
- type: PromptMessageContentType = PromptMessageContentType.VIDEO
+ type: Literal[PromptMessageContentType.VIDEO] = PromptMessageContentType.VIDEO
class AudioPromptMessageContent(MultiModalPromptMessageContent):
- type: PromptMessageContentType = PromptMessageContentType.AUDIO
+ type: Literal[PromptMessageContentType.AUDIO] = PromptMessageContentType.AUDIO
class ImagePromptMessageContent(MultiModalPromptMessageContent):
@@ -110,12 +105,24 @@ class ImagePromptMessageContent(MultiModalPromptMessageContent):
LOW = "low"
HIGH = "high"
- type: PromptMessageContentType = PromptMessageContentType.IMAGE
+ type: Literal[PromptMessageContentType.IMAGE] = PromptMessageContentType.IMAGE
detail: DETAIL = DETAIL.LOW
class DocumentPromptMessageContent(MultiModalPromptMessageContent):
- type: PromptMessageContentType = PromptMessageContentType.DOCUMENT
+ type: Literal[PromptMessageContentType.DOCUMENT] = PromptMessageContentType.DOCUMENT
+
+
+PromptMessageContentUnionTypes = Annotated[
+ Union[
+ TextPromptMessageContent,
+ ImagePromptMessageContent,
+ DocumentPromptMessageContent,
+ AudioPromptMessageContent,
+ VideoPromptMessageContent,
+ ],
+ Field(discriminator="type"),
+]
class PromptMessage(BaseModel):
@@ -124,7 +131,7 @@ class PromptMessage(BaseModel):
"""
role: PromptMessageRole
- content: Optional[str | Sequence[PromptMessageContent]] = None
+ content: Optional[str | list[PromptMessageContentUnionTypes]] = None
name: Optional[str] = None
def is_empty(self) -> bool:
diff --git a/api/core/prompt/advanced_prompt_transform.py b/api/core/prompt/advanced_prompt_transform.py
index c7427f797e..25964ae063 100644
--- a/api/core/prompt/advanced_prompt_transform.py
+++ b/api/core/prompt/advanced_prompt_transform.py
@@ -9,13 +9,12 @@ from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_runtime.entities import (
AssistantPromptMessage,
PromptMessage,
- PromptMessageContent,
PromptMessageRole,
SystemPromptMessage,
TextPromptMessageContent,
UserPromptMessage,
)
-from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig
from core.prompt.prompt_transform import PromptTransform
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
@@ -125,7 +124,7 @@ class AdvancedPromptTransform(PromptTransform):
prompt = Jinja2Formatter.format(prompt, prompt_inputs)
if files:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=prompt))
for file in files:
prompt_message_contents.append(
@@ -201,7 +200,7 @@ class AdvancedPromptTransform(PromptTransform):
prompt_messages = self._append_chat_histories(memory, memory_config, prompt_messages, model_config)
if files and query is not None:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=query))
for file in files:
prompt_message_contents.append(
diff --git a/api/core/prompt/simple_prompt_transform.py b/api/core/prompt/simple_prompt_transform.py
index ad56d84cb6..47808928f7 100644
--- a/api/core/prompt/simple_prompt_transform.py
+++ b/api/core/prompt/simple_prompt_transform.py
@@ -11,7 +11,7 @@ from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_runtime.entities.message_entities import (
ImagePromptMessageContent,
PromptMessage,
- PromptMessageContent,
+ PromptMessageContentUnionTypes,
SystemPromptMessage,
TextPromptMessageContent,
UserPromptMessage,
@@ -277,7 +277,7 @@ class SimplePromptTransform(PromptTransform):
image_detail_config: Optional[ImagePromptMessageContent.DETAIL] = None,
) -> UserPromptMessage:
if files:
- prompt_message_contents: list[PromptMessageContent] = []
+ prompt_message_contents: list[PromptMessageContentUnionTypes] = []
prompt_message_contents.append(TextPromptMessageContent(data=prompt))
for file in files:
prompt_message_contents.append(
diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py
index 23ea775dec..4869a21e80 100644
--- a/api/core/rag/retrieval/dataset_retrieval.py
+++ b/api/core/rag/retrieval/dataset_retrieval.py
@@ -869,7 +869,9 @@ class DatasetRetrieval:
)
)
metadata_condition = MetadataCondition(
- logical_operator=metadata_filtering_conditions.logical_operator, # type: ignore
+ logical_operator=metadata_filtering_conditions.logical_operator
+ if metadata_filtering_conditions
+ else "or", # type: ignore
conditions=conditions,
)
elif metadata_filtering_mode == "manual":
@@ -891,10 +893,10 @@ class DatasetRetrieval:
else:
raise ValueError("Invalid metadata filtering mode")
if filters:
- if metadata_filtering_conditions.logical_operator == "or": # type: ignore
- document_query = document_query.filter(or_(*filters))
- else:
+ if metadata_filtering_conditions and metadata_filtering_conditions.logical_operator == "and": # type: ignore
document_query = document_query.filter(and_(*filters))
+ else:
+ document_query = document_query.filter(or_(*filters))
documents = document_query.all()
# group by dataset_id
metadata_filter_document_ids = defaultdict(list) if documents else None # type: ignore
diff --git a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py
index 07a711cc4e..4ec033572c 100644
--- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py
+++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py
@@ -349,7 +349,9 @@ class KnowledgeRetrievalNode(LLMNode):
)
)
metadata_condition = MetadataCondition(
- logical_operator=node_data.metadata_filtering_conditions.logical_operator, # type: ignore
+ logical_operator=node_data.metadata_filtering_conditions.logical_operator
+ if node_data.metadata_filtering_conditions
+ else "or", # type: ignore
conditions=conditions,
)
elif node_data.metadata_filtering_mode == "manual":
@@ -380,7 +382,10 @@ class KnowledgeRetrievalNode(LLMNode):
else:
raise ValueError("Invalid metadata filtering mode")
if filters:
- if node_data.metadata_filtering_conditions.logical_operator == "and": # type: ignore
+ if (
+ node_data.metadata_filtering_conditions
+ and node_data.metadata_filtering_conditions.logical_operator == "and"
+ ): # type: ignore
document_query = document_query.filter(and_(*filters))
else:
document_query = document_query.filter(or_(*filters))
diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py
index 8db7394e54..1089e7168e 100644
--- a/api/core/workflow/nodes/llm/node.py
+++ b/api/core/workflow/nodes/llm/node.py
@@ -24,7 +24,7 @@ from core.model_runtime.entities import (
from core.model_runtime.entities.llm_entities import LLMResult, LLMUsage
from core.model_runtime.entities.message_entities import (
AssistantPromptMessage,
- PromptMessageContent,
+ PromptMessageContentUnionTypes,
PromptMessageRole,
SystemPromptMessage,
UserPromptMessage,
@@ -594,8 +594,7 @@ class LLMNode(BaseNode[LLMNodeData]):
variable_pool: VariablePool,
jinja2_variables: Sequence[VariableSelector],
) -> tuple[Sequence[PromptMessage], Optional[Sequence[str]]]:
- # FIXME: fix the type error cause prompt_messages is type quick a few times
- prompt_messages: list[Any] = []
+ prompt_messages: list[PromptMessage] = []
if isinstance(prompt_template, list):
# For chat model
@@ -657,12 +656,14 @@ class LLMNode(BaseNode[LLMNodeData]):
# For issue #11247 - Check if prompt content is a string or a list
prompt_content_type = type(prompt_content)
if prompt_content_type == str:
+ prompt_content = str(prompt_content)
if "#histories#" in prompt_content:
prompt_content = prompt_content.replace("#histories#", memory_text)
else:
prompt_content = memory_text + "\n" + prompt_content
prompt_messages[0].content = prompt_content
elif prompt_content_type == list:
+ prompt_content = prompt_content if isinstance(prompt_content, list) else []
for content_item in prompt_content:
if content_item.type == PromptMessageContentType.TEXT:
if "#histories#" in content_item.data:
@@ -675,9 +676,10 @@ class LLMNode(BaseNode[LLMNodeData]):
# Add current query to the prompt message
if sys_query:
if prompt_content_type == str:
- prompt_content = prompt_messages[0].content.replace("#sys.query#", sys_query)
+ prompt_content = str(prompt_messages[0].content).replace("#sys.query#", sys_query)
prompt_messages[0].content = prompt_content
elif prompt_content_type == list:
+ prompt_content = prompt_content if isinstance(prompt_content, list) else []
for content_item in prompt_content:
if content_item.type == PromptMessageContentType.TEXT:
content_item.data = sys_query + "\n" + content_item.data
@@ -707,7 +709,7 @@ class LLMNode(BaseNode[LLMNodeData]):
filtered_prompt_messages = []
for prompt_message in prompt_messages:
if isinstance(prompt_message.content, list):
- prompt_message_content = []
+ prompt_message_content: list[PromptMessageContentUnionTypes] = []
for content_item in prompt_message.content:
# Skip content if features are not defined
if not model_config.model_schema.features:
@@ -1132,7 +1134,9 @@ class LLMNode(BaseNode[LLMNodeData]):
)
-def _combine_message_content_with_role(*, contents: Sequence[PromptMessageContent], role: PromptMessageRole):
+def _combine_message_content_with_role(
+ *, contents: Optional[str | list[PromptMessageContentUnionTypes]] = None, role: PromptMessageRole
+):
match role:
case PromptMessageRole.USER:
return UserPromptMessage(content=contents)
diff --git a/api/extensions/ext_otel_patch.py b/api/extensions/ext_otel_patch.py
new file mode 100644
index 0000000000..58309fe4d1
--- /dev/null
+++ b/api/extensions/ext_otel_patch.py
@@ -0,0 +1,63 @@
+"""
+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")
diff --git a/api/models/model.py b/api/models/model.py
index 6577492d1b..d1490d75c8 100644
--- a/api/models/model.py
+++ b/api/models/model.py
@@ -3,8 +3,8 @@ import re
import uuid
from collections.abc import Mapping
from datetime import datetime
-from enum import Enum
-from typing import TYPE_CHECKING, Optional
+from enum import Enum, StrEnum
+from typing import TYPE_CHECKING, Any, Literal, Optional, cast
from core.plugin.entities.plugin import GenericProviderID
from core.tools.entities.tool_entities import ToolProviderType
@@ -13,9 +13,6 @@ from services.plugin.plugin_service import PluginService
if TYPE_CHECKING:
from models.workflow import Workflow
-from enum import StrEnum
-from typing import TYPE_CHECKING, Any, Literal, cast
-
import sqlalchemy as sa
from flask import request
from flask_login import UserMixin # type: ignore
diff --git a/api/models/workflow.py b/api/models/workflow.py
index 5a67fa47a8..da60617de5 100644
--- a/api/models/workflow.py
+++ b/api/models/workflow.py
@@ -1,14 +1,12 @@
import json
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime
-from enum import Enum
+from enum import Enum, StrEnum
from typing import TYPE_CHECKING, Any, Optional, Self, Union
from uuid import uuid4
if TYPE_CHECKING:
from models.model import AppMode
-from enum import StrEnum
-from typing import TYPE_CHECKING
import sqlalchemy as sa
from sqlalchemy import Index, PrimaryKeyConstraint, func
diff --git a/api/repositories/workflow_node_execution/sqlalchemy_repository.py b/api/repositories/workflow_node_execution/sqlalchemy_repository.py
index 0594d816a2..e0ad384be6 100644
--- a/api/repositories/workflow_node_execution/sqlalchemy_repository.py
+++ b/api/repositories/workflow_node_execution/sqlalchemy_repository.py
@@ -37,8 +37,12 @@ class SQLAlchemyWorkflowNodeExecutionRepository:
# If an engine is provided, create a sessionmaker from it
if isinstance(session_factory, Engine):
self._session_factory = sessionmaker(bind=session_factory, expire_on_commit=False)
- else:
+ elif isinstance(session_factory, sessionmaker):
self._session_factory = session_factory
+ else:
+ raise ValueError(
+ f"Invalid session_factory type {type(session_factory).__name__}; expected sessionmaker or Engine"
+ )
self._tenant_id = tenant_id
self._app_id = app_id
diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py
index 2e2b729021..936101c78c 100644
--- a/api/services/app_dsl_service.py
+++ b/api/services/app_dsl_service.py
@@ -40,7 +40,7 @@ IMPORT_INFO_REDIS_KEY_PREFIX = "app_import_info:"
CHECK_DEPENDENCIES_REDIS_KEY_PREFIX = "app_check_dependencies:"
IMPORT_INFO_REDIS_EXPIRY = 10 * 60 # 10 minutes
DSL_MAX_SIZE = 10 * 1024 * 1024 # 10MB
-CURRENT_DSL_VERSION = "0.1.5"
+CURRENT_DSL_VERSION = "0.2.0"
class ImportMode(StrEnum):
diff --git a/api/services/workflow_run_service.py b/api/services/workflow_run_service.py
index ff3b33eecd..8b7213eefb 100644
--- a/api/services/workflow_run_service.py
+++ b/api/services/workflow_run_service.py
@@ -133,7 +133,7 @@ class WorkflowRunService:
params={
"tenant_id": app_model.tenant_id,
"app_id": app_model.id,
- "session_factory": db.session.get_bind,
+ "session_factory": db.session.get_bind(),
}
)
diff --git a/api/tasks/remove_app_and_related_data_task.py b/api/tasks/remove_app_and_related_data_task.py
index 4542b1b923..cd8981abf6 100644
--- a/api/tasks/remove_app_and_related_data_task.py
+++ b/api/tasks/remove_app_and_related_data_task.py
@@ -193,7 +193,7 @@ def _delete_app_workflow_node_executions(tenant_id: str, app_id: str):
params={
"tenant_id": tenant_id,
"app_id": app_id,
- "session_factory": db.session.get_bind,
+ "session_factory": db.session.get_bind(),
}
)
diff --git a/api/tests/unit_tests/core/prompt/test_prompt_message.py b/api/tests/unit_tests/core/prompt/test_prompt_message.py
new file mode 100644
index 0000000000..e5da51d733
--- /dev/null
+++ b/api/tests/unit_tests/core/prompt/test_prompt_message.py
@@ -0,0 +1,27 @@
+from core.model_runtime.entities.message_entities import (
+ ImagePromptMessageContent,
+ TextPromptMessageContent,
+ UserPromptMessage,
+)
+
+
+def test_build_prompt_message_with_prompt_message_contents():
+ prompt = UserPromptMessage(content=[TextPromptMessageContent(data="Hello, World!")])
+ assert isinstance(prompt.content, list)
+ assert isinstance(prompt.content[0], TextPromptMessageContent)
+ assert prompt.content[0].data == "Hello, World!"
+
+
+def test_dump_prompt_message():
+ example_url = "https://example.com/image.jpg"
+ prompt = UserPromptMessage(
+ content=[
+ ImagePromptMessageContent(
+ url=example_url,
+ format="jpeg",
+ mime_type="image/jpeg",
+ )
+ ]
+ )
+ data = prompt.model_dump()
+ assert data["content"][0].get("url") == example_url
diff --git a/docker/.env.example b/docker/.env.example
index f8310a10f1..0b80dccb37 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -1068,3 +1068,6 @@ OTEL_MAX_EXPORT_BATCH_SIZE=512
OTEL_METRIC_EXPORT_INTERVAL=60000
OTEL_BATCH_EXPORT_TIMEOUT=10000
OTEL_METRIC_EXPORT_TIMEOUT=30000
+
+# Prevent Clickjacking
+ALLOW_EMBED=false
diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml
index c6d41849ef..8c57a7c4c2 100644
--- a/docker/docker-compose-template.yaml
+++ b/docker/docker-compose-template.yaml
@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
- image: langgenius/dify-api:1.2.0
+ image: langgenius/dify-api:1.3.0
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.2.0
+ image: langgenius/dify-api:1.3.0
restart: always
environment:
# Use the shared environment variables.
@@ -57,7 +57,7 @@ services:
# Frontend web application.
web:
- image: langgenius/dify-web:1.2.0
+ image: langgenius/dify-web:1.3.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -66,6 +66,7 @@ services:
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
CSP_WHITELIST: ${CSP_WHITELIST:-}
+ ALLOW_EMBED: ${ALLOW_EMBED:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
@@ -141,7 +142,7 @@ services:
# plugin daemon
plugin_daemon:
- image: langgenius/dify-plugin-daemon:0.0.7-local
+ image: langgenius/dify-plugin-daemon:0.0.8-local
restart: always
environment:
# Use the shared environment variables.
@@ -552,7 +553,7 @@ services:
volumes:
- ./volumes/opengauss/data:/var/lib/opengauss/data
healthcheck:
- test: ["CMD-SHELL", "netstat -lntp | grep tcp6 > /dev/null 2>&1"]
+ test: [ "CMD-SHELL", "netstat -lntp | grep tcp6 > /dev/null 2>&1" ]
interval: 10s
timeout: 10s
retries: 10
diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml
index 1702a5395f..fc08edd264 100644
--- a/docker/docker-compose.middleware.yaml
+++ b/docker/docker-compose.middleware.yaml
@@ -71,7 +71,7 @@ services:
# plugin daemon
plugin_daemon:
- image: langgenius/dify-plugin-daemon:0.0.7-local
+ image: langgenius/dify-plugin-daemon:0.0.8-local
restart: always
env_file:
- ./middleware.env
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index d8ff7d841a..3d3e3a901f 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -474,11 +474,12 @@ x-shared-env: &shared-api-worker-env
OTEL_METRIC_EXPORT_INTERVAL: ${OTEL_METRIC_EXPORT_INTERVAL:-60000}
OTEL_BATCH_EXPORT_TIMEOUT: ${OTEL_BATCH_EXPORT_TIMEOUT:-10000}
OTEL_METRIC_EXPORT_TIMEOUT: ${OTEL_METRIC_EXPORT_TIMEOUT:-30000}
+ ALLOW_EMBED: ${ALLOW_EMBED:-false}
services:
# API service
api:
- image: langgenius/dify-api:1.2.0
+ image: langgenius/dify-api:1.3.0
restart: always
environment:
# Use the shared environment variables.
@@ -507,7 +508,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
- image: langgenius/dify-api:1.2.0
+ image: langgenius/dify-api:1.3.0
restart: always
environment:
# Use the shared environment variables.
@@ -533,7 +534,7 @@ services:
# Frontend web application.
web:
- image: langgenius/dify-web:1.2.0
+ image: langgenius/dify-web:1.3.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -542,6 +543,7 @@ services:
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
CSP_WHITELIST: ${CSP_WHITELIST:-}
+ ALLOW_EMBED: ${ALLOW_EMBED:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
@@ -617,7 +619,7 @@ services:
# plugin daemon
plugin_daemon:
- image: langgenius/dify-plugin-daemon:0.0.7-local
+ image: langgenius/dify-plugin-daemon:0.0.8-local
restart: always
environment:
# Use the shared environment variables.
@@ -1028,7 +1030,7 @@ services:
volumes:
- ./volumes/opengauss/data:/var/lib/opengauss/data
healthcheck:
- test: ["CMD-SHELL", "netstat -lntp | grep tcp6 > /dev/null 2>&1"]
+ test: [ "CMD-SHELL", "netstat -lntp | grep tcp6 > /dev/null 2>&1" ]
interval: 10s
timeout: 10s
retries: 10
diff --git a/sdks/php-client/.gitignore b/sdks/php-client/.gitignore
new file mode 100644
index 0000000000..61ead86667
--- /dev/null
+++ b/sdks/php-client/.gitignore
@@ -0,0 +1 @@
+/vendor
diff --git a/sdks/php-client/README.md b/sdks/php-client/README.md
index b0a435bbaf..812980d834 100644
--- a/sdks/php-client/README.md
+++ b/sdks/php-client/README.md
@@ -9,6 +9,21 @@ This is the PHP SDK for the Dify API, which allows you to easily integrate Dify
## Usage
+If you want to try the example, you can run `composer install` in this directory.
+
+In exist project, copy the `dify-client.php` to you project, and merge the following to your `composer.json` file, then run `composer install && composer dump-autoload` to install. Guzzle does not require 7.9, other versions have not been tested, but you can try.
+
+```json
+{
+ "require": {
+ "guzzlehttp/guzzle": "^7.9"
+ },
+ "autoload": {
+ "files": ["path/to/dify-client.php"]
+ }
+}
+```
+
After installing the SDK, you can use it in your project like this:
```php
@@ -16,10 +31,6 @@ After installing the SDK, you can use it in your project like this:
require 'vendor/autoload.php';
-use YourVendorName\DifyPHP\DifyClient;
-use YourVendorName\DifyPHP\CompletionClient;
-use YourVendorName\DifyPHP\ChatClient;
-
$apiKey = 'your-api-key-here';
$difyClient = new DifyClient($apiKey);
diff --git a/sdks/php-client/composer.json b/sdks/php-client/composer.json
new file mode 100644
index 0000000000..6e49e44075
--- /dev/null
+++ b/sdks/php-client/composer.json
@@ -0,0 +1,9 @@
+{
+ "require": {
+ "php": ">=7.2",
+ "guzzlehttp/guzzle": "^7.9"
+ },
+ "autoload": {
+ "files": ["dify-client.php"]
+ }
+}
diff --git a/sdks/php-client/composer.lock b/sdks/php-client/composer.lock
new file mode 100644
index 0000000000..aa07dc03ef
--- /dev/null
+++ b/sdks/php-client/composer.lock
@@ -0,0 +1,663 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "7827c548fdcc7e87cb0ae341dd2c6b1b",
+ "packages": [
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.9.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "d281ed313b989f213357e3be1a179f02196ac99b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
+ "reference": "d281ed313b989f213357e3be1a179f02196ac99b",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
+ "guzzlehttp/psr7": "^2.7.0",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "guzzle/client-integration-tests": "3.0.2",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.9.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-07-24T11:22:20+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-03-27T13:27:01+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "0.9.0",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-03-27T12:30:47+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "e616d01114759c4c489f93b099585439f795fe35"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
+ "reference": "e616d01114759c4c489f93b099585439f795fe35",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+ },
+ "time": "2023-04-10T20:10:41+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
+ "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:20:29+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.6.0"
+}
diff --git a/sdks/php-client/dify-client.php b/sdks/php-client/dify-client.php
index ccd61f091a..acb862093a 100644
--- a/sdks/php-client/dify-client.php
+++ b/sdks/php-client/dify-client.php
@@ -1,7 +1,5 @@
api_key = $api_key;
- $this->base_url = $base_url ?? "https://api.dify.ai/v1/";
+ $this->base_url = $base_url ?? 'https://api.dify.ai/v1/';
$this->client = new Client([
'base_uri' => $this->base_url,
'headers' => [
@@ -19,13 +17,6 @@ class DifyClient {
'Content-Type' => 'application/json',
],
]);
- $this->file_client = new Client([
- 'base_uri' => $this->base_url,
- 'headers' => [
- 'Authorization' => 'Bearer ' . $this->api_key,
- 'Content-Type' => 'multipart/form-data',
- ],
- ]);
}
protected function send_request($method, $endpoint, $data = null, $params = null, $stream = false) {
@@ -58,7 +49,7 @@ class DifyClient {
'multipart' => $this->prepareMultipart($data, $files)
];
- return $this->file_client->request('POST', 'files/upload', $options);
+ return $this->client->request('POST', 'files/upload', $options);
}
protected function prepareMultipart($data, $files) {
@@ -132,7 +123,7 @@ class ChatClient extends DifyClient {
public function get_suggestions($message_id, $user) {
$params = [
'user' => $user
- ]
+ ];
return $this->send_request('GET', "messages/{$message_id}/suggested", null, $params);
}
@@ -188,10 +179,9 @@ class ChatClient extends DifyClient {
'user' => $user,
];
$options = [
- 'multipart' => $this->prepareMultipart($data, $files)
+ 'multipart' => $this->prepareMultipart($data, $audio_file)
];
- return $this->file_client->request('POST', 'audio-to-text', $options);
-
+ return $this->client->request('POST', 'audio-to-text', $options);
}
}
diff --git a/web/.env.example b/web/.env.example
index 1c3f42ddfc..51631c2437 100644
--- a/web/.env.example
+++ b/web/.env.example
@@ -29,6 +29,8 @@ NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
# CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
NEXT_PUBLIC_CSP_WHITELIST=
+# Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
+NEXT_PUBLIC_ALLOW_EMBED=
# Github Access Token, used for invoking Github API
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN=
diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx
index 37fbd5e291..6ebd0fce69 100644
--- a/web/app/components/app/overview/embedded/index.tsx
+++ b/web/app/components/app/overview/embedded/index.tsx
@@ -29,7 +29,7 @@ const OPTION_MAP = {
iframe: {
getContent: (url: string, token: string) =>
`