diff --git a/.github/workflows/build-api-image.yml b/.github/workflows/build-api-image.yml
index 006a8d98ab..4aaebf0863 100644
--- a/.github/workflows/build-api-image.yml
+++ b/.github/workflows/build-api-image.yml
@@ -5,8 +5,6 @@ on:
branches:
- 'main'
- 'deploy/dev'
- pull_request:
- types: [synchronize, opened, reopened, ready_for_review]
jobs:
build-and-push:
diff --git a/.github/workflows/build-web-image.yml b/.github/workflows/build-web-image.yml
index 69a44f97de..6f097ee7fa 100644
--- a/.github/workflows/build-web-image.yml
+++ b/.github/workflows/build-web-image.yml
@@ -5,8 +5,6 @@ on:
branches:
- 'main'
- 'deploy/dev'
- pull_request:
- types: [synchronize, opened, reopened, ready_for_review]
jobs:
build-and-push:
diff --git a/.gitignore b/.gitignore
index a3e3d66fed..50f682499e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -139,7 +139,7 @@ dmypy.json
api/.env
api/storage/*
-docker/volumes/app/storage/privkeys/*
+docker/volumes/app/storage/*
docker/volumes/db/data/*
docker/volumes/redis/data/*
docker/volumes/weaviate/*
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e96a51072f..de764d6a56 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,14 +22,14 @@ To set up a working development environment, just fork the project git repositor
### Fork the repository
-you need to fork the [repository](https://github.com/langgenius/langgenius-gateway).
+you need to fork the [repository](https://github.com/langgenius/dify).
### Clone the repo
Clone your GitHub forked repository:
```
-git clone git@github.com:/langgenius-gateway.git
+git clone git@github.com:/dify.git
```
### Install backend
diff --git a/README.md b/README.md
index 8b8369704e..e80559bcf7 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
简体中文
-[Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai)
+[Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7)
**Dify** is an easy-to-use LLMOps platform designed to empower more people to create sustainable, AI-native applications. With visual orchestration for various application types, Dify offers out-of-the-box, ready-to-use applications that can also serve as Backend-as-a-Service APIs. Unify your development process with one API for plugins and datasets integration, and streamline your operations using a single interface for prompt engineering, visual analytics, and continuous improvement.
@@ -41,7 +41,7 @@ cd docker
docker-compose up -d
```
-After running, you can access the Dify console in your browser at [http://localhost](http://localhost) and start the initialization operation.
+After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization installation process.
### Configuration
@@ -86,7 +86,7 @@ A: English and Chinese are currently supported, and you can contribute language
If you have any questions, suggestions, or partnership inquiries, feel free to contact us through the following channels:
- Submit an Issue or PR on our GitHub Repo
-- Join the discussion in our [Discord](https://discord.gg/AhzKf7dNgk) Community
+- Join the discussion in our [Discord](https://discord.gg/FngNHpbcY7) Community
- Send an email to hello@dify.ai
We're eager to assist you and together create more fun and useful AI applications!
diff --git a/README_CN.md b/README_CN.md
index b8d659cb06..c72c03bd6a 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -5,7 +5,7 @@
-[官方网站](https://dify.ai) • [文档](https://docs.dify.ai/v/zh-hans) • [Twitter](https://twitter.com/dify_ai)
+[官方网站](https://dify.ai) • [文档](https://docs.dify.ai/v/zh-hans) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7)
**Dify** 是一个易用的 LLMOps 平台,旨在让更多人可以创建可持续运营的原生 AI 应用。Dify 提供多种类型应用的可视化编排,应用可开箱即用,也能以“后端即服务”的 API 提供服务。
@@ -43,7 +43,7 @@ cd docker
docker-compose up -d
```
-运行后,可以在浏览器上访问 [http://localhost](http://localhost) 进入 Dify 控制台,并开始初始化操作。
+运行后,可以在浏览器上访问 [http://localhost/install](http://localhost/install) 进入 Dify 控制台并开始初始化安装操作。
### 配置
@@ -87,7 +87,7 @@ A: 现已支持英文与中文,你可以为我们贡献语言包。
如果您有任何问题、建议或合作意向,欢迎通过以下方式联系我们:
- 在我们的 [GitHub Repo](https://github.com/langgenius/dify) 上提交 Issue 或 PR
-- 在我们的 [Discord 社区](https://discord.gg/AhzKf7dNgk) 上加入讨论
+- 在我们的 [Discord 社区](https://discord.gg/FngNHpbcY7) 上加入讨论
- 发送邮件至 hello@dify.ai
## 贡献代码
diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py
index 62752deaac..419068468d 100644
--- a/api/controllers/console/app/conversation.py
+++ b/api/controllers/console/app/conversation.py
@@ -45,7 +45,7 @@ message_detail_fields = {
'message_tokens': fields.Integer,
'answer': fields.String,
'answer_tokens': fields.Integer,
- 'provider_response_latency': fields.Integer,
+ 'provider_response_latency': fields.Float,
'from_source': fields.String,
'from_end_user_id': fields.String,
'from_account_id': fields.String,
diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py
index 27698d965c..88594425de 100644
--- a/api/controllers/console/app/message.py
+++ b/api/controllers/console/app/message.py
@@ -26,46 +26,46 @@ from services.errors.conversation import ConversationNotExistsError
from services.errors.message import MessageNotExistsError
from services.message_service import MessageService
+account_fields = {
+ 'id': fields.String,
+ 'name': fields.String,
+ 'email': fields.String
+}
-class ChatMessageApi(Resource):
- account_fields = {
- 'id': fields.String,
- 'name': fields.String,
- 'email': fields.String
- }
+feedback_fields = {
+ 'rating': fields.String,
+ 'content': fields.String,
+ 'from_source': fields.String,
+ 'from_end_user_id': fields.String,
+ 'from_account': fields.Nested(account_fields, allow_null=True),
+}
- feedback_fields = {
- 'rating': fields.String,
- 'content': fields.String,
- 'from_source': fields.String,
- 'from_end_user_id': fields.String,
- 'from_account': fields.Nested(account_fields, allow_null=True),
- }
+annotation_fields = {
+ 'content': fields.String,
+ 'account': fields.Nested(account_fields, allow_null=True),
+ 'created_at': TimestampField
+}
- annotation_fields = {
- 'content': fields.String,
- 'account': fields.Nested(account_fields, allow_null=True),
- 'created_at': TimestampField
- }
+message_detail_fields = {
+ 'id': fields.String,
+ 'conversation_id': fields.String,
+ 'inputs': fields.Raw,
+ 'query': fields.String,
+ 'message': fields.Raw,
+ 'message_tokens': fields.Integer,
+ 'answer': fields.String,
+ 'answer_tokens': fields.Integer,
+ 'provider_response_latency': fields.Float,
+ 'from_source': fields.String,
+ 'from_end_user_id': fields.String,
+ 'from_account_id': fields.String,
+ 'feedbacks': fields.List(fields.Nested(feedback_fields)),
+ 'annotation': fields.Nested(annotation_fields, allow_null=True),
+ 'created_at': TimestampField
+}
- message_detail_fields = {
- 'id': fields.String,
- 'conversation_id': fields.String,
- 'inputs': fields.Raw,
- 'query': fields.String,
- 'message': fields.Raw,
- 'message_tokens': fields.Integer,
- 'answer': fields.String,
- 'answer_tokens': fields.Integer,
- 'provider_response_latency': fields.Integer,
- 'from_source': fields.String,
- 'from_end_user_id': fields.String,
- 'from_account_id': fields.String,
- 'feedbacks': fields.List(fields.Nested(feedback_fields)),
- 'annotation': fields.Nested(annotation_fields, allow_null=True),
- 'created_at': TimestampField
- }
+class ChatMessageListApi(Resource):
message_infinite_scroll_pagination_fields = {
'limit': fields.Integer,
'has_more': fields.Boolean,
@@ -253,7 +253,8 @@ class MessageMoreLikeThisApi(Resource):
message_id = str(message_id)
parser = reqparse.RequestParser()
- parser.add_argument('response_mode', type=str, required=True, choices=['blocking', 'streaming'], location='args')
+ parser.add_argument('response_mode', type=str, required=True, choices=['blocking', 'streaming'],
+ location='args')
args = parser.parse_args()
streaming = args['response_mode'] == 'streaming'
@@ -301,7 +302,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
except QuotaExceededError:
yield "data: " + json.dumps(api.handle_error(ProviderQuotaExceededError()).get_json()) + "\n\n"
except ModelCurrentlyNotSupportError:
- yield "data: " + json.dumps(api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
+ yield "data: " + json.dumps(
+ api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
yield "data: " + json.dumps(api.handle_error(CompletionRequestError(str(e))).get_json()) + "\n\n"
@@ -353,9 +355,33 @@ class MessageSuggestedQuestionApi(Resource):
return {'data': questions}
+class MessageApi(Resource):
+ @setup_required
+ @login_required
+ @account_initialization_required
+ @marshal_with(message_detail_fields)
+ def get(self, app_id, message_id):
+ app_id = str(app_id)
+ message_id = str(message_id)
+
+ # get app info
+ app_model = _get_app(app_id, 'chat')
+
+ message = db.session.query(Message).filter(
+ Message.id == message_id,
+ Message.app_id == app_model.id
+ ).first()
+
+ if not message:
+ raise NotFound("Message Not Exists.")
+
+ return message
+
+
api.add_resource(MessageMoreLikeThisApi, '/apps//completion-messages//more-like-this')
api.add_resource(MessageSuggestedQuestionApi, '/apps//chat-messages//suggested-questions')
-api.add_resource(ChatMessageApi, '/apps//chat-messages', endpoint='chat_messages')
+api.add_resource(ChatMessageListApi, '/apps//chat-messages', endpoint='console_chat_messages')
api.add_resource(MessageFeedbackApi, '/apps//feedbacks')
api.add_resource(MessageAnnotationApi, '/apps//annotations')
api.add_resource(MessageAnnotationCountApi, '/apps//annotations/count')
+api.add_resource(MessageApi, '/apps//messages/', endpoint='console_message')
diff --git a/api/core/conversation_message_task.py b/api/core/conversation_message_task.py
index 0df26637a3..81477533e7 100644
--- a/api/core/conversation_message_task.py
+++ b/api/core/conversation_message_task.py
@@ -2,8 +2,6 @@ import decimal
import json
from typing import Optional, Union
-from gunicorn.config import User
-
from core.callback_handler.entity.agent_loop import AgentLoop
from core.callback_handler.entity.dataset_query import DatasetQueryObj
from core.callback_handler.entity.llm_message import LLMMessage
@@ -269,7 +267,7 @@ class ConversationMessageTask:
class PubHandler:
- def __init__(self, user: Union[Account | User], task_id: str,
+ def __init__(self, user: Union[Account | EndUser], task_id: str,
message: Message, conversation: Conversation,
chain_pub: bool = False, agent_thought_pub: bool = False):
self._channel = PubHandler.generate_channel_name(user, task_id)
@@ -282,12 +280,12 @@ class PubHandler:
self._agent_thought_pub = agent_thought_pub
@classmethod
- def generate_channel_name(cls, user: Union[Account | User], task_id: str):
+ def generate_channel_name(cls, user: Union[Account | EndUser], task_id: str):
user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id
return "generate_result:{}-{}".format(user_str, task_id)
@classmethod
- def generate_stopped_cache_key(cls, user: Union[Account | User], task_id: str):
+ def generate_stopped_cache_key(cls, user: Union[Account | EndUser], task_id: str):
user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id
return "generate_result_stopped:{}-{}".format(user_str, task_id)
@@ -366,7 +364,7 @@ class PubHandler:
redis_client.publish(self._channel, json.dumps(content))
@classmethod
- def pub_error(cls, user: Union[Account | User], task_id: str, e):
+ def pub_error(cls, user: Union[Account | EndUser], task_id: str, e):
content = {
'error': type(e).__name__,
'description': e.description if getattr(e, 'description', None) is not None else str(e)
@@ -379,7 +377,7 @@ class PubHandler:
return redis_client.get(self._stopped_cache_key) is not None
@classmethod
- def stop(cls, user: Union[Account | User], task_id: str):
+ def stop(cls, user: Union[Account | EndUser], task_id: str):
stopped_cache_key = cls.generate_stopped_cache_key(user, task_id)
redis_client.setex(stopped_cache_key, 600, 1)
diff --git a/api/core/index/spiltter/fixed_text_splitter.py b/api/core/index/spiltter/fixed_text_splitter.py
new file mode 100644
index 0000000000..aaaf8e5a10
--- /dev/null
+++ b/api/core/index/spiltter/fixed_text_splitter.py
@@ -0,0 +1,68 @@
+"""Functionality for splitting text."""
+from __future__ import annotations
+
+from typing import (
+ Any,
+ List,
+ Optional,
+)
+
+from langchain.text_splitter import RecursiveCharacterTextSplitter
+
+
+class FixedRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter):
+ def __init__(self, fixed_separator: str = "\n\n", separators: Optional[List[str]] = None, **kwargs: Any):
+ """Create a new TextSplitter."""
+ super().__init__(**kwargs)
+ self._fixed_separator = fixed_separator
+ self._separators = separators or ["\n\n", "\n", " ", ""]
+
+ def split_text(self, text: str) -> List[str]:
+ """Split incoming text and return chunks."""
+ if self._fixed_separator:
+ chunks = text.split(self._fixed_separator)
+ else:
+ chunks = list(text)
+
+ final_chunks = []
+ for chunk in chunks:
+ if self._length_function(chunk) > self._chunk_size:
+ final_chunks.extend(self.recursive_split_text(chunk))
+ else:
+ final_chunks.append(chunk)
+
+ return final_chunks
+
+ def recursive_split_text(self, text: str) -> List[str]:
+ """Split incoming text and return chunks."""
+ final_chunks = []
+ # Get appropriate separator to use
+ separator = self._separators[-1]
+ for _s in self._separators:
+ if _s == "":
+ separator = _s
+ break
+ if _s in text:
+ separator = _s
+ break
+ # Now that we have the separator, split the text
+ if separator:
+ splits = text.split(separator)
+ else:
+ splits = list(text)
+ # Now go merging things, recursively splitting longer texts.
+ _good_splits = []
+ for s in splits:
+ if self._length_function(s) < self._chunk_size:
+ _good_splits.append(s)
+ else:
+ if _good_splits:
+ merged_text = self._merge_splits(_good_splits, separator)
+ final_chunks.extend(merged_text)
+ _good_splits = []
+ other_info = self.recursive_split_text(s)
+ final_chunks.extend(other_info)
+ if _good_splits:
+ merged_text = self._merge_splits(_good_splits, separator)
+ final_chunks.extend(merged_text)
+ return final_chunks
diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py
index fd9e430116..f06f3a0034 100644
--- a/api/core/indexing_runner.py
+++ b/api/core/indexing_runner.py
@@ -18,6 +18,7 @@ from core.docstore.dataset_docstore import DatesetDocumentStore
from core.index.keyword_table_index import KeywordTableIndex
from core.index.readers.html_parser import HTMLParser
from core.index.readers.pdf_parser import PDFParser
+from core.index.spiltter.fixed_text_splitter import FixedRecursiveCharacterTextSplitter
from core.index.vector_index import VectorIndex
from core.llm.token_calculator import TokenCalculator
from extensions.ext_database import db
@@ -267,16 +268,14 @@ class IndexingRunner:
raise ValueError("Custom segment length should be between 50 and 1000.")
separator = segmentation["separator"]
- if not separator:
- separators = ["\n\n", "。", ".", " ", ""]
- else:
+ if separator:
separator = separator.replace('\\n', '\n')
- separators = [separator, ""]
- character_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
+ character_splitter = FixedRecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=segmentation["max_tokens"],
chunk_overlap=0,
- separators=separators
+ fixed_separator=separator,
+ separators=["\n\n", "。", ".", " ", ""]
)
else:
# Automatic segmentation
diff --git a/api/models/model.py b/api/models/model.py
index 9b4e2a38bc..c9fe5189ee 100644
--- a/api/models/model.py
+++ b/api/models/model.py
@@ -1,6 +1,6 @@
import json
-from flask import current_app
+from flask import current_app, request
from flask_login import UserMixin
from sqlalchemy.dialects.postgresql import UUID
@@ -56,7 +56,7 @@ class App(db.Model):
@property
def api_base_url(self):
- return current_app.config['API_URL'] + '/v1'
+ return (current_app.config['API_URL'] if current_app.config['API_URL'] else request.host_url.rstrip('/')) + '/v1'
@property
def tenant(self):
@@ -505,7 +505,7 @@ class Site(db.Model):
@property
def app_base_url(self):
- return current_app.config['APP_URL']
+ return (current_app.config['APP_URL'] if current_app.config['APP_URL'] else request.host_url.rstrip('/'))
class ApiToken(db.Model):
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index 40935d2926..a337377cfa 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -11,12 +11,18 @@ services:
LOG_LEVEL: INFO
# A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`.
SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
- # The base URL of console application, refers to the Console base URL of WEB service.
- CONSOLE_URL: http://localhost
- # The URL for Service API endpoints,refers to the base URL of the current API service.
- API_URL: http://localhost
- # The URL for Web APP, refers to the Web App base URL of WEB service.
- APP_URL: http://localhost
+ # The base URL of console application, refers to the Console base URL of WEB service if console domain is
+ # different from api or web app domain.
+ # example: http://cloud.dify.ai
+ CONSOLE_URL: ''
+ # The URL for Service API endpoints,refers to the base URL of the current API service if api domain is
+ # different from console domain.
+ # example: http://api.dify.ai
+ API_URL: ''
+ # The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
+ # console or api domain.
+ # example: http://udify.app
+ APP_URL: ''
# When enabled, migrations will be executed prior to application startup and the application will start after the migrations have completed.
MIGRATION_ENABLED: 'true'
# The configurations of postgres database connection.
@@ -43,16 +49,25 @@ services:
# The configurations of celery broker.
# Use redis as the broker, and redis db 1 for celery broker.
CELERY_BROKER_URL: redis://:difyai123456@redis:6379/1
- # Specifies the allowed origins for cross-origin requests to the Web API
- WEB_API_CORS_ALLOW_ORIGINS: http://localhost,*
- # Specifies the allowed origins for cross-origin requests to the console API
- CONSOLE_CORS_ALLOW_ORIGINS: http://localhost,*
+ # Specifies the allowed origins for cross-origin requests to the Web API, e.g. https://dify.app or * for all origins.
+ WEB_API_CORS_ALLOW_ORIGINS: '*'
+ # Specifies the allowed origins for cross-origin requests to the console API, e.g. https://cloud.dify.ai or * for all origins.
+ CONSOLE_CORS_ALLOW_ORIGINS: '*'
# CSRF Cookie settings
# Controls whether a cookie is sent with cross-site requests,
# providing some protection against cross-site request forgery attacks
+ #
+ # Default: `SameSite=Lax, Secure=false, HttpOnly=true`
+ # This default configuration supports same-origin requests using either HTTP or HTTPS,
+ # but does not support cross-origin requests. It is suitable for local debugging purposes.
+ #
+ # If you want to enable cross-origin support,
+ # you must use the HTTPS protocol and set the configuration to `SameSite=None, Secure=true, HttpOnly=true`.
+ #
+ # For **production** purposes, please set `SameSite=Lax, Secure=true, HttpOnly=true`.
COOKIE_HTTPONLY: 'true'
- COOKIE_SAMESITE: 'None'
- COOKIE_SECURE: 'true'
+ COOKIE_SAMESITE: 'Lax'
+ COOKIE_SECURE: 'false'
# The type of storage to use for storing user files. Supported values are `local` and `s3`, Default: `local`
STORAGE_TYPE: local
# The path to the local storage directory, the directory relative the root path of API service codes or absolute path. Default: `storage` or `/home/john/storage`.
@@ -86,7 +101,7 @@ services:
- weaviate
volumes:
# Mount the storage directory to the container, for storing user files.
- - ./volumes/app/storage:/app/storage
+ - ./volumes/app/storage:/app/api/storage
# worker service
# The Celery worker for processing the queue.
@@ -104,12 +119,6 @@ services:
# A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`.
# same as the API service
SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
- # The base URL of console application, refers to the Console base URL of WEB service.
- CONSOLE_URL: http://localhost
- # The URL for Service API endpoints,refers to the base URL of the current API service.
- API_URL: http://localhost
- # The URL for Web APP, refers to the Web App base URL of WEB service.
- APP_URL: http://localhost
# The configurations of postgres database connection.
# It is consistent with the configuration in the 'db' service below.
DB_USERNAME: postgres
@@ -137,7 +146,7 @@ services:
- weaviate
volumes:
# Mount the storage directory to the container, for storing user files.
- - ./volumes/app/storage:/app/storage
+ - ./volumes/app/storage:/app/api/storage
# Frontend web application.
web:
@@ -145,10 +154,14 @@ services:
restart: always
environment:
EDITION: SELF_HOSTED
- # The base URL of console application, refers to the Console base URL of WEB service.
- CONSOLE_URL: http://localhost
- # The URL for Web APP, refers to the Web App base URL of WEB service.
- APP_URL: http://localhost
+ # The base URL of console application, refers to the Console base URL of WEB service if console domain is
+ # different from api or web app domain.
+ # example: http://cloud.dify.ai
+ CONSOLE_URL: ''
+ # The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
+ # console or api domain.
+ # example: http://udify.app
+ APP_URL: ''
# The postgres database.
db:
diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf
index 3b153f40b0..279de0c328 100644
--- a/docker/nginx/conf.d/default.conf
+++ b/docker/nginx/conf.d/default.conf
@@ -1,6 +1,6 @@
server {
listen 80;
- server_name localhost;
+ server_name _;
location /console/api {
proxy_pass http://api:5001;
diff --git a/sdks/nodejs-client/babel.config.json b/sdks/nodejs-client/babel.config.json
new file mode 100644
index 0000000000..0639bf7643
--- /dev/null
+++ b/sdks/nodejs-client/babel.config.json
@@ -0,0 +1,5 @@
+{
+ "presets": [
+ "@babel/preset-env"
+ ]
+}
\ No newline at end of file
diff --git a/sdks/nodejs-client/index.js b/sdks/nodejs-client/index.js
index 902ecbd880..48b6ea753f 100644
--- a/sdks/nodejs-client/index.js
+++ b/sdks/nodejs-client/index.js
@@ -1,8 +1,8 @@
import axios from 'axios'
-const BASE_URL = 'https://api.dify.ai/v1'
+export const BASE_URL = 'https://api.dify.ai/v1'
-const routes = {
+export const routes = {
application: {
method: 'GET',
url: () => `/parameters`
@@ -17,7 +17,7 @@ const routes = {
},
createChatMessage: {
method: 'POST',
- url: () => `/chat-message`,
+ url: () => `/chat-messages`,
},
getConversationMessages: {
method: 'GET',
diff --git a/sdks/nodejs-client/index.test.js b/sdks/nodejs-client/index.test.js
new file mode 100644
index 0000000000..e08b8e82af
--- /dev/null
+++ b/sdks/nodejs-client/index.test.js
@@ -0,0 +1,66 @@
+import { DifyClient, BASE_URL, routes } from ".";
+
+import axios from 'axios'
+
+jest.mock('axios')
+
+describe('Client', () => {
+ let difyClient
+ beforeEach(() => {
+ difyClient = new DifyClient('test')
+ })
+
+ test('should create a client', () => {
+ expect(difyClient).toBeDefined();
+ })
+ // test updateApiKey
+ test('should update the api key', () => {
+ difyClient.updateApiKey('test2');
+ expect(difyClient.apiKey).toBe('test2');
+ })
+});
+
+describe('Send Requests', () => {
+ let difyClient
+
+ beforeEach(() => {
+ difyClient = new DifyClient('test')
+ })
+
+ afterEach(() => {
+ jest.resetAllMocks()
+ })
+
+ it('should make a successful request to the application parameter', async () => {
+ const method = 'GET'
+ const endpoint = routes.application.url
+ const expectedResponse = { data: 'response' }
+ axios.mockResolvedValue(expectedResponse)
+
+ await difyClient.sendRequest(method, endpoint)
+
+ expect(axios).toHaveBeenCalledWith({
+ method,
+ url: `${BASE_URL}${endpoint}`,
+ data: null,
+ params: null,
+ headers: {
+ Authorization: `Bearer ${difyClient.apiKey}`,
+ 'Content-Type': 'application/json',
+ },
+ responseType: 'json',
+ })
+
+ })
+
+ it('should handle errors from the API', async () => {
+ const method = 'GET'
+ const endpoint = '/test-endpoint'
+ const errorMessage = 'Request failed with status code 404'
+ axios.mockRejectedValue(new Error(errorMessage))
+
+ await expect(difyClient.sendRequest(method, endpoint)).rejects.toThrow(
+ errorMessage
+ )
+ })
+})
\ No newline at end of file
diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json
index 350cfc9732..c658b73d98 100644
--- a/sdks/nodejs-client/package.json
+++ b/sdks/nodejs-client/package.json
@@ -1,6 +1,6 @@
{
"name": "dify-client",
- "version": "1.0.2",
+ "version": "1.0.3",
"description": "This is the Node.js SDK for the Dify.AI API, which allows you to easily integrate Dify.AI into your Node.js applications.",
"main": "index.js",
"type": "module",
@@ -14,7 +14,21 @@
" <<427733928@qq.com>> (https://github.com/crazywoola)"
],
"license": "MIT",
+ "scripts": {
+ "test": "jest"
+ },
+ "jest": {
+ "transform": {
+ "^.+\\.[t|j]sx?$": "babel-jest"
+ }
+ },
"dependencies": {
"axios": "^1.3.5"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.21.8",
+ "@babel/preset-env": "^7.21.5",
+ "babel-jest": "^29.5.0",
+ "jest": "^29.5.0"
}
}
\ No newline at end of file
diff --git a/web/app/components/app/chat/copy-btn/index.tsx b/web/app/components/app/chat/copy-btn/index.tsx
new file mode 100644
index 0000000000..f384e714d5
--- /dev/null
+++ b/web/app/components/app/chat/copy-btn/index.tsx
@@ -0,0 +1,44 @@
+'use client'
+import React from 'react'
+import Tooltip from '@/app/components/base/tooltip'
+import { t } from 'i18next'
+import s from './style.module.css'
+import copy from 'copy-to-clipboard'
+
+
+type ICopyBtnProps = {
+ value: string
+ className?: string
+}
+
+const CopyBtn = ({
+ value,
+ className,
+}: ICopyBtnProps) => {
+ const [isCopied, setIsCopied] = React.useState(false)
+
+ return (
+
+
+ {
+ copy(value)
+ setIsCopied(true)
+ }}
+ >
+
+
+
+
+ )
+}
+
+export default CopyBtn
diff --git a/web/app/components/app/chat/copy-btn/style.module.css b/web/app/components/app/chat/copy-btn/style.module.css
new file mode 100644
index 0000000000..56c756025b
--- /dev/null
+++ b/web/app/components/app/chat/copy-btn/style.module.css
@@ -0,0 +1,15 @@
+.copyIcon {
+ background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.copyIcon:hover {
+ background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.copyIcon.copied {
+ background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
+}
\ No newline at end of file
diff --git a/web/app/components/app/chat/index.tsx b/web/app/components/app/chat/index.tsx
index 7b512c57d4..38d85edc4c 100644
--- a/web/app/components/app/chat/index.tsx
+++ b/web/app/components/app/chat/index.tsx
@@ -16,6 +16,8 @@ import type { Annotation, MessageRating } from '@/models/log'
import AppContext from '@/context/app-context'
import { Markdown } from '@/app/components/base/markdown'
import LoadingAnim from './loading-anim'
+import { formatNumber } from '@/utils/format'
+import CopyBtn from './copy-btn'
const stopIcon = (