From b9bf60ea23d4a09c700fa1e21fcd4b3b6e4c390b Mon Sep 17 00:00:00 2001 From: zhuhao <37029601+hwzhuhao@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:30:25 +0800 Subject: [PATCH 001/155] fix: resolve the error with the db-pool-stat endpoint (#9478) --- api/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/app.py b/api/app.py index 7fef62cd38..a3efabf06c 100644 --- a/api/app.py +++ b/api/app.py @@ -20,6 +20,7 @@ from app_factory import create_app # DO NOT REMOVE BELOW from events import event_handlers # noqa: F401 +from extensions.ext_database import db # TODO: Find a way to avoid importing models here from models import account, dataset, model, source, task, tool, tools, web # noqa: F401 From e0846792d23542d548d28876b0d91af5bec5722e Mon Sep 17 00:00:00 2001 From: zhuhao <37029601+hwzhuhao@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:23:21 +0800 Subject: [PATCH 002/155] feat: add yi custom llm intergration (#9482) --- .../model_providers/yi/llm/llm.py | 67 ++++++++++++++++++- .../model_runtime/model_providers/yi/yi.yaml | 55 +++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/api/core/model_runtime/model_providers/yi/llm/llm.py b/api/core/model_runtime/model_providers/yi/llm/llm.py index 5ab7fd126e..0642e72ed5 100644 --- a/api/core/model_runtime/model_providers/yi/llm/llm.py +++ b/api/core/model_runtime/model_providers/yi/llm/llm.py @@ -4,12 +4,22 @@ from urllib.parse import urlparse import tiktoken -from core.model_runtime.entities.llm_entities import LLMResult +from core.model_runtime.entities.common_entities import I18nObject +from core.model_runtime.entities.llm_entities import LLMMode, LLMResult from core.model_runtime.entities.message_entities import ( PromptMessage, PromptMessageTool, SystemPromptMessage, ) +from core.model_runtime.entities.model_entities import ( + AIModelEntity, + FetchFrom, + ModelFeature, + ModelPropertyKey, + ModelType, + ParameterRule, + ParameterType, +) from core.model_runtime.model_providers.openai.llm.llm import OpenAILargeLanguageModel @@ -125,3 +135,58 @@ class YiLargeLanguageModel(OpenAILargeLanguageModel): else: parsed_url = urlparse(credentials["endpoint_url"]) credentials["openai_api_base"] = f"{parsed_url.scheme}://{parsed_url.netloc}" + + def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None: + return AIModelEntity( + model=model, + label=I18nObject(en_US=model, zh_Hans=model), + model_type=ModelType.LLM, + features=[ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL, ModelFeature.STREAM_TOOL_CALL] + if credentials.get("function_calling_type") == "tool_call" + else [], + fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, + model_properties={ + ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 8000)), + ModelPropertyKey.MODE: LLMMode.CHAT.value, + }, + parameter_rules=[ + ParameterRule( + name="temperature", + use_template="temperature", + label=I18nObject(en_US="Temperature", zh_Hans="温度"), + type=ParameterType.FLOAT, + ), + ParameterRule( + name="max_tokens", + use_template="max_tokens", + default=512, + min=1, + max=int(credentials.get("max_tokens", 8192)), + label=I18nObject( + en_US="Max Tokens", zh_Hans="指定生成结果长度的上限。如果生成结果截断,可以调大该参数" + ), + type=ParameterType.INT, + ), + ParameterRule( + name="top_p", + use_template="top_p", + label=I18nObject( + en_US="Top P", + zh_Hans="控制生成结果的随机性。数值越小,随机性越弱;数值越大,随机性越强。", + ), + type=ParameterType.FLOAT, + ), + ParameterRule( + name="top_k", + use_template="top_k", + label=I18nObject(en_US="Top K", zh_Hans="取样数量"), + type=ParameterType.FLOAT, + ), + ParameterRule( + name="frequency_penalty", + use_template="frequency_penalty", + label=I18nObject(en_US="Frequency Penalty", zh_Hans="重复惩罚"), + type=ParameterType.FLOAT, + ), + ], + ) diff --git a/api/core/model_runtime/model_providers/yi/yi.yaml b/api/core/model_runtime/model_providers/yi/yi.yaml index de741afb10..393526c31e 100644 --- a/api/core/model_runtime/model_providers/yi/yi.yaml +++ b/api/core/model_runtime/model_providers/yi/yi.yaml @@ -20,6 +20,7 @@ supported_model_types: - llm configurate_methods: - predefined-model + - customizable-model provider_credential_schema: credential_form_schemas: - variable: api_key @@ -39,3 +40,57 @@ provider_credential_schema: placeholder: zh_Hans: Base URL, e.g. https://api.lingyiwanwu.com/v1 en_US: Base URL, e.g. https://api.lingyiwanwu.com/v1 +model_credential_schema: + model: + label: + en_US: Model Name + zh_Hans: 模型名称 + placeholder: + en_US: Enter your model name + zh_Hans: 输入模型名称 + credential_form_schemas: + - variable: api_key + label: + en_US: API Key + type: secret-input + required: true + placeholder: + zh_Hans: 在此输入您的 API Key + en_US: Enter your API Key + - variable: context_size + label: + zh_Hans: 模型上下文长度 + en_US: Model context size + required: true + type: text-input + default: '4096' + placeholder: + zh_Hans: 在此输入您的模型上下文长度 + en_US: Enter your Model context size + - variable: max_tokens + label: + zh_Hans: 最大 token 上限 + en_US: Upper bound for max tokens + default: '4096' + type: text-input + show_on: + - variable: __model_type + value: llm + - variable: function_calling_type + label: + en_US: Function calling + type: select + required: false + default: no_call + options: + - value: no_call + label: + en_US: Not Support + zh_Hans: 不支持 + - value: function_call + label: + en_US: Support + zh_Hans: 支持 + show_on: + - variable: __model_type + value: llm From b92504bebcc483cded2af4381c0e4a5eba27a394 Mon Sep 17 00:00:00 2001 From: Tao Wang <74752235+taowang1993@users.noreply.github.com> Date: Fri, 18 Oct 2024 03:10:33 -0700 Subject: [PATCH 003/155] Added Llama 3.2 Vision Models Speech2Text Models for Groq (#9479) --- .../model_providers/groq/groq.yaml | 1 + .../llm/llama-3.2-11b-vision-preview.yaml | 26 ++++++++++++++++ .../llm/llama-3.2-90b-vision-preview.yaml | 26 ++++++++++++++++ .../groq/speech2text/__init__.py | 0 .../distil-whisper-large-v3-en.yaml | 5 ++++ .../groq/speech2text/speech2text.py | 30 +++++++++++++++++++ .../speech2text/whisper-large-v3-turbo.yaml | 5 ++++ .../groq/speech2text/whisper-large-v3.yaml | 5 ++++ 8 files changed, 98 insertions(+) create mode 100644 api/core/model_runtime/model_providers/groq/llm/llama-3.2-11b-vision-preview.yaml create mode 100644 api/core/model_runtime/model_providers/groq/llm/llama-3.2-90b-vision-preview.yaml create mode 100644 api/core/model_runtime/model_providers/groq/speech2text/__init__.py create mode 100644 api/core/model_runtime/model_providers/groq/speech2text/distil-whisper-large-v3-en.yaml create mode 100644 api/core/model_runtime/model_providers/groq/speech2text/speech2text.py create mode 100644 api/core/model_runtime/model_providers/groq/speech2text/whisper-large-v3-turbo.yaml create mode 100644 api/core/model_runtime/model_providers/groq/speech2text/whisper-large-v3.yaml diff --git a/api/core/model_runtime/model_providers/groq/groq.yaml b/api/core/model_runtime/model_providers/groq/groq.yaml index db17cc8bdd..d6534e1bf1 100644 --- a/api/core/model_runtime/model_providers/groq/groq.yaml +++ b/api/core/model_runtime/model_providers/groq/groq.yaml @@ -18,6 +18,7 @@ help: en_US: https://console.groq.com/ supported_model_types: - llm + - speech2text configurate_methods: - predefined-model provider_credential_schema: diff --git a/api/core/model_runtime/model_providers/groq/llm/llama-3.2-11b-vision-preview.yaml b/api/core/model_runtime/model_providers/groq/llm/llama-3.2-11b-vision-preview.yaml new file mode 100644 index 0000000000..5632218797 --- /dev/null +++ b/api/core/model_runtime/model_providers/groq/llm/llama-3.2-11b-vision-preview.yaml @@ -0,0 +1,26 @@ +model: llama-3.2-11b-vision-preview +label: + zh_Hans: Llama 3.2 11B Vision (Preview) + en_US: Llama 3.2 11B Vision (Preview) +model_type: llm +features: + - agent-thought + - vision +model_properties: + mode: chat + context_size: 131072 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: max_tokens + use_template: max_tokens + default: 512 + min: 1 + max: 8192 +pricing: + input: '0.05' + output: '0.1' + unit: '0.000001' + currency: USD diff --git a/api/core/model_runtime/model_providers/groq/llm/llama-3.2-90b-vision-preview.yaml b/api/core/model_runtime/model_providers/groq/llm/llama-3.2-90b-vision-preview.yaml new file mode 100644 index 0000000000..e7b93101e8 --- /dev/null +++ b/api/core/model_runtime/model_providers/groq/llm/llama-3.2-90b-vision-preview.yaml @@ -0,0 +1,26 @@ +model: llama-3.2-90b-vision-preview +label: + zh_Hans: Llama 3.2 90B Vision (Preview) + en_US: Llama 3.2 90B Vision (Preview) +model_type: llm +features: + - agent-thought + - vision +model_properties: + mode: chat + context_size: 131072 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: max_tokens + use_template: max_tokens + default: 512 + min: 1 + max: 8192 +pricing: + input: '0.05' + output: '0.1' + unit: '0.000001' + currency: USD diff --git a/api/core/model_runtime/model_providers/groq/speech2text/__init__.py b/api/core/model_runtime/model_providers/groq/speech2text/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/groq/speech2text/distil-whisper-large-v3-en.yaml b/api/core/model_runtime/model_providers/groq/speech2text/distil-whisper-large-v3-en.yaml new file mode 100644 index 0000000000..202d006a66 --- /dev/null +++ b/api/core/model_runtime/model_providers/groq/speech2text/distil-whisper-large-v3-en.yaml @@ -0,0 +1,5 @@ +model: distil-whisper-large-v3-en +model_type: speech2text +model_properties: + file_upload_limit: 1 + supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm diff --git a/api/core/model_runtime/model_providers/groq/speech2text/speech2text.py b/api/core/model_runtime/model_providers/groq/speech2text/speech2text.py new file mode 100644 index 0000000000..75feeb9cb9 --- /dev/null +++ b/api/core/model_runtime/model_providers/groq/speech2text/speech2text.py @@ -0,0 +1,30 @@ +from typing import IO, Optional + +from core.model_runtime.model_providers.openai_api_compatible.speech2text.speech2text import OAICompatSpeech2TextModel + + +class GroqSpeech2TextModel(OAICompatSpeech2TextModel): + """ + Model class for Groq Speech to text model. + """ + + def _invoke(self, model: str, credentials: dict, file: IO[bytes], user: Optional[str] = None) -> str: + """ + Invoke speech2text model + + :param model: model name + :param credentials: model credentials + :param file: audio file + :param user: unique user id + :return: text for given audio file + """ + self._add_custom_parameters(credentials) + return super()._invoke(model, credentials, file) + + def validate_credentials(self, model: str, credentials: dict) -> None: + self._add_custom_parameters(credentials) + return super().validate_credentials(model, credentials) + + @classmethod + def _add_custom_parameters(cls, credentials: dict) -> None: + credentials["endpoint_url"] = "https://api.groq.com/openai/v1" diff --git a/api/core/model_runtime/model_providers/groq/speech2text/whisper-large-v3-turbo.yaml b/api/core/model_runtime/model_providers/groq/speech2text/whisper-large-v3-turbo.yaml new file mode 100644 index 0000000000..3882a3f4f2 --- /dev/null +++ b/api/core/model_runtime/model_providers/groq/speech2text/whisper-large-v3-turbo.yaml @@ -0,0 +1,5 @@ +model: whisper-large-v3-turbo +model_type: speech2text +model_properties: + file_upload_limit: 1 + supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm diff --git a/api/core/model_runtime/model_providers/groq/speech2text/whisper-large-v3.yaml b/api/core/model_runtime/model_providers/groq/speech2text/whisper-large-v3.yaml new file mode 100644 index 0000000000..ed02477d70 --- /dev/null +++ b/api/core/model_runtime/model_providers/groq/speech2text/whisper-large-v3.yaml @@ -0,0 +1,5 @@ +model: whisper-large-v3 +model_type: speech2text +model_properties: + file_upload_limit: 1 + supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm From 424a7da470b17ab121a3bf492bc892488c9230f7 Mon Sep 17 00:00:00 2001 From: Oliver Lee Date: Fri, 18 Oct 2024 18:15:34 +0800 Subject: [PATCH 004/155] =?UTF-8?q?add=20yuqye=EF=BC=88https://www.yuque.c?= =?UTF-8?q?om=EF=BC=89tools=20(#8960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 佐井 --- api/core/tools/provider/_position.yaml | 1 + .../builtin/aliyuque/_assets/icon.svg | 32 +++ .../provider/builtin/aliyuque/aliyuque.py | 19 ++ .../provider/builtin/aliyuque/aliyuque.yaml | 29 +++ .../provider/builtin/aliyuque/tools/base.py | 50 ++++ .../builtin/aliyuque/tools/create_document.py | 22 ++ .../aliyuque/tools/create_document.yaml | 99 ++++++++ .../builtin/aliyuque/tools/delete_document.py | 25 ++ .../aliyuque/tools/delete_document.yaml | 37 +++ .../tools/describe_book_index_page.py | 24 ++ .../tools/describe_book_index_page.yaml | 38 +++ .../tools/describe_book_table_of_contents.py | 23 ++ .../describe_book_table_of_contents.yaml | 25 ++ .../tools/describe_document_content.py | 61 +++++ .../tools/describe_document_content.yaml | 50 ++++ .../aliyuque/tools/describe_documents.py | 24 ++ .../aliyuque/tools/describe_documents.yaml | 38 +++ .../tools/update_book_table_of_contents.py | 29 +++ .../tools/update_book_table_of_contents.yaml | 222 ++++++++++++++++++ .../builtin/aliyuque/tools/update_document.py | 24 ++ .../aliyuque/tools/update_document.yaml | 87 +++++++ 21 files changed, 959 insertions(+) create mode 100644 api/core/tools/provider/builtin/aliyuque/_assets/icon.svg create mode 100644 api/core/tools/provider/builtin/aliyuque/aliyuque.py create mode 100644 api/core/tools/provider/builtin/aliyuque/aliyuque.yaml create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/base.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/create_document.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/create_document.yaml create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/delete_document.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/delete_document.yaml create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/describe_book_index_page.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/describe_book_index_page.yaml create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/describe_book_table_of_contents.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/describe_book_table_of_contents.yaml create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/describe_document_content.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/describe_document_content.yaml create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/describe_documents.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/describe_documents.yaml create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/update_book_table_of_contents.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/update_book_table_of_contents.yaml create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/update_document.py create mode 100644 api/core/tools/provider/builtin/aliyuque/tools/update_document.yaml diff --git a/api/core/tools/provider/_position.yaml b/api/core/tools/provider/_position.yaml index 6bab9a09d8..336588e3e2 100644 --- a/api/core/tools/provider/_position.yaml +++ b/api/core/tools/provider/_position.yaml @@ -61,6 +61,7 @@ - vectorizer - qrcode - tianditu +- aliyuque - google_translate - hap - json_process diff --git a/api/core/tools/provider/builtin/aliyuque/_assets/icon.svg b/api/core/tools/provider/builtin/aliyuque/_assets/icon.svg new file mode 100644 index 0000000000..82b23ebbc6 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/_assets/icon.svg @@ -0,0 +1,32 @@ + + 绿 lgo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/core/tools/provider/builtin/aliyuque/aliyuque.py b/api/core/tools/provider/builtin/aliyuque/aliyuque.py new file mode 100644 index 0000000000..56eac1a4b5 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/aliyuque.py @@ -0,0 +1,19 @@ +from core.tools.errors import ToolProviderCredentialValidationError +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController + + +class AliYuqueProvider(BuiltinToolProviderController): + def _validate_credentials(self, credentials: dict) -> None: + token = credentials.get("token") + if not token: + raise ToolProviderCredentialValidationError("token is required") + + try: + resp = AliYuqueTool.auth(token) + if resp and resp.get("data", {}).get("id"): + return + + raise ToolProviderCredentialValidationError(resp) + except Exception as e: + raise ToolProviderCredentialValidationError(str(e)) diff --git a/api/core/tools/provider/builtin/aliyuque/aliyuque.yaml b/api/core/tools/provider/builtin/aliyuque/aliyuque.yaml new file mode 100644 index 0000000000..73d39aa96c --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/aliyuque.yaml @@ -0,0 +1,29 @@ +identity: + author: 佐井 + name: aliyuque + label: + en_US: yuque + zh_Hans: 语雀 + pt_BR: yuque + description: + en_US: Yuque, https://www.yuque.com. + zh_Hans: 语雀,https://www.yuque.com。 + pt_BR: Yuque, https://www.yuque.com. + icon: icon.svg + tags: + - productivity + - search +credentials_for_provider: + token: + type: secret-input + required: true + label: + en_US: Yuque Team Token + zh_Hans: 语雀团队Token + placeholder: + en_US: Please input your Yuque team token + zh_Hans: 请输入你的语雀团队Token + help: + en_US: Get Alibaba Yuque team token + zh_Hans: 先获取语雀团队Token + url: https://www.yuque.com/settings/tokens diff --git a/api/core/tools/provider/builtin/aliyuque/tools/base.py b/api/core/tools/provider/builtin/aliyuque/tools/base.py new file mode 100644 index 0000000000..fb7e219bff --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/base.py @@ -0,0 +1,50 @@ +""" +语雀客户端 +""" + +__author__ = "佐井" +__created__ = "2024-06-01 09:45:20" + +from typing import Any + +import requests + + +class AliYuqueTool: + # yuque service url + server_url = "https://www.yuque.com" + + @staticmethod + def auth(token): + session = requests.Session() + session.headers.update({"Accept": "application/json", "X-Auth-Token": token}) + login = session.request("GET", AliYuqueTool.server_url + "/api/v2/user") + login.raise_for_status() + resp = login.json() + return resp + + def request(self, method: str, token, tool_parameters: dict[str, Any], path: str) -> str: + if not token: + raise Exception("token is required") + session = requests.Session() + session.headers.update({"accept": "application/json", "X-Auth-Token": token}) + new_params = {**tool_parameters} + # 找出需要替换的变量 + replacements = {k: v for k, v in new_params.items() if f"{{{k}}}" in path} + + # 替换 path 中的变量 + for key, value in replacements.items(): + path = path.replace(f"{{{key}}}", str(value)) + del new_params[key] # 从 kwargs 中删除已经替换的变量 + # 请求接口 + if method.upper() in {"POST", "PUT"}: + session.headers.update( + { + "Content-Type": "application/json", + } + ) + response = session.request(method.upper(), self.server_url + path, json=new_params) + else: + response = session.request(method, self.server_url + path, params=new_params) + response.raise_for_status() + return response.text diff --git a/api/core/tools/provider/builtin/aliyuque/tools/create_document.py b/api/core/tools/provider/builtin/aliyuque/tools/create_document.py new file mode 100644 index 0000000000..feadc29258 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/create_document.py @@ -0,0 +1,22 @@ +""" +创建文档 +""" + +__author__ = "佐井" +__created__ = "2024-06-01 10:45:20" + +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.tool.builtin_tool import BuiltinTool + + +class AliYuqueCreateDocumentTool(AliYuqueTool, BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + token = self.runtime.credentials.get("token", None) + if not token: + raise Exception("token is required") + return self.create_text_message(self.request("POST", token, tool_parameters, "/api/v2/repos/{book_id}/docs")) diff --git a/api/core/tools/provider/builtin/aliyuque/tools/create_document.yaml b/api/core/tools/provider/builtin/aliyuque/tools/create_document.yaml new file mode 100644 index 0000000000..b9d1c60327 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/create_document.yaml @@ -0,0 +1,99 @@ +identity: + name: aliyuque_create_document + author: 佐井 + label: + en_US: Create Document + zh_Hans: 创建文档 + icon: icon.svg +description: + human: + en_US: Creates a new document within a knowledge base without automatic addition to the table of contents. Requires a subsequent call to the "knowledge base directory update API". Supports setting visibility, format, and content. # 接口英文描述 + zh_Hans: 在知识库中创建新文档,但不会自动加入目录,需额外调用“知识库目录更新接口”。允许设置公开性、格式及正文内容。 + llm: Creates docs in a KB. + +parameters: + - name: book_id + type: number + required: true + form: llm + label: + en_US: Knowledge Base ID + zh_Hans: 知识库ID + human_description: + en_US: The unique identifier of the knowledge base where the document will be created. + zh_Hans: 文档将被创建的知识库的唯一标识。 + llm_description: ID of the target knowledge base. + + - name: title + type: string + required: false + form: llm + label: + en_US: Title + zh_Hans: 标题 + human_description: + en_US: The title of the document, defaults to 'Untitled' if not provided. + zh_Hans: 文档标题,默认为'无标题'如未提供。 + llm_description: Title of the document, defaults to 'Untitled'. + + - name: public + type: select + required: false + form: llm + options: + - value: 0 + label: + en_US: Private + zh_Hans: 私密 + - value: 1 + label: + en_US: Public + zh_Hans: 公开 + - value: 2 + label: + en_US: Enterprise-only + zh_Hans: 企业内公开 + label: + en_US: Visibility + zh_Hans: 公开性 + human_description: + en_US: Document visibility (0 Private, 1 Public, 2 Enterprise-only). + zh_Hans: 文档可见性(0 私密, 1 公开, 2 企业内公开)。 + llm_description: Doc visibility options, 0-private, 1-public, 2-enterprise. + + - name: format + type: select + required: false + form: llm + options: + - value: markdown + label: + en_US: markdown + zh_Hans: markdown + - value: html + label: + en_US: html + zh_Hans: html + - value: lake + label: + en_US: lake + zh_Hans: lake + label: + en_US: Content Format + zh_Hans: 内容格式 + human_description: + en_US: Format of the document content (markdown, HTML, Lake). + zh_Hans: 文档内容格式(markdown, HTML, Lake)。 + llm_description: Content format choices, markdown, HTML, Lake. + + - name: body + type: string + required: true + form: llm + label: + en_US: Body Content + zh_Hans: 正文内容 + human_description: + en_US: The actual content of the document. + zh_Hans: 文档的实际内容。 + llm_description: Content of the document. diff --git a/api/core/tools/provider/builtin/aliyuque/tools/delete_document.py b/api/core/tools/provider/builtin/aliyuque/tools/delete_document.py new file mode 100644 index 0000000000..74c731a944 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/delete_document.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +""" +删除文档 +""" + +__author__ = "佐井" +__created__ = "2024-09-17 22:04" + +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.tool.builtin_tool import BuiltinTool + + +class AliYuqueDeleteDocumentTool(AliYuqueTool, BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + token = self.runtime.credentials.get("token", None) + if not token: + raise Exception("token is required") + return self.create_text_message( + self.request("DELETE", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}") + ) diff --git a/api/core/tools/provider/builtin/aliyuque/tools/delete_document.yaml b/api/core/tools/provider/builtin/aliyuque/tools/delete_document.yaml new file mode 100644 index 0000000000..87372c5350 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/delete_document.yaml @@ -0,0 +1,37 @@ +identity: + name: aliyuque_delete_document + author: 佐井 + label: + en_US: Delete Document + zh_Hans: 删除文档 + icon: icon.svg +description: + human: + en_US: Delete Document + zh_Hans: 根据id删除文档 + llm: Delete document. + +parameters: + - name: book_id + type: number + required: true + form: llm + label: + en_US: Knowledge Base ID + zh_Hans: 知识库ID + human_description: + en_US: The unique identifier of the knowledge base where the document will be created. + zh_Hans: 文档将被创建的知识库的唯一标识。 + llm_description: ID of the target knowledge base. + + - name: id + type: string + required: true + form: llm + label: + en_US: Document ID or Path + zh_Hans: 文档 ID or 路径 + human_description: + en_US: Document ID or path. + zh_Hans: 文档 ID or 路径。 + llm_description: Document ID or path. diff --git a/api/core/tools/provider/builtin/aliyuque/tools/describe_book_index_page.py b/api/core/tools/provider/builtin/aliyuque/tools/describe_book_index_page.py new file mode 100644 index 0000000000..02bf603a24 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/describe_book_index_page.py @@ -0,0 +1,24 @@ +""" +获取知识库首页 +""" + +__author__ = "佐井" +__created__ = "2024-06-01 22:57:14" + +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.tool.builtin_tool import BuiltinTool + + +class AliYuqueDescribeBookIndexPageTool(AliYuqueTool, BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + token = self.runtime.credentials.get("token", None) + if not token: + raise Exception("token is required") + return self.create_text_message( + self.request("GET", token, tool_parameters, "/api/v2/repos/{group_login}/{book_slug}/index_page") + ) diff --git a/api/core/tools/provider/builtin/aliyuque/tools/describe_book_index_page.yaml b/api/core/tools/provider/builtin/aliyuque/tools/describe_book_index_page.yaml new file mode 100644 index 0000000000..5e490725d1 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/describe_book_index_page.yaml @@ -0,0 +1,38 @@ +identity: + name: aliyuque_describe_book_index_page + author: 佐井 + label: + en_US: Get Repo Index Page + zh_Hans: 获取知识库首页 + icon: icon.svg + +description: + human: + en_US: Retrieves the homepage of a knowledge base within a group, supporting both book ID and group login with book slug access. + zh_Hans: 获取团队中知识库的首页信息,可通过书籍ID或团队登录名与书籍路径访问。 + llm: Fetches the knowledge base homepage using group and book identifiers with support for alternate access paths. + +parameters: + - name: group_login + type: string + required: true + form: llm + label: + en_US: Group Login + zh_Hans: 团队登录名 + human_description: + en_US: The login name of the group that owns the knowledge base. + zh_Hans: 拥有该知识库的团队登录名。 + llm_description: Team login identifier for the knowledge base owner. + + - name: book_slug + type: string + required: true + form: llm + label: + en_US: Book Slug + zh_Hans: 知识库路径 + human_description: + en_US: The unique slug representing the path of the knowledge base. + zh_Hans: 知识库的唯一路径标识。 + llm_description: Unique path identifier for the knowledge base. diff --git a/api/core/tools/provider/builtin/aliyuque/tools/describe_book_table_of_contents.py b/api/core/tools/provider/builtin/aliyuque/tools/describe_book_table_of_contents.py new file mode 100644 index 0000000000..fcfe449c6d --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/describe_book_table_of_contents.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +""" +获取知识库目录 +""" + +__author__ = "佐井" +__created__ = "2024-09-17 15:17:11" + +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.tool.builtin_tool import BuiltinTool + + +class YuqueDescribeBookTableOfContentsTool(AliYuqueTool, BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> (Union)[ToolInvokeMessage, list[ToolInvokeMessage]]: + token = self.runtime.credentials.get("token", None) + if not token: + raise Exception("token is required") + return self.create_text_message(self.request("GET", token, tool_parameters, "/api/v2/repos/{book_id}/toc")) diff --git a/api/core/tools/provider/builtin/aliyuque/tools/describe_book_table_of_contents.yaml b/api/core/tools/provider/builtin/aliyuque/tools/describe_book_table_of_contents.yaml new file mode 100644 index 0000000000..0c2bd22132 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/describe_book_table_of_contents.yaml @@ -0,0 +1,25 @@ +identity: + name: aliyuque_describe_book_table_of_contents + author: 佐井 + label: + en_US: Get Book's Table of Contents + zh_Hans: 获取知识库的目录 + icon: icon.svg +description: + human: + en_US: Get Book's Table of Contents. + zh_Hans: 获取知识库的目录。 + llm: Get Book's Table of Contents. + +parameters: + - name: book_id + type: number + required: true + form: llm + label: + en_US: Book ID + zh_Hans: 知识库 ID + human_description: + en_US: Book ID. + zh_Hans: 知识库 ID。 + llm_description: Book ID. diff --git a/api/core/tools/provider/builtin/aliyuque/tools/describe_document_content.py b/api/core/tools/provider/builtin/aliyuque/tools/describe_document_content.py new file mode 100644 index 0000000000..1e70593879 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/describe_document_content.py @@ -0,0 +1,61 @@ +""" +获取文档 +""" + +__author__ = "佐井" +__created__ = "2024-06-02 07:11:45" + +import json +from typing import Any, Union +from urllib.parse import urlparse + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.tool.builtin_tool import BuiltinTool + + +class AliYuqueDescribeDocumentContentTool(AliYuqueTool, BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + new_params = {**tool_parameters} + token = new_params.pop("token") + if not token or token.lower() == "none": + token = self.runtime.credentials.get("token", None) + if not token: + raise Exception("token is required") + new_params = {**tool_parameters} + url = new_params.pop("url") + if not url or not url.startswith("http"): + raise Exception("url is not valid") + + parsed_url = urlparse(url) + path_parts = parsed_url.path.strip("/").split("/") + if len(path_parts) < 3: + raise Exception("url is not correct") + doc_id = path_parts[-1] + book_slug = path_parts[-2] + group_id = path_parts[-3] + + # 1. 请求首页信息,获取book_id + new_params["group_login"] = group_id + new_params["book_slug"] = book_slug + index_page = json.loads( + self.request("GET", token, new_params, "/api/v2/repos/{group_login}/{book_slug}/index_page") + ) + book_id = index_page.get("data", {}).get("book", {}).get("id") + if not book_id: + raise Exception(f"can not parse book_id from {index_page}") + # 2. 获取文档内容 + new_params["book_id"] = book_id + new_params["id"] = doc_id + data = self.request("GET", token, new_params, "/api/v2/repos/{book_id}/docs/{id}") + data = json.loads(data) + body_only = tool_parameters.get("body_only") or "" + if body_only.lower() == "true": + return self.create_text_message(data.get("data").get("body")) + else: + raw = data.get("data") + del raw["body_lake"] + del raw["body_html"] + return self.create_text_message(json.dumps(data)) diff --git a/api/core/tools/provider/builtin/aliyuque/tools/describe_document_content.yaml b/api/core/tools/provider/builtin/aliyuque/tools/describe_document_content.yaml new file mode 100644 index 0000000000..6116886a96 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/describe_document_content.yaml @@ -0,0 +1,50 @@ +identity: + name: aliyuque_describe_document_content + author: 佐井 + label: + en_US: Fetch Document Content + zh_Hans: 获取文档内容 + icon: icon.svg + +description: + human: + en_US: Retrieves document content from Yuque based on the provided document URL, which can be a normal or shared link. + zh_Hans: 根据提供的语雀文档地址(支持正常链接或分享链接)获取文档内容。 + llm: Fetches Yuque document content given a URL. + +parameters: + - name: url + type: string + required: true + form: llm + label: + en_US: Document URL + zh_Hans: 文档地址 + human_description: + en_US: The URL of the document to retrieve content from, can be normal or shared. + zh_Hans: 需要获取内容的文档地址,可以是正常链接或分享链接。 + llm_description: URL of the Yuque document to fetch content. + + - name: body_only + type: string + required: false + form: llm + label: + en_US: return body content only + zh_Hans: 仅返回body内容 + human_description: + en_US: true:Body content only, false:Full response with metadata. + zh_Hans: true:仅返回body内容,不返回其他元数据,false:返回所有元数据。 + llm_description: true:Body content only, false:Full response with metadata. + + - name: token + type: secret-input + required: false + form: llm + label: + en_US: Yuque API Token + zh_Hans: 语雀接口Token + human_description: + en_US: The token for calling the Yuque API defaults to the Yuque token bound to the current tool if not provided. + zh_Hans: 调用语雀接口的token,如果不传则默认为当前工具绑定的语雀Token。 + llm_description: If the token for calling the Yuque API is not provided, it will default to the Yuque token bound to the current tool. diff --git a/api/core/tools/provider/builtin/aliyuque/tools/describe_documents.py b/api/core/tools/provider/builtin/aliyuque/tools/describe_documents.py new file mode 100644 index 0000000000..ed1b2a8643 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/describe_documents.py @@ -0,0 +1,24 @@ +""" +获取文档 +""" + +__author__ = "佐井" +__created__ = "2024-06-01 10:45:20" + +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.tool.builtin_tool import BuiltinTool + + +class AliYuqueDescribeDocumentsTool(AliYuqueTool, BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + token = self.runtime.credentials.get("token", None) + if not token: + raise Exception("token is required") + return self.create_text_message( + self.request("GET", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}") + ) diff --git a/api/core/tools/provider/builtin/aliyuque/tools/describe_documents.yaml b/api/core/tools/provider/builtin/aliyuque/tools/describe_documents.yaml new file mode 100644 index 0000000000..5156345d71 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/describe_documents.yaml @@ -0,0 +1,38 @@ +identity: + name: aliyuque_describe_documents + author: 佐井 + label: + en_US: Get Doc Detail + zh_Hans: 获取文档详情 + icon: icon.svg + +description: + human: + en_US: Retrieves detailed information of a specific document identified by its ID or path within a knowledge base. + zh_Hans: 根据知识库ID和文档ID或路径获取文档详细信息。 + llm: Fetches detailed doc info using ID/path from a knowledge base; supports doc lookup in Yuque. + +parameters: + - name: book_id + type: number + required: true + form: llm + label: + en_US: Knowledge Base ID + zh_Hans: 知识库 ID + human_description: + en_US: Identifier for the knowledge base where the document resides. + zh_Hans: 文档所属知识库的唯一标识。 + llm_description: ID of the knowledge base holding the document. + + - name: id + type: string + required: true + form: llm + label: + en_US: Document ID or Path + zh_Hans: 文档 ID 或路径 + human_description: + en_US: The unique identifier or path of the document to retrieve. + zh_Hans: 需要获取的文档的ID或其在知识库中的路径。 + llm_description: Unique doc ID or its path for retrieval. diff --git a/api/core/tools/provider/builtin/aliyuque/tools/update_book_table_of_contents.py b/api/core/tools/provider/builtin/aliyuque/tools/update_book_table_of_contents.py new file mode 100644 index 0000000000..932559445e --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/update_book_table_of_contents.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +""" +获取知识库目录 +""" + +__author__ = "佐井" +__created__ = "2024-09-17 15:17:11" + +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.tool.builtin_tool import BuiltinTool + + +class YuqueDescribeBookTableOfContentsTool(AliYuqueTool, BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> (Union)[ToolInvokeMessage, list[ToolInvokeMessage]]: + token = self.runtime.credentials.get("token", None) + if not token: + raise Exception("token is required") + + doc_ids = tool_parameters.get("doc_ids") + if doc_ids: + doc_ids = [int(doc_id.strip()) for doc_id in doc_ids.split(",")] + tool_parameters["doc_ids"] = doc_ids + + return self.create_text_message(self.request("PUT", token, tool_parameters, "/api/v2/repos/{book_id}/toc")) diff --git a/api/core/tools/provider/builtin/aliyuque/tools/update_book_table_of_contents.yaml b/api/core/tools/provider/builtin/aliyuque/tools/update_book_table_of_contents.yaml new file mode 100644 index 0000000000..f0c0024f17 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/update_book_table_of_contents.yaml @@ -0,0 +1,222 @@ +identity: + name: aliyuque_update_book_table_of_contents + author: 佐井 + label: + en_US: Update Book's Table of Contents + zh_Hans: 更新知识库目录 + icon: icon.svg +description: + human: + en_US: Update Book's Table of Contents. + zh_Hans: 更新知识库目录。 + llm: Update Book's Table of Contents. + +parameters: + - name: book_id + type: number + required: true + form: llm + label: + en_US: Book ID + zh_Hans: 知识库 ID + human_description: + en_US: Book ID. + zh_Hans: 知识库 ID。 + llm_description: Book ID. + + - name: action + type: select + required: true + form: llm + options: + - value: appendNode + label: + en_US: appendNode + zh_Hans: appendNode + pt_BR: appendNode + - value: prependNode + label: + en_US: prependNode + zh_Hans: prependNode + pt_BR: prependNode + - value: editNode + label: + en_US: editNode + zh_Hans: editNode + pt_BR: editNode + - value: editNode + label: + en_US: removeNode + zh_Hans: removeNode + pt_BR: removeNode + label: + en_US: Action Type + zh_Hans: 操作 + human_description: + en_US: In the operation scenario, sibling node prepending is not supported, deleting a node doesn't remove associated documents, and node deletion has two modes, 'sibling' (delete current node) and 'child' (delete current node and its children). + zh_Hans: 操作,创建场景下不支持同级头插 prependNode,删除节点不会删除关联文档,删除节点时action_mode=sibling (删除当前节点), action_mode=child (删除当前节点及子节点) + llm_description: In the operation scenario, sibling node prepending is not supported, deleting a node doesn't remove associated documents, and node deletion has two modes, 'sibling' (delete current node) and 'child' (delete current node and its children). + + + - name: action_mode + type: select + required: false + form: llm + options: + - value: sibling + label: + en_US: sibling + zh_Hans: 同级 + pt_BR: sibling + - value: child + label: + en_US: child + zh_Hans: 子集 + pt_BR: child + label: + en_US: Action Type + zh_Hans: 操作 + human_description: + en_US: Operation mode (sibling:same level, child:child level). + zh_Hans: 操作模式 (sibling:同级, child:子级)。 + llm_description: Operation mode (sibling:same level, child:child level). + + - name: target_uuid + type: string + required: false + form: llm + label: + en_US: Target node UUID + zh_Hans: 目标节点 UUID + human_description: + en_US: Target node UUID, defaults to root node if left empty. + zh_Hans: 目标节点 UUID, 不填默认为根节点。 + llm_description: Target node UUID, defaults to root node if left empty. + + - name: node_uuid + type: string + required: false + form: llm + label: + en_US: Node UUID + zh_Hans: 操作节点 UUID + human_description: + en_US: Operation node UUID [required for move/update/delete]. + zh_Hans: 操作节点 UUID [移动/更新/删除必填]。 + llm_description: Operation node UUID [required for move/update/delete]. + + - name: doc_ids + type: string + required: false + form: llm + label: + en_US: Document IDs + zh_Hans: 文档id列表 + human_description: + en_US: Document IDs [required for creating documents], separate multiple IDs with ','. + zh_Hans: 文档 IDs [创建文档必填],多个用','分隔。 + llm_description: Document IDs [required for creating documents], separate multiple IDs with ','. + + + - name: type + type: select + required: false + form: llm + default: DOC + options: + - value: DOC + label: + en_US: DOC + zh_Hans: 文档 + pt_BR: DOC + - value: LINK + label: + en_US: LINK + zh_Hans: 链接 + pt_BR: LINK + - value: TITLE + label: + en_US: TITLE + zh_Hans: 分组 + pt_BR: TITLE + label: + en_US: Node type + zh_Hans: 操节点类型 + human_description: + en_US: Node type [required for creation] (DOC:document, LINK:external link, TITLE:group). + zh_Hans: 操节点类型 [创建必填] (DOC:文档, LINK:外链, TITLE:分组)。 + llm_description: Node type [required for creation] (DOC:document, LINK:external link, TITLE:group). + + - name: title + type: string + required: false + form: llm + label: + en_US: Node Name + zh_Hans: 节点名称 + human_description: + en_US: Node name [required for creating groups/external links]. + zh_Hans: 节点名称 [创建分组/外链必填]。 + llm_description: Node name [required for creating groups/external links]. + + - name: url + type: string + required: false + form: llm + label: + en_US: Node URL + zh_Hans: 节点URL + human_description: + en_US: Node URL [required for creating external links]. + zh_Hans: 节点 URL [创建外链必填]。 + llm_description: Node URL [required for creating external links]. + + + - name: open_window + type: select + required: false + form: llm + default: 0 + options: + - value: 0 + label: + en_US: DOC + zh_Hans: Current Page + pt_BR: DOC + - value: 1 + label: + en_US: LINK + zh_Hans: New Page + pt_BR: LINK + label: + en_US: Open in new window + zh_Hans: 是否新窗口打开 + human_description: + en_US: Open in new window [optional for external links] (0:open in current page, 1:open in new window). + zh_Hans: 是否新窗口打开 [外链选填] (0:当前页打开, 1:新窗口打开)。 + llm_description: Open in new window [optional for external links] (0:open in current page, 1:open in new window). + + + - name: visible + type: select + required: false + form: llm + default: 1 + options: + - value: 0 + label: + en_US: Invisible + zh_Hans: 隐藏 + pt_BR: Invisible + - value: 1 + label: + en_US: Visible + zh_Hans: 可见 + pt_BR: Visible + label: + en_US: Visibility + zh_Hans: 是否可见 + human_description: + en_US: Visibility (0:invisible, 1:visible). + zh_Hans: 是否可见 (0:不可见, 1:可见)。 + llm_description: Visibility (0:invisible, 1:visible). diff --git a/api/core/tools/provider/builtin/aliyuque/tools/update_document.py b/api/core/tools/provider/builtin/aliyuque/tools/update_document.py new file mode 100644 index 0000000000..0c6e0205e1 --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/update_document.py @@ -0,0 +1,24 @@ +""" +更新文档 +""" + +__author__ = "佐井" +__created__ = "2024-06-19 16:50:07" + +from typing import Any, Union + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool +from core.tools.tool.builtin_tool import BuiltinTool + + +class AliYuqueUpdateDocumentTool(AliYuqueTool, BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + token = self.runtime.credentials.get("token", None) + if not token: + raise Exception("token is required") + return self.create_text_message( + self.request("PUT", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}") + ) diff --git a/api/core/tools/provider/builtin/aliyuque/tools/update_document.yaml b/api/core/tools/provider/builtin/aliyuque/tools/update_document.yaml new file mode 100644 index 0000000000..87f88c9b1b --- /dev/null +++ b/api/core/tools/provider/builtin/aliyuque/tools/update_document.yaml @@ -0,0 +1,87 @@ +identity: + name: aliyuque_update_document + author: 佐井 + label: + en_US: Update Document + zh_Hans: 更新文档 + icon: icon.svg +description: + human: + en_US: Update an existing document within a specified knowledge base by providing the document ID or path. + zh_Hans: 通过提供文档ID或路径,更新指定知识库中的现有文档。 + llm: Update doc in a knowledge base via ID/path. +parameters: + - name: book_id + type: number + required: true + form: llm + label: + en_US: Knowledge Base ID + zh_Hans: 知识库 ID + human_description: + en_US: The unique identifier of the knowledge base where the document resides. + zh_Hans: 文档所属知识库的ID。 + llm_description: ID of the knowledge base holding the doc. + - name: id + type: string + required: true + form: llm + label: + en_US: Document ID or Path + zh_Hans: 文档 ID 或 路径 + human_description: + en_US: The unique identifier or the path of the document to be updated. + zh_Hans: 要更新的文档的唯一ID或路径。 + llm_description: Doc's ID or path for update. + + - name: title + type: string + required: false + form: llm + label: + en_US: Title + zh_Hans: 标题 + human_description: + en_US: The title of the document, defaults to 'Untitled' if not provided. + zh_Hans: 文档标题,默认为'无标题'如未提供。 + llm_description: Title of the document, defaults to 'Untitled'. + + - name: format + type: select + required: false + form: llm + options: + - value: markdown + label: + en_US: markdown + zh_Hans: markdown + pt_BR: markdown + - value: html + label: + en_US: html + zh_Hans: html + pt_BR: html + - value: lake + label: + en_US: lake + zh_Hans: lake + pt_BR: lake + label: + en_US: Content Format + zh_Hans: 内容格式 + human_description: + en_US: Format of the document content (markdown, HTML, Lake). + zh_Hans: 文档内容格式(markdown, HTML, Lake)。 + llm_description: Content format choices, markdown, HTML, Lake. + + - name: body + type: string + required: true + form: llm + label: + en_US: Body Content + zh_Hans: 正文内容 + human_description: + en_US: The actual content of the document. + zh_Hans: 文档的实际内容。 + llm_description: Content of the document. From b2aa385942a74f7cba5cfa2e32e9027ca5860bd5 Mon Sep 17 00:00:00 2001 From: ice yao Date: Fri, 18 Oct 2024 20:08:41 +0800 Subject: [PATCH 005/155] feat: Add volcengine tos storage test (#9495) --- api/tests/unit_tests/oss/__mock/__init__.py | 0 .../unit_tests/oss/__mock/volcengine_tos.py | 100 ++++++++++++++++++ .../unit_tests/oss/volcengine_tos/__init__.py | 0 .../oss/volcengine_tos/test_volcengine_tos.py | 67 ++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 api/tests/unit_tests/oss/__mock/__init__.py create mode 100644 api/tests/unit_tests/oss/__mock/volcengine_tos.py create mode 100644 api/tests/unit_tests/oss/volcengine_tos/__init__.py create mode 100644 api/tests/unit_tests/oss/volcengine_tos/test_volcengine_tos.py diff --git a/api/tests/unit_tests/oss/__mock/__init__.py b/api/tests/unit_tests/oss/__mock/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/oss/__mock/volcengine_tos.py b/api/tests/unit_tests/oss/__mock/volcengine_tos.py new file mode 100644 index 0000000000..241764c521 --- /dev/null +++ b/api/tests/unit_tests/oss/__mock/volcengine_tos.py @@ -0,0 +1,100 @@ +import os +from typing import Union +from unittest.mock import MagicMock + +import pytest +from _pytest.monkeypatch import MonkeyPatch +from tos import TosClientV2 +from tos.clientv2 import DeleteObjectOutput, GetObjectOutput, HeadObjectOutput, PutObjectOutput + + +class AttrDict(dict): + def __getattr__(self, item): + return self.get(item) + + +def get_example_bucket() -> str: + return "dify" + + +def get_example_filename() -> str: + return "test.txt" + + +def get_example_data() -> bytes: + return b"test" + + +def get_example_filepath() -> str: + return "/test" + + +class MockVolcengineTosClass: + def __init__(self, ak="", sk="", endpoint="", region=""): + self.bucket_name = get_example_bucket() + self.key = get_example_filename() + self.content = get_example_data() + self.filepath = get_example_filepath() + self.resp = AttrDict( + { + "x-tos-server-side-encryption": "kms", + "x-tos-server-side-encryption-kms-key-id": "trn:kms:cn-beijing:****:keyrings/ring-test/keys/key-test", + "x-tos-server-side-encryption-customer-algorithm": "AES256", + "x-tos-version-id": "test", + "x-tos-hash-crc64ecma": 123456, + "request_id": "test", + "headers": { + "x-tos-id-2": "test", + "ETag": "123456", + }, + "status": 200, + } + ) + + def put_object(self, bucket: str, key: str, content=None) -> PutObjectOutput: + assert bucket == self.bucket_name + assert key == self.key + assert content == self.content + return PutObjectOutput(self.resp) + + def get_object(self, bucket: str, key: str) -> GetObjectOutput: + assert bucket == self.bucket_name + assert key == self.key + + get_object_output = MagicMock(GetObjectOutput) + get_object_output.read.return_value = self.content + return get_object_output + + def get_object_to_file(self, bucket: str, key: str, file_path: str): + assert bucket == self.bucket_name + assert key == self.key + assert file_path == self.filepath + + def head_object(self, bucket: str, key: str) -> HeadObjectOutput: + assert bucket == self.bucket_name + assert key == self.key + return HeadObjectOutput(self.resp) + + def delete_object(self, bucket: str, key: str): + assert bucket == self.bucket_name + assert key == self.key + return DeleteObjectOutput(self.resp) + + +MOCK = os.getenv("MOCK_SWITCH", "false").lower() == "true" + + +@pytest.fixture +def setup_volcengine_tos_mock(monkeypatch: MonkeyPatch): + if MOCK: + monkeypatch.setattr(TosClientV2, "__init__", MockVolcengineTosClass.__init__) + monkeypatch.setattr(TosClientV2, "put_object", MockVolcengineTosClass.put_object) + monkeypatch.setattr(TosClientV2, "get_object", MockVolcengineTosClass.get_object) + monkeypatch.setattr(TosClientV2, "get_object_to_file", MockVolcengineTosClass.get_object_to_file) + monkeypatch.setattr(TosClientV2, "head_object", MockVolcengineTosClass.head_object) + monkeypatch.setattr(TosClientV2, "delete_object", MockVolcengineTosClass.delete_object) + + yield + + if MOCK: + monkeypatch.undo() diff --git a/api/tests/unit_tests/oss/volcengine_tos/__init__.py b/api/tests/unit_tests/oss/volcengine_tos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/oss/volcengine_tos/test_volcengine_tos.py b/api/tests/unit_tests/oss/volcengine_tos/test_volcengine_tos.py new file mode 100644 index 0000000000..3f334a3764 --- /dev/null +++ b/api/tests/unit_tests/oss/volcengine_tos/test_volcengine_tos.py @@ -0,0 +1,67 @@ +from collections.abc import Generator + +from flask import Flask +from tos import TosClientV2 +from tos.clientv2 import GetObjectOutput, HeadObjectOutput, PutObjectOutput + +from extensions.storage.volcengine_tos_storage import VolcengineTosStorage +from tests.unit_tests.oss.__mock.volcengine_tos import ( + get_example_bucket, + get_example_data, + get_example_filename, + get_example_filepath, + setup_volcengine_tos_mock, +) + + +class VolcengineTosTest: + _instance = None + + def __new__(cls): + if cls._instance == None: + cls._instance = object.__new__(cls) + return cls._instance + else: + return cls._instance + + def __init__(self): + self.storage = VolcengineTosStorage(app=Flask(__name__)) + self.storage.bucket_name = get_example_bucket() + self.storage.client = TosClientV2( + ak="dify", + sk="dify", + endpoint="https://xxx.volces.com", + region="cn-beijing", + ) + + +def test_save(setup_volcengine_tos_mock): + volc_tos = VolcengineTosTest() + volc_tos.storage.save(get_example_filename(), get_example_data()) + + +def test_load_once(setup_volcengine_tos_mock): + volc_tos = VolcengineTosTest() + assert volc_tos.storage.load_once(get_example_filename()) == get_example_data() + + +def test_load_stream(setup_volcengine_tos_mock): + volc_tos = VolcengineTosTest() + generator = volc_tos.storage.load_stream(get_example_filename()) + assert isinstance(generator, Generator) + assert next(generator) == get_example_data() + + +def test_download(setup_volcengine_tos_mock): + volc_tos = VolcengineTosTest() + volc_tos.storage.download(get_example_filename(), get_example_filepath()) + + +def test_exists(setup_volcengine_tos_mock): + volc_tos = VolcengineTosTest() + assert volc_tos.storage.exists(get_example_filename()) + + +def test_delete(setup_volcengine_tos_mock): + volc_tos = VolcengineTosTest() + volc_tos.storage.delete(get_example_filename()) From a9fc85027d31d76cd8714d6d2633c208cc2f2dcb Mon Sep 17 00:00:00 2001 From: Zven Date: Fri, 18 Oct 2024 20:22:57 +0800 Subject: [PATCH 006/155] chore: update the description for storage_type (#9492) --- api/.env.example | 2 +- api/configs/middleware/__init__.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/.env.example b/api/.env.example index aa155003ab..a7f5206088 100644 --- a/api/.env.example +++ b/api/.env.example @@ -42,7 +42,7 @@ DB_DATABASE=dify # Storage configuration # use for store upload files, private keys... -# storage type: local, s3, azure-blob, google-storage, tencent-cos, huawei-obs, volcengine-tos, baidu-obs, supabase +# storage type: local, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase STORAGE_TYPE=local STORAGE_LOCAL_PATH=storage S3_USE_AWS_MANAGED_IAM=false diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py index fa7f41d630..84d03e2f45 100644 --- a/api/configs/middleware/__init__.py +++ b/api/configs/middleware/__init__.py @@ -35,7 +35,8 @@ from configs.middleware.vdb.weaviate_config import WeaviateConfig class StorageConfig(BaseSettings): STORAGE_TYPE: str = Field( description="Type of storage to use." - " Options: 'local', 's3', 'azure-blob', 'aliyun-oss', 'google-storage'. Default is 'local'.", + " Options: 'local', 's3', 'aliyun-oss', 'azure-blob', 'baidu-obs', 'google-storage', 'huawei-obs', " + "'oci-storage', 'tencent-cos', 'volcengine-tos', 'supabase'. Default is 'local'.", default="local", ) From ce476f2e5c9df876d2f4ca166e1464a9451f4214 Mon Sep 17 00:00:00 2001 From: Chenhe Gu Date: Sat, 19 Oct 2024 09:58:22 +0800 Subject: [PATCH 007/155] refine wording in license (#9505) --- LICENSE | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 06b0fa1d12..d7b8373839 100644 --- a/LICENSE +++ b/LICENSE @@ -6,8 +6,9 @@ Dify is licensed under the Apache License 2.0, with the following additional con a. Multi-tenant service: Unless explicitly authorized by Dify in writing, you may not use the Dify source code to operate a multi-tenant environment. - Tenant Definition: Within the context of Dify, one tenant corresponds to one workspace. The workspace provides a separated area for each tenant's data and configurations. - -b. LOGO and copyright information: In the process of using Dify's frontend components, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend components. + +b. LOGO and copyright information: In the process of using Dify's frontend, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend. + - Frontend Definition: For the purposes of this license, the "frontend" of Dify includes all components located in the `web/` directory when running Dify from the raw source code, or the "web" image when running Dify with Docker. Please contact business@dify.ai by email to inquire about licensing matters. From c71af7f610c84fa989997cd0b938146bdf705b21 Mon Sep 17 00:00:00 2001 From: zhuhao <37029601+hwzhuhao@users.noreply.github.com> Date: Sat, 19 Oct 2024 09:58:50 +0800 Subject: [PATCH 008/155] fix: resolve the error of docker-compose startup when the storage is baidu-obs (#9502) --- docker/docker-compose.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 065c5e92bc..a3ea7c6059 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -96,6 +96,10 @@ x-shared-env: &shared-api-worker-env VOLCENGINE_TOS_ACCESS_KEY: ${VOLCENGINE_TOS_ACCESS_KEY:-} VOLCENGINE_TOS_ENDPOINT: ${VOLCENGINE_TOS_ENDPOINT:-} VOLCENGINE_TOS_REGION: ${VOLCENGINE_TOS_REGION:-} + BAIDU_OBS_BUCKET_NAME: ${BAIDU_OBS_BUCKET_NAME:-} + BAIDU_OBS_SECRET_KEY: ${BAIDU_OBS_SECRET_KEY:-} + BAIDU_OBS_ACCESS_KEY: ${BAIDU_OBS_ACCESS_KEY:-} + BAIDU_OBS_ENDPOINT: ${BAIDU_OBS_ENDPOINT:-} VECTOR_STORE: ${VECTOR_STORE:-weaviate} WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080} WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih} From 660fc3bb347b4b4b865ca2bdd449bb317cf84c24 Mon Sep 17 00:00:00 2001 From: Ziyu Huang Date: Sun, 20 Oct 2024 21:59:58 +0800 Subject: [PATCH 009/155] Resolve 9508 openai compatible rerank (#9511) --- .../openai_api_compatible.yaml | 14 ++ .../openai_api_compatible/rerank/__init__.py | 0 .../openai_api_compatible/rerank/rerank.py | 159 ++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 api/core/model_runtime/model_providers/openai_api_compatible/rerank/__init__.py create mode 100644 api/core/model_runtime/model_providers/openai_api_compatible/rerank/rerank.py diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/openai_api_compatible.yaml b/api/core/model_runtime/model_providers/openai_api_compatible/openai_api_compatible.yaml index 88c76fe16e..69a081f35c 100644 --- a/api/core/model_runtime/model_providers/openai_api_compatible/openai_api_compatible.yaml +++ b/api/core/model_runtime/model_providers/openai_api_compatible/openai_api_compatible.yaml @@ -8,6 +8,7 @@ supported_model_types: - llm - text-embedding - speech2text + - rerank configurate_methods: - customizable-model model_credential_schema: @@ -83,6 +84,19 @@ model_credential_schema: placeholder: zh_Hans: 在此输入您的模型上下文长度 en_US: Enter your Model context size + - variable: context_size + label: + zh_Hans: 模型上下文长度 + en_US: Model context size + required: true + show_on: + - variable: __model_type + value: rerank + type: text-input + default: '4096' + placeholder: + zh_Hans: 在此输入您的模型上下文长度 + en_US: Enter your Model context size - variable: max_tokens_to_sample label: zh_Hans: 最大 token 上限 diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/rerank/__init__.py b/api/core/model_runtime/model_providers/openai_api_compatible/rerank/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/rerank/rerank.py b/api/core/model_runtime/model_providers/openai_api_compatible/rerank/rerank.py new file mode 100644 index 0000000000..508da4bf20 --- /dev/null +++ b/api/core/model_runtime/model_providers/openai_api_compatible/rerank/rerank.py @@ -0,0 +1,159 @@ +from json import dumps +from typing import Optional + +import httpx +from requests import post +from yarl import URL + +from core.model_runtime.entities.common_entities import I18nObject +from core.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType +from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult +from core.model_runtime.errors.invoke import ( + InvokeAuthorizationError, + InvokeBadRequestError, + InvokeConnectionError, + InvokeError, + InvokeRateLimitError, + InvokeServerUnavailableError, +) +from core.model_runtime.errors.validate import CredentialsValidateFailedError +from core.model_runtime.model_providers.__base.rerank_model import RerankModel + + +class OAICompatRerankModel(RerankModel): + """ + rerank model API is compatible with Jina rerank model API. So copy the JinaRerankModel class code here. + we need enhance for llama.cpp , which return raw score, not normalize score 0~1. It seems Dify need it + """ + + def _invoke( + self, + model: str, + credentials: dict, + query: str, + docs: list[str], + score_threshold: Optional[float] = None, + top_n: Optional[int] = None, + user: Optional[str] = None, + ) -> RerankResult: + """ + Invoke rerank model + + :param model: model name + :param credentials: model credentials + :param query: search query + :param docs: docs for reranking + :param score_threshold: score threshold + :param top_n: top n documents to return + :param user: unique user id + :return: rerank result + """ + if len(docs) == 0: + return RerankResult(model=model, docs=[]) + + server_url = credentials["endpoint_url"] + model_name = model + + if not server_url: + raise CredentialsValidateFailedError("server_url is required") + if not model_name: + raise CredentialsValidateFailedError("model_name is required") + + url = server_url + headers = {"Authorization": f"Bearer {credentials.get('api_key')}", "Content-Type": "application/json"} + + # TODO: Do we need truncate docs to avoid llama.cpp return error? + + data = {"model": model_name, "query": query, "documents": docs, "top_n": top_n} + + try: + response = post(str(URL(url) / "rerank"), headers=headers, data=dumps(data), timeout=60) + response.raise_for_status() + results = response.json() + + rerank_documents = [] + scores = [result["relevance_score"] for result in results["results"]] + + # Min-Max Normalization: Normalize scores to 0 ~ 1.0 range + min_score = min(scores) + max_score = max(scores) + score_range = max_score - min_score if max_score != min_score else 1.0 # Avoid division by zero + + for result in results["results"]: + index = result["index"] + + # Retrieve document text (fallback if llama.cpp rerank doesn't return it) + text = result.get("document", {}).get("text", docs[index]) + + # Normalize the score + normalized_score = (result["relevance_score"] - min_score) / score_range + + # Create RerankDocument object with normalized score + rerank_document = RerankDocument( + index=index, + text=text, + score=normalized_score, + ) + + # Apply threshold (if defined) + if score_threshold is None or normalized_score >= score_threshold: + rerank_documents.append(rerank_document) + + # Sort rerank_documents by normalized score in descending order + rerank_documents.sort(key=lambda doc: doc.score, reverse=True) + + return RerankResult(model=model, docs=rerank_documents) + + except httpx.HTTPStatusError as e: + raise InvokeServerUnavailableError(str(e)) + + def validate_credentials(self, model: str, credentials: dict) -> None: + """ + Validate model credentials + + :param model: model name + :param credentials: model credentials + :return: + """ + try: + self._invoke( + model=model, + credentials=credentials, + query="What is the capital of the United States?", + docs=[ + "Carson City is the capital city of the American state of Nevada. At the 2010 United States " + "Census, Carson City had a population of 55,274.", + "The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that " + "are a political division controlled by the United States. Its capital is Saipan.", + ], + score_threshold=0.8, + ) + except Exception as ex: + raise CredentialsValidateFailedError(str(ex)) + + @property + def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]: + """ + Map model invoke error to unified error + """ + return { + InvokeConnectionError: [httpx.ConnectError], + InvokeServerUnavailableError: [httpx.RemoteProtocolError], + InvokeRateLimitError: [], + InvokeAuthorizationError: [httpx.HTTPStatusError], + InvokeBadRequestError: [httpx.RequestError], + } + + def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity: + """ + generate custom model entities from credentials + """ + entity = AIModelEntity( + model=model, + label=I18nObject(en_US=model), + model_type=ModelType.RERANK, + fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, + model_properties={}, + ) + + return entity From 95ce10f23bb848a13c51706945b200e9a6761b7f Mon Sep 17 00:00:00 2001 From: ice yao Date: Sun, 20 Oct 2024 22:06:18 +0800 Subject: [PATCH 010/155] feat: Add custom username and avatar define in discord tool (#9514) --- .../builtin/discord/tools/discord_webhook.py | 6 ++--- .../discord/tools/discord_webhook.yaml | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/api/core/tools/provider/builtin/discord/tools/discord_webhook.py b/api/core/tools/provider/builtin/discord/tools/discord_webhook.py index 7fdf791aba..c1834a1a26 100644 --- a/api/core/tools/provider/builtin/discord/tools/discord_webhook.py +++ b/api/core/tools/provider/builtin/discord/tools/discord_webhook.py @@ -21,7 +21,6 @@ class DiscordWebhookTool(BuiltinTool): return self.create_text_message("Invalid parameter content") webhook_url = tool_parameters.get("webhook_url", "") - if not webhook_url.startswith("https://discord.com/api/webhooks/"): return self.create_text_message( f"Invalid parameter webhook_url ${webhook_url}, \ @@ -31,13 +30,14 @@ class DiscordWebhookTool(BuiltinTool): headers = { "Content-Type": "application/json", } - params = {} payload = { + "username": tool_parameters.get("username") or user_id, "content": content, + "avatar_url": tool_parameters.get("avatar_url") or None, } try: - res = httpx.post(webhook_url, headers=headers, params=params, json=payload) + res = httpx.post(webhook_url, headers=headers, json=payload) if res.is_success: return self.create_text_message("Text message was sent successfully") else: diff --git a/api/core/tools/provider/builtin/discord/tools/discord_webhook.yaml b/api/core/tools/provider/builtin/discord/tools/discord_webhook.yaml index bb3fa43f24..6847b973ca 100644 --- a/api/core/tools/provider/builtin/discord/tools/discord_webhook.yaml +++ b/api/core/tools/provider/builtin/discord/tools/discord_webhook.yaml @@ -38,3 +38,28 @@ parameters: pt_BR: Content to sent to the channel or person. llm_description: Content of the message form: llm + - name: username + type: string + required: false + label: + en_US: Discord Webhook Username + zh_Hans: Discord Webhook用户名 + pt_BR: Discord Webhook Username + human_description: + en_US: Discord Webhook Username + zh_Hans: Discord Webhook用户名 + pt_BR: Discord Webhook Username + llm_description: Discord Webhook Username + form: llm + - name: avatar_url + type: string + required: false + label: + en_US: Discord Webhook Avatar + zh_Hans: Discord Webhook头像 + pt_BR: Discord Webhook Avatar + human_description: + en_US: Discord Webhook Avatar URL + zh_Hans: Discord Webhook头像地址 + pt_BR: Discord Webhook Avatar URL + form: form From 444dc019313de08657dec298d5fc94b94c136a74 Mon Sep 17 00:00:00 2001 From: kurokobo Date: Sun, 20 Oct 2024 23:53:32 +0900 Subject: [PATCH 011/155] fix: ignore all files except for .gitkeep under docker/nginx/ssl by gitignore (#9518) --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 29374d13dc..27cf8a4ba3 100644 --- a/.gitignore +++ b/.gitignore @@ -175,6 +175,8 @@ docker/volumes/pgvector/data/* docker/volumes/pgvecto_rs/data/* docker/nginx/conf.d/default.conf +docker/nginx/ssl/* +!docker/nginx/ssl/.gitkeep docker/middleware.env sdks/python-client/build From 42fe208edad6efcb85dff7ac00cd7d664fd61158 Mon Sep 17 00:00:00 2001 From: chzphoenix Date: Mon, 21 Oct 2024 09:03:25 +0800 Subject: [PATCH 012/155] refactor wenxin rerank (#9486) Co-authored-by: cuihz --- .../model_providers/wenxin/rerank/rerank.py | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/api/core/model_runtime/model_providers/wenxin/rerank/rerank.py b/api/core/model_runtime/model_providers/wenxin/rerank/rerank.py index b22aead22b..9e6a7dd99e 100644 --- a/api/core/model_runtime/model_providers/wenxin/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/wenxin/rerank/rerank.py @@ -2,20 +2,15 @@ from typing import Optional import httpx -from core.model_runtime.entities.common_entities import I18nObject -from core.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelPropertyKey, ModelType from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult -from core.model_runtime.errors.invoke import ( - InvokeAuthorizationError, - InvokeBadRequestError, - InvokeConnectionError, - InvokeError, - InvokeRateLimitError, - InvokeServerUnavailableError, -) +from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.validate import CredentialsValidateFailedError from core.model_runtime.model_providers.__base.rerank_model import RerankModel from core.model_runtime.model_providers.wenxin._common import _CommonWenxin +from core.model_runtime.model_providers.wenxin.wenxin_errors import ( + InternalServerError, + invoke_error_mapping, +) class WenxinRerank(_CommonWenxin): @@ -32,7 +27,7 @@ class WenxinRerank(_CommonWenxin): response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: - raise InvokeServerUnavailableError(str(e)) + raise InternalServerError(str(e)) class WenxinRerankModel(RerankModel): @@ -93,7 +88,7 @@ class WenxinRerankModel(RerankModel): return RerankResult(model=model, docs=rerank_documents) except httpx.HTTPStatusError as e: - raise InvokeServerUnavailableError(str(e)) + raise InternalServerError(str(e)) def validate_credentials(self, model: str, credentials: dict) -> None: """ @@ -124,24 +119,4 @@ class WenxinRerankModel(RerankModel): """ Map model invoke error to unified error """ - return { - InvokeConnectionError: [httpx.ConnectError], - InvokeServerUnavailableError: [httpx.RemoteProtocolError], - InvokeRateLimitError: [], - InvokeAuthorizationError: [httpx.HTTPStatusError], - InvokeBadRequestError: [httpx.RequestError], - } - - def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity: - """ - generate custom model entities from credentials - """ - entity = AIModelEntity( - model=model, - label=I18nObject(en_US=model), - model_type=ModelType.RERANK, - fetch_from=FetchFrom.CUSTOMIZABLE_MODEL, - model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))}, - ) - - return entity + return invoke_error_mapping() From 853b0e84ccb21390ad0b88540480443afdd2c396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Mon, 21 Oct 2024 09:05:42 +0800 Subject: [PATCH 013/155] fix: draft run workflow node with image will raise error (#9406) --- api/core/workflow/workflow_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index 74a598ada5..d3576197d1 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -289,7 +289,7 @@ class WorkflowEntry: new_value.append(file) if new_value: - value = new_value + input_value = new_value # append variable and value to variable pool variable_pool.add([variable_node_id] + variable_key_list, input_value) From 3898fe3311a530347d0520f039e355338e3e06fa Mon Sep 17 00:00:00 2001 From: NFish Date: Mon, 21 Oct 2024 09:23:20 +0800 Subject: [PATCH 014/155] New Auth Methods (#8119) --- web/app/account/account-page/index.tsx | 79 +++- web/app/account/avatar.tsx | 2 +- web/app/activate/activateForm.tsx | 202 +--------- .../base/icons/assets/public/common/lock.svg | 5 + .../account-setting/account-page/index.tsx | 7 +- web/app/components/signin/countdown.tsx | 41 ++ web/app/components/swr-initor.tsx | 70 +++- .../forgot-password/ChangePasswordForm.tsx | 41 +- .../forgot-password/ForgotPasswordForm.tsx | 6 +- web/app/install/installForm.tsx | 3 +- web/app/reset-password/check-code/page.tsx | 92 +++++ web/app/reset-password/layout.tsx | 39 ++ web/app/reset-password/page.tsx | 101 +++++ web/app/reset-password/set-password/page.tsx | 193 +++++++++ web/app/signin/check-code/page.tsx | 96 +++++ .../signin/components/mail-and-code-auth.tsx | 71 ++++ .../components/mail-and-password-auth.tsx | 167 ++++++++ web/app/signin/components/social-auth.tsx | 62 +++ web/app/signin/components/sso-auth.tsx | 73 ++++ web/app/signin/forms.tsx | 34 -- web/app/signin/invite-settings/page.tsx | 154 +++++++ web/app/signin/layout.tsx | 54 +++ web/app/signin/normalForm.tsx | 377 ++++++------------ web/app/signin/oneMoreStep.tsx | 34 +- web/app/signin/page.tsx | 95 +---- web/app/signin/userSSOForm.tsx | 107 ----- web/i18n/en-US/login.ts | 42 +- web/i18n/zh-Hans/login.ts | 43 +- web/i18n/zh-Hant/login.ts | 4 +- web/service/common.ts | 26 +- web/service/sso.ts | 15 +- web/types/feature.ts | 20 +- 32 files changed, 1568 insertions(+), 787 deletions(-) create mode 100644 web/app/components/base/icons/assets/public/common/lock.svg create mode 100644 web/app/components/signin/countdown.tsx create mode 100644 web/app/reset-password/check-code/page.tsx create mode 100644 web/app/reset-password/layout.tsx create mode 100644 web/app/reset-password/page.tsx create mode 100644 web/app/reset-password/set-password/page.tsx create mode 100644 web/app/signin/check-code/page.tsx create mode 100644 web/app/signin/components/mail-and-code-auth.tsx create mode 100644 web/app/signin/components/mail-and-password-auth.tsx create mode 100644 web/app/signin/components/social-auth.tsx create mode 100644 web/app/signin/components/sso-auth.tsx delete mode 100644 web/app/signin/forms.tsx create mode 100644 web/app/signin/invite-settings/page.tsx create mode 100644 web/app/signin/layout.tsx delete mode 100644 web/app/signin/userSSOForm.tsx diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index 53f7692e6c..b42b481eba 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -15,6 +15,7 @@ import { ToastContext } from '@/app/components/base/toast' import AppIcon from '@/app/components/base/app-icon' import Avatar from '@/app/components/base/avatar' import { IS_CE_EDITION } from '@/config' +import Input from '@/app/components/base/input' const titleClassName = ` text-sm font-medium text-gray-900 @@ -31,6 +32,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ export default function AccountPage() { const { t } = useTranslation() + const { systemFeatures } = useAppContext() const { mutateUserProfile, userProfile, apps } = useAppContext() const { notify } = useContext(ToastContext) const [editNameModalVisible, setEditNameModalVisible] = useState(false) @@ -41,6 +43,9 @@ export default function AccountPage() { const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) + const [showCurrentPassword, setShowCurrentPassword] = useState(false) + const [showPassword, setShowPassword] = useState(false) + const [showConfirmPassword, setShowConfirmPassword] = useState(false) const handleEditName = () => { setEditNameModalVisible(true) @@ -158,8 +163,8 @@ export default function AccountPage() { { - IS_CE_EDITION && ( -
+ systemFeatures.enable_email_password_login && ( +
{t('common.account.password')}
{t('common.account.passwordTip')}
@@ -191,8 +196,7 @@ export default function AccountPage() { >
{t('common.account.editName')}
{t('common.account.name')}
- setEditName(e.target.value)} /> @@ -223,30 +227,61 @@ export default function AccountPage() { {userProfile.is_password_set && ( <>
{t('common.account.currentPassword')}
- setCurrentPassword(e.target.value)} - /> +
+ setCurrentPassword(e.target.value)} + /> + +
+ +
+
)}
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
- setPassword(e.target.value)} - /> +
+ setPassword(e.target.value)} + /> +
+ +
+
{t('common.account.confirmPassword')}
- setConfirmPassword(e.target.value)} - /> +
+ setConfirmPassword(e.target.value)} + /> +
+ +
+
)} - {checkRes && checkRes.is_valid && !showSuccess && ( -
-
-
-
-

- {`${t('login.join')} ${checkRes.workspace_name}`} -

-

- {`${t('login.joinTipStart')} ${checkRes.workspace_name} ${t('login.joinTipEnd')}`} -

-
- -
-
- {/* username */} -
- -
- setName(e.target.value)} - placeholder={t('login.namePlaceholder') || ''} - className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'} - tabIndex={1} - /> -
-
- {/* password */} -
- -
- setPassword(e.target.value)} - placeholder={t('login.passwordPlaceholder') || ''} - className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'} - tabIndex={2} - /> -
-
{t('login.error.passwordInvalid')}
-
- {/* language */} -
- -
- item.supported)} - onSelect={(item) => { - setLanguage(item.value as string) - }} - /> -
-
- {/* timezone */} -
- -
- { - setTimezone(item.value as string) - }} - /> -
-
-
- -
-
- {t('login.license.tip')} -   - {t('login.license.link')} -
-
-
-
- )} - {checkRes && checkRes.is_valid && showSuccess && ( -
-
-
- -
-

- {`${t('login.activatedTipStart')} ${checkRes.workspace_name} ${t('login.activatedTipEnd')}`} -

-
- -
- )}
) } diff --git a/web/app/components/base/icons/assets/public/common/lock.svg b/web/app/components/base/icons/assets/public/common/lock.svg new file mode 100644 index 0000000000..a6987846f7 --- /dev/null +++ b/web/app/components/base/icons/assets/public/common/lock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/header/account-setting/account-page/index.tsx b/web/app/components/header/account-setting/account-page/index.tsx index eecd275b35..7b400dd50d 100644 --- a/web/app/components/header/account-setting/account-page/index.tsx +++ b/web/app/components/header/account-setting/account-page/index.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' +import { useContext, useContextSelector } from 'use-context-selector' import Collapse from '../collapse' import type { IItem } from '../collapse' import s from './index.module.css' @@ -11,7 +11,7 @@ import Modal from '@/app/components/base/modal' import Confirm from '@/app/components/base/confirm' import Button from '@/app/components/base/button' import { updateUserProfile } from '@/service/common' -import { useAppContext } from '@/context/app-context' +import AppContext, { useAppContext } from '@/context/app-context' import { ToastContext } from '@/app/components/base/toast' import AppIcon from '@/app/components/base/app-icon' import Avatar from '@/app/components/base/avatar' @@ -42,6 +42,7 @@ export default function AccountPage() { const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) + const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) const handleEditName = () => { setEditNameModalVisible(true) @@ -144,7 +145,7 @@ export default function AccountPage() {
{t('common.account.email')}
{userProfile.email}
- {IS_CE_EDITION && ( + {systemFeatures.enable_email_password_login && (
{t('common.account.password')}
{t('common.account.passwordTip')}
diff --git a/web/app/components/signin/countdown.tsx b/web/app/components/signin/countdown.tsx new file mode 100644 index 0000000000..6282480d10 --- /dev/null +++ b/web/app/components/signin/countdown.tsx @@ -0,0 +1,41 @@ +'use client' +import { useCountDown } from 'ahooks' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +export const COUNT_DOWN_TIME_MS = 59000 +export const COUNT_DOWN_KEY = 'leftTime' + +type CountdownProps = { + onResend?: () => void +} + +export default function Countdown({ onResend }: CountdownProps) { + const { t } = useTranslation() + const [leftTime, setLeftTime] = useState(Number(localStorage.getItem(COUNT_DOWN_KEY) || COUNT_DOWN_TIME_MS)) + const [time] = useCountDown({ + leftTime, + onEnd: () => { + setLeftTime(0) + localStorage.removeItem(COUNT_DOWN_KEY) + }, + }) + + const resend = async function () { + setLeftTime(COUNT_DOWN_TIME_MS) + localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) + onResend?.() + } + + useEffect(() => { + localStorage.setItem(COUNT_DOWN_KEY, `${time}`) + }, [time]) + + return

+ {t('login.checkCode.didNotReceiveCode')} + {time > 0 && {Math.round(time / 1000)}s} + { + time <= 0 && {t('login.checkCode.resend')} + } +

+} diff --git a/web/app/components/swr-initor.tsx b/web/app/components/swr-initor.tsx index ce126512fa..89141359d6 100644 --- a/web/app/components/swr-initor.tsx +++ b/web/app/components/swr-initor.tsx @@ -1,10 +1,11 @@ 'use client' import { SWRConfig } from 'swr' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import type { ReactNode } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import useRefreshToken from '@/hooks/use-refresh-token' +import { fetchSetupStatus } from '@/service/common' type SwrInitorProps = { children: ReactNode @@ -21,27 +22,60 @@ const SwrInitor = ({ const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token') const [init, setInit] = useState(false) - useEffect(() => { - if (!(consoleToken || refreshToken || consoleTokenFromLocalStorage || refreshTokenFromLocalStorage)) { - router.replace('/signin') - return + const isSetupFinished = useCallback(async () => { + try { + if (localStorage.getItem('setup_status') === 'finished') + return true + const setUpStatus = await fetchSetupStatus() + if (setUpStatus.step !== 'finished') { + localStorage.removeItem('setup_status') + return false + } + localStorage.setItem('setup_status', 'finished') + return true } - if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage) - getNewAccessToken() - - if (consoleToken && refreshToken) { - localStorage.setItem('console_token', consoleToken) - localStorage.setItem('refresh_token', refreshToken) - getNewAccessToken().then(() => { - router.replace('/apps', { forceOptimisticNavigation: false } as any) - }).catch(() => { - router.replace('/signin') - }) + catch (error) { + console.error(error) + return false } - - setInit(true) }, []) + const setRefreshToken = useCallback(async () => { + try { + if (!(consoleToken || refreshToken || consoleTokenFromLocalStorage || refreshTokenFromLocalStorage)) + return Promise.reject(new Error('No token found')) + + if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage) + await getNewAccessToken() + + if (consoleToken && refreshToken) { + localStorage.setItem('console_token', consoleToken) + localStorage.setItem('refresh_token', refreshToken) + await getNewAccessToken() + } + } + catch (error) { + return Promise.reject(error) + } + }, [consoleToken, refreshToken, consoleTokenFromLocalStorage, refreshTokenFromLocalStorage, getNewAccessToken]) + + useEffect(() => { + (async () => { + try { + const isFinished = await isSetupFinished() + if (!isFinished) { + router.replace('/install') + return + } + await setRefreshToken() + setInit(true) + } + catch (error) { + router.replace('/signin') + } + })() + }, [isSetupFinished, setRefreshToken, router]) + return init ? ( { -
- setPassword(e.target.value)} - placeholder={t('login.passwordPlaceholder') || ''} - className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'} - /> -
-
{t('login.error.passwordInvalid')}
+ setPassword(e.target.value)} + placeholder={t('login.passwordPlaceholder') || ''} + className='mt-1' + /> +
{t('login.error.passwordInvalid')}
{/* Confirm Password */}
-
- setConfirmPassword(e.target.value)} - placeholder={t('login.confirmPasswordPlaceholder') || ''} - className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'} - /> -
+ setConfirmPassword(e.target.value)} + placeholder={t('login.confirmPasswordPlaceholder') || ''} + className='mt-1' + />
diff --git a/web/app/forgot-password/ForgotPasswordForm.tsx b/web/app/forgot-password/ForgotPasswordForm.tsx index 6fd69a3638..df744924da 100644 --- a/web/app/forgot-password/ForgotPasswordForm.tsx +++ b/web/app/forgot-password/ForgotPasswordForm.tsx @@ -8,6 +8,7 @@ import { useForm } from 'react-hook-form' import { z } from 'zod' import { zodResolver } from '@hookform/resolvers/zod' import Loading from '../components/base/loading' +import Input from '../components/base/input' import Button from '@/app/components/base/button' import { @@ -78,7 +79,7 @@ const ForgotPasswordForm = () => { return ( loading - ? + ? : <>

@@ -98,10 +99,9 @@ const ForgotPasswordForm = () => { {t('login.email')}
- {errors.email && {t(`${errors.email?.message}`)}}
diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index 0db88c8e25..abf377e389 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -65,6 +65,7 @@ const InstallForm = () => { useEffect(() => { fetchSetupStatus().then((res: SetupStatusResponse) => { if (res.step === 'finished') { + localStorage.setItem('setup_status', 'finished') window.location.href = '/signin' } else { @@ -153,7 +154,7 @@ const InstallForm = () => {

-
+
{t('login.license.tip')}   { + try { + if (!code.trim()) { + Toast.notify({ + type: 'error', + message: t('login.checkCode.emptyCode'), + }) + return + } + if (!/\d{6}/.test(code)) { + Toast.notify({ + type: 'error', + message: t('login.checkCode.invalidCode'), + }) + return + } + setIsLoading(true) + const ret = await verifyResetPasswordCode({ email, code, token }) + ret.is_valid && router.push(`/reset-password/set-password?${searchParams.toString()}`) + } + catch (error) { console.error(error) } + finally { + setIsLoading(false) + } + } + + const resendCode = async () => { + try { + const res = await sendResetPasswordCode(email, locale) + if (res.result === 'success') { + const params = new URLSearchParams(searchParams) + params.set('token', encodeURIComponent(res.data)) + router.replace(`/reset-password/check-code?${params.toString()}`) + } + } + catch (error) { console.error(error) } + } + + return
+
+ +
+
+

{t('login.checkCode.checkYourEmail')}

+

+ +
+ {t('login.checkCode.validTime')} +

+
+ +
+ + + setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} /> + + + +
+
+
+
router.back()} className='flex items-center justify-center h-9 text-text-tertiary cursor-pointer'> +
+ +
+ {t('login.back')} +
+
+} diff --git a/web/app/reset-password/layout.tsx b/web/app/reset-password/layout.tsx new file mode 100644 index 0000000000..16d8642ed2 --- /dev/null +++ b/web/app/reset-password/layout.tsx @@ -0,0 +1,39 @@ +import Header from '../signin/_header' +import style from '../signin/page.module.css' + +import cn from '@/utils/classnames' + +export default async function SignInLayout({ children }: any) { + return <> +
+
+
+
+
+ {children} +
+
+
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved. +
+
+
+ +} diff --git a/web/app/reset-password/page.tsx b/web/app/reset-password/page.tsx new file mode 100644 index 0000000000..65f1db3fb5 --- /dev/null +++ b/web/app/reset-password/page.tsx @@ -0,0 +1,101 @@ +'use client' +import Link from 'next/link' +import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { useContext } from 'use-context-selector' +import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '../components/signin/countdown' +import { emailRegex } from '@/config' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Toast from '@/app/components/base/toast' +import { sendResetPasswordCode } from '@/service/common' +import I18NContext from '@/context/i18n' + +export default function CheckCode() { + const { t } = useTranslation() + const searchParams = useSearchParams() + const router = useRouter() + const [email, setEmail] = useState('') + const [loading, setIsLoading] = useState(false) + const { locale } = useContext(I18NContext) + + const handleGetEMailVerificationCode = async () => { + try { + if (!email) { + Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) + return + } + + if (!emailRegex.test(email)) { + Toast.notify({ + type: 'error', + message: t('login.error.emailInValid'), + }) + return + } + setIsLoading(true) + const res = await sendResetPasswordCode(email, locale) + if (res.result === 'success') { + localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) + const params = new URLSearchParams(searchParams) + params.set('token', encodeURIComponent(res.data)) + params.set('email', encodeURIComponent(email)) + router.push(`/reset-password/check-code?${params.toString()}`) + } + else if (res.code === 'account_not_found') { + Toast.notify({ + type: 'error', + message: t('login.error.registrationNotAllowed'), + }) + } + else { + Toast.notify({ + type: 'error', + message: res.data, + }) + } + } + catch (error) { + console.error(error) + } + finally { + setIsLoading(false) + } + } + + return
+
+ +
+
+

{t('login.resetPassword')}

+

+ {t('login.resetPasswordDesc')} +

+
+ +
{ }}> + +
+ +
+ setEmail(e.target.value)} /> +
+
+ +
+
+
+
+
+
+ +
+ +
+ {t('login.backToLogin')} + +
+} diff --git a/web/app/reset-password/set-password/page.tsx b/web/app/reset-password/set-password/page.tsx new file mode 100644 index 0000000000..7948c59a9a --- /dev/null +++ b/web/app/reset-password/set-password/page.tsx @@ -0,0 +1,193 @@ +'use client' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useRouter, useSearchParams } from 'next/navigation' +import cn from 'classnames' +import { RiCheckboxCircleFill } from '@remixicon/react' +import { useCountDown } from 'ahooks' +import Button from '@/app/components/base/button' +import { changePasswordWithToken } from '@/service/common' +import Toast from '@/app/components/base/toast' +import Input from '@/app/components/base/input' + +const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ + +const ChangePasswordForm = () => { + const { t } = useTranslation() + const router = useRouter() + const searchParams = useSearchParams() + const token = decodeURIComponent(searchParams.get('token') || '') + + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [showSuccess, setShowSuccess] = useState(false) + const [showPassword, setShowPassword] = useState(false) + const [showConfirmPassword, setShowConfirmPassword] = useState(false) + + const showErrorMessage = useCallback((message: string) => { + Toast.notify({ + type: 'error', + message, + }) + }, []) + + const getSignInUrl = () => { + if (searchParams.has('invite_token')) { + const params = new URLSearchParams() + params.set('token', searchParams.get('invite_token') as string) + return `/activate?${params.toString()}` + } + return '/signin' + } + + const AUTO_REDIRECT_TIME = 5000 + const [leftTime, setLeftTime] = useState(undefined) + const [countdown] = useCountDown({ + leftTime, + onEnd: () => { + router.replace(getSignInUrl()) + }, + }) + + const valid = useCallback(() => { + if (!password.trim()) { + showErrorMessage(t('login.error.passwordEmpty')) + return false + } + if (!validPassword.test(password)) { + showErrorMessage(t('login.error.passwordInvalid')) + return false + } + if (password !== confirmPassword) { + showErrorMessage(t('common.account.notEqual')) + return false + } + return true + }, [password, confirmPassword, showErrorMessage, t]) + + const handleChangePassword = useCallback(async () => { + if (!valid()) + return + try { + await changePasswordWithToken({ + url: '/forgot-password/resets', + body: { + token, + new_password: password, + password_confirm: confirmPassword, + }, + }) + setShowSuccess(true) + setLeftTime(AUTO_REDIRECT_TIME) + } + catch (error) { + console.error(error) + } + }, [password, token, valid, confirmPassword]) + + return ( +
+ {!showSuccess && ( +
+
+

+ {t('login.changePassword')} +

+

+ {t('login.changePasswordTip')} +

+
+ +
+
+ {/* Password */} +
+ +
+ setPassword(e.target.value)} + placeholder={t('login.passwordPlaceholder') || ''} + /> + +
+ +
+
+
{t('login.error.passwordInvalid')}
+
+ {/* Confirm Password */} +
+ +
+ setConfirmPassword(e.target.value)} + placeholder={t('login.confirmPasswordPlaceholder') || ''} + /> +
+ +
+
+
+
+ +
+
+
+
+ )} + {showSuccess && ( +
+
+
+ +
+

+ {t('login.passwordChangedTip')} +

+
+
+ +
+
+ )} +
+ ) +} + +export default ChangePasswordForm diff --git a/web/app/signin/check-code/page.tsx b/web/app/signin/check-code/page.tsx new file mode 100644 index 0000000000..4767308f72 --- /dev/null +++ b/web/app/signin/check-code/page.tsx @@ -0,0 +1,96 @@ +'use client' +import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { useContext } from 'use-context-selector' +import Countdown from '@/app/components/signin/countdown' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Toast from '@/app/components/base/toast' +import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common' +import I18NContext from '@/context/i18n' + +export default function CheckCode() { + const { t } = useTranslation() + const router = useRouter() + const searchParams = useSearchParams() + const email = decodeURIComponent(searchParams.get('email') as string) + const token = decodeURIComponent(searchParams.get('token') as string) + const invite_token = decodeURIComponent(searchParams.get('invite_token') || '') + const [code, setVerifyCode] = useState('') + const [loading, setIsLoading] = useState(false) + const { locale } = useContext(I18NContext) + + const verify = async () => { + try { + if (!code.trim()) { + Toast.notify({ + type: 'error', + message: t('login.checkCode.emptyCode'), + }) + return + } + if (!/\d{6}/.test(code)) { + Toast.notify({ + type: 'error', + message: t('login.checkCode.invalidCode'), + }) + return + } + setIsLoading(true) + const ret = await emailLoginWithCode({ email, code, token }) + if (ret.result === 'success') { + localStorage.setItem('console_token', ret.data.access_token) + localStorage.setItem('refresh_token', ret.data.refresh_token) + router.replace(invite_token ? `/signin/invite-settings?${searchParams.toString()}` : '/apps') + } + } + catch (error) { console.error(error) } + finally { + setIsLoading(false) + } + } + + const resendCode = async () => { + try { + const ret = await sendEMailLoginCode(email, locale) + if (ret.result === 'success') { + const params = new URLSearchParams(searchParams) + params.set('token', encodeURIComponent(ret.data)) + router.replace(`/signin/check-code?${params.toString()}`) + } + } + catch (error) { console.error(error) } + } + + return
+
+ +
+
+

{t('login.checkCode.checkYourEmail')}

+

+ +
+ {t('login.checkCode.validTime')} +

+
+ +
+ + setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} /> + + + +
+
+
+
router.back()} className='flex items-center justify-center h-9 text-text-tertiary cursor-pointer'> +
+ +
+ {t('login.back')} +
+
+} diff --git a/web/app/signin/components/mail-and-code-auth.tsx b/web/app/signin/components/mail-and-code-auth.tsx new file mode 100644 index 0000000000..7225b094d4 --- /dev/null +++ b/web/app/signin/components/mail-and-code-auth.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useRouter, useSearchParams } from 'next/navigation' +import { useContext } from 'use-context-selector' +import Input from '@/app/components/base/input' +import Button from '@/app/components/base/button' +import { emailRegex } from '@/config' +import Toast from '@/app/components/base/toast' +import { sendEMailLoginCode } from '@/service/common' +import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' +import I18NContext from '@/context/i18n' + +type MailAndCodeAuthProps = { + isInvite: boolean +} + +export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) { + const { t } = useTranslation() + const router = useRouter() + const searchParams = useSearchParams() + const emailFromLink = decodeURIComponent(searchParams.get('email') || '') + const [email, setEmail] = useState(emailFromLink) + const [loading, setIsLoading] = useState(false) + const { locale } = useContext(I18NContext) + + const handleGetEMailVerificationCode = async () => { + try { + if (!email) { + Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) + return + } + + if (!emailRegex.test(email)) { + Toast.notify({ + type: 'error', + message: t('login.error.emailInValid'), + }) + return + } + setIsLoading(true) + const ret = await sendEMailLoginCode(email, locale) + if (ret.result === 'success') { + localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) + const params = new URLSearchParams(searchParams) + params.set('email', encodeURIComponent(email)) + params.set('token', encodeURIComponent(ret.data)) + router.push(`/signin/check-code?${params.toString()}`) + } + } + catch (error) { + console.error(error) + } + finally { + setIsLoading(false) + } + } + + return (
{ }}> + +
+ +
+ setEmail(e.target.value)} /> +
+
+ +
+
+
+ ) +} diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx new file mode 100644 index 0000000000..210c877bb7 --- /dev/null +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -0,0 +1,167 @@ +import Link from 'next/link' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useRouter, useSearchParams } from 'next/navigation' +import { useContext } from 'use-context-selector' +import Button from '@/app/components/base/button' +import Toast from '@/app/components/base/toast' +import { emailRegex } from '@/config' +import { login } from '@/service/common' +import Input from '@/app/components/base/input' +import I18NContext from '@/context/i18n' + +type MailAndPasswordAuthProps = { + isInvite: boolean + allowRegistration: boolean +} + +const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ + +export default function MailAndPasswordAuth({ isInvite, allowRegistration }: MailAndPasswordAuthProps) { + const { t } = useTranslation() + const { locale } = useContext(I18NContext) + const router = useRouter() + const searchParams = useSearchParams() + const [showPassword, setShowPassword] = useState(false) + const emailFromLink = decodeURIComponent(searchParams.get('email') || '') + const [email, setEmail] = useState(emailFromLink) + const [password, setPassword] = useState('') + + const [isLoading, setIsLoading] = useState(false) + const handleEmailPasswordLogin = async () => { + if (!email) { + Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) + return + } + if (!emailRegex.test(email)) { + Toast.notify({ + type: 'error', + message: t('login.error.emailInValid'), + }) + return + } + if (!password?.trim()) { + Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') }) + return + } + if (!passwordRegex.test(password)) { + Toast.notify({ + type: 'error', + message: t('login.error.passwordInvalid'), + }) + return + } + try { + setIsLoading(true) + const loginData: Record = { + email, + password, + language: locale, + remember_me: true, + } + if (isInvite) + loginData.invite_token = decodeURIComponent(searchParams.get('invite_token') as string) + const res = await login({ + url: '/login', + body: loginData, + }) + if (res.result === 'success') { + if (isInvite) { + router.replace(`/signin/invite-settings?${searchParams.toString()}`) + } + else { + localStorage.setItem('console_token', res.data.access_token) + localStorage.setItem('refresh_token', res.data.refresh_token) + router.replace('/apps') + } + } + else if (res.code === 'account_not_found') { + if (allowRegistration) { + const params = new URLSearchParams() + params.append('email', encodeURIComponent(email)) + params.append('token', encodeURIComponent(res.data)) + router.replace(`/reset-password/check-code?${params.toString()}`) + } + else { + Toast.notify({ + type: 'error', + message: t('login.error.registrationNotAllowed'), + }) + } + } + else { + Toast.notify({ + type: 'error', + message: res.data, + }) + } + } + + finally { + setIsLoading(false) + } + } + + return
{ }}> +
+ +
+ setEmail(e.target.value)} + disabled={isInvite} + id="email" + type="email" + autoComplete="email" + placeholder={t('login.emailPlaceholder') || ''} + tabIndex={1} + /> +
+
+ +
+ +
+ setPassword(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') + handleEmailPasswordLogin() + }} + type={showPassword ? 'text' : 'password'} + autoComplete="current-password" + placeholder={t('login.passwordPlaceholder') || ''} + tabIndex={2} + /> +
+ +
+
+
+ +
+ +
+
+} diff --git a/web/app/signin/components/social-auth.tsx b/web/app/signin/components/social-auth.tsx new file mode 100644 index 0000000000..39d7ceaa40 --- /dev/null +++ b/web/app/signin/components/social-auth.tsx @@ -0,0 +1,62 @@ +import { useTranslation } from 'react-i18next' +import { useSearchParams } from 'next/navigation' +import style from '../page.module.css' +import Button from '@/app/components/base/button' +import { apiPrefix } from '@/config' +import classNames from '@/utils/classnames' +import { getPurifyHref } from '@/utils' + +type SocialAuthProps = { + disabled?: boolean +} + +export default function SocialAuth(props: SocialAuthProps) { + const { t } = useTranslation() + const searchParams = useSearchParams() + + const getOAuthLink = (href: string) => { + const url = getPurifyHref(`${apiPrefix}${href}`) + if (searchParams.has('invite_token')) + return `${url}?${searchParams.toString()}` + + return url + } + return <> + + + +} diff --git a/web/app/signin/components/sso-auth.tsx b/web/app/signin/components/sso-auth.tsx new file mode 100644 index 0000000000..fb303b93e2 --- /dev/null +++ b/web/app/signin/components/sso-auth.tsx @@ -0,0 +1,73 @@ +'use client' +import { useRouter, useSearchParams } from 'next/navigation' +import type { FC } from 'react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' +import Toast from '@/app/components/base/toast' +import { getUserOAuth2SSOUrl, getUserOIDCSSOUrl, getUserSAMLSSOUrl } from '@/service/sso' +import Button from '@/app/components/base/button' +import { SSOProtocol } from '@/types/feature' + +type SSOAuthProps = { + protocol: SSOProtocol | '' +} + +const SSOAuth: FC = ({ + protocol, +}) => { + const router = useRouter() + const { t } = useTranslation() + const searchParams = useSearchParams() + const invite_token = decodeURIComponent(searchParams.get('invite_token') || '') + + const [isLoading, setIsLoading] = useState(false) + + const handleSSOLogin = () => { + setIsLoading(true) + if (protocol === SSOProtocol.SAML) { + getUserSAMLSSOUrl(invite_token).then((res) => { + router.push(res.url) + }).finally(() => { + setIsLoading(false) + }) + } + else if (protocol === SSOProtocol.OIDC) { + getUserOIDCSSOUrl(invite_token).then((res) => { + document.cookie = `user-oidc-state=${res.state}` + router.push(res.url) + }).finally(() => { + setIsLoading(false) + }) + } + else if (protocol === SSOProtocol.OAuth2) { + getUserOAuth2SSOUrl(invite_token).then((res) => { + document.cookie = `user-oauth2-state=${res.state}` + router.push(res.url) + }).finally(() => { + setIsLoading(false) + }) + } + else { + Toast.notify({ + type: 'error', + message: 'invalid SSO protocol', + }) + setIsLoading(false) + } + } + + return ( + + ) +} + +export default SSOAuth diff --git a/web/app/signin/forms.tsx b/web/app/signin/forms.tsx deleted file mode 100644 index 70a34c26fa..0000000000 --- a/web/app/signin/forms.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client' -import React from 'react' -import { useSearchParams } from 'next/navigation' - -import NormalForm from './normalForm' -import OneMoreStep from './oneMoreStep' -import cn from '@/utils/classnames' - -const Forms = () => { - const searchParams = useSearchParams() - const step = searchParams.get('step') - - const getForm = () => { - switch (step) { - case 'next': - return - default: - return - } - } - return
-
- {getForm()} -
-
-} - -export default Forms diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx new file mode 100644 index 0000000000..2138399ec3 --- /dev/null +++ b/web/app/signin/invite-settings/page.tsx @@ -0,0 +1,154 @@ +'use client' +import { useTranslation } from 'react-i18next' +import { useCallback, useState } from 'react' +import Link from 'next/link' +import { useContext } from 'use-context-selector' +import { useRouter, useSearchParams } from 'next/navigation' +import useSWR from 'swr' +import { RiAccountCircleLine } from '@remixicon/react' +import Input from '@/app/components/base/input' +import { SimpleSelect } from '@/app/components/base/select' +import Button from '@/app/components/base/button' +import { timezones } from '@/utils/timezone' +import { LanguagesSupported, languages } from '@/i18n/language' +import I18n from '@/context/i18n' +import { activateMember, invitationCheck } from '@/service/common' +import Loading from '@/app/components/base/loading' +import Toast from '@/app/components/base/toast' + +export default function InviteSettingsPage() { + const { t } = useTranslation() + const router = useRouter() + const searchParams = useSearchParams() + const token = decodeURIComponent(searchParams.get('invite_token') as string) + const { locale, setLocaleOnClient } = useContext(I18n) + const [name, setName] = useState('') + const [language, setLanguage] = useState(LanguagesSupported[0]) + const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Los_Angeles') + + const checkParams = { + url: '/activate/check', + params: { + token, + }, + } + const { data: checkRes, mutate: recheck } = useSWR(checkParams, invitationCheck, { + revalidateOnFocus: false, + }) + + const handleActivate = useCallback(async () => { + try { + if (!name) { + Toast.notify({ type: 'error', message: t('login.enterYourName') }) + return + } + const res = await activateMember({ + url: '/activate', + body: { + token, + name, + interface_language: language, + timezone, + }, + }) + if (res.result === 'success') { + localStorage.setItem('console_token', res.data.access_token) + localStorage.setItem('refresh_token', res.data.refresh_token) + setLocaleOnClient(language, false) + router.replace('/apps') + } + } + catch { + recheck() + } + }, [language, name, recheck, setLocaleOnClient, timezone, token, router, t]) + + if (!checkRes) + return + if (!checkRes.is_valid) { + return
+
+
🤷‍♂️
+

{t('login.invalid')}

+
+ +
+ } + + return
+
+ +
+
+

{t('login.setYourAccount')}

+
+
+ +
+ +
+ setName(e.target.value)} + placeholder={t('login.namePlaceholder') || ''} + /> +
+
+
+ +
+ item.supported)} + onSelect={(item) => { + setLanguage(item.value as string) + }} + /> +
+
+ {/* timezone */} +
+ +
+ { + setTimezone(item.value as string) + }} + /> +
+
+
+ +
+
+
+ {t('login.license.tip')} +   + {t('login.license.link')} +
+
+} diff --git a/web/app/signin/layout.tsx b/web/app/signin/layout.tsx new file mode 100644 index 0000000000..342876bc53 --- /dev/null +++ b/web/app/signin/layout.tsx @@ -0,0 +1,54 @@ +import Script from 'next/script' +import Header from './_header' +import style from './page.module.css' + +import cn from '@/utils/classnames' +import { IS_CE_EDITION } from '@/config' + +export default async function SignInLayout({ children }: any) { + return <> + {!IS_CE_EDITION && ( + <> + + + + )} + +
+
+
+
+
+ {children} +
+
+
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved. +
+
+
+ +} diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index 113ed64b57..c0f2d89b37 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -1,299 +1,170 @@ -'use client' -import React, { useEffect, useReducer, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useRouter } from 'next/navigation' -import useSWR from 'swr' import Link from 'next/link' -import Toast from '../components/base/toast' -import style from './page.module.css' -import classNames from '@/utils/classnames' -import { IS_CE_EDITION, SUPPORT_MAIL_LOGIN, apiPrefix, emailRegex } from '@/config' -import Button from '@/app/components/base/button' -import { login, oauth } from '@/service/common' -import { getPurifyHref } from '@/utils' +import { useRouter, useSearchParams } from 'next/navigation' +import { RiDoorLockLine } from '@remixicon/react' +import Loading from '../components/base/loading' +import MailAndCodeAuth from './components/mail-and-code-auth' +import MailAndPasswordAuth from './components/mail-and-password-auth' +import SocialAuth from './components/social-auth' +import SSOAuth from './components/sso-auth' +import cn from '@/utils/classnames' +import { getSystemFeatures, invitationCheck } from '@/service/common' +import { defaultSystemFeatures } from '@/types/feature' +import Toast from '@/app/components/base/toast' import useRefreshToken from '@/hooks/use-refresh-token' - -type IState = { - formValid: boolean - github: boolean - google: boolean -} - -type IAction = { - type: 'login' | 'login_failed' | 'github_login' | 'github_login_failed' | 'google_login' | 'google_login_failed' -} - -function reducer(state: IState, action: IAction) { - switch (action.type) { - case 'login': - return { - ...state, - formValid: true, - } - case 'login_failed': - return { - ...state, - formValid: true, - } - case 'github_login': - return { - ...state, - github: true, - } - case 'github_login_failed': - return { - ...state, - github: false, - } - case 'google_login': - return { - ...state, - google: true, - } - case 'google_login_failed': - return { - ...state, - google: false, - } - default: - throw new Error('Unknown action.') - } -} +import { IS_CE_EDITION } from '@/config' const NormalForm = () => { - const { t } = useTranslation() const { getNewAccessToken } = useRefreshToken() - const useEmailLogin = IS_CE_EDITION || SUPPORT_MAIL_LOGIN - + const { t } = useTranslation() const router = useRouter() + const searchParams = useSearchParams() + const consoleToken = decodeURIComponent(searchParams.get('access_token') || '') + const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '') + const message = decodeURIComponent(searchParams.get('message') || '') + const invite_token = decodeURIComponent(searchParams.get('invite_token') || '') + const [isLoading, setIsLoading] = useState(true) + const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures) + const [authType, updateAuthType] = useState<'code' | 'password'>('password') + const [showORLine, setShowORLine] = useState(false) + const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) + const [workspaceName, setWorkSpaceName] = useState('') - const [state, dispatch] = useReducer(reducer, { - formValid: false, - github: false, - google: false, - }) + const isInviteLink = Boolean(invite_token && invite_token !== 'null') - const [showPassword, setShowPassword] = useState(false) - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - - const [isLoading, setIsLoading] = useState(false) - const handleEmailPasswordLogin = async () => { - if (!emailRegex.test(email)) { - Toast.notify({ - type: 'error', - message: t('login.error.emailInValid'), - }) - return - } + const init = useCallback(async () => { try { - setIsLoading(true) - const res = await login({ - url: '/login', - body: { - email, - password, - remember_me: true, - }, - }) - if (res.result === 'success') { - localStorage.setItem('console_token', res.data.access_token) - localStorage.setItem('refresh_token', res.data.refresh_token) + if (consoleToken && refreshToken) { + localStorage.setItem('console_token', consoleToken) + localStorage.setItem('refresh_token', refreshToken) getNewAccessToken() router.replace('/apps') + return } - else { + + if (message) { Toast.notify({ type: 'error', - message: res.data, + message, }) } + const features = await getSystemFeatures() + const allFeatures = { ...defaultSystemFeatures, ...features } + setSystemFeatures(allFeatures) + setAllMethodsAreDisabled(!allFeatures.enable_social_oauth_login && !allFeatures.enable_email_code_login && !allFeatures.enable_email_password_login && !allFeatures.sso_enforced_for_signin) + setShowORLine((allFeatures.enable_social_oauth_login || allFeatures.sso_enforced_for_signin) && (allFeatures.enable_email_code_login || allFeatures.enable_email_password_login)) + updateAuthType(allFeatures.enable_email_password_login ? 'password' : 'code') + if (isInviteLink) { + const checkRes = await invitationCheck({ + url: '/activate/check', + params: { + token: invite_token, + }, + }) + setWorkSpaceName(checkRes?.data?.workspace_name || '') + } } - finally { - setIsLoading(false) + catch (error) { + console.error(error) + setAllMethodsAreDisabled(true) + setSystemFeatures(defaultSystemFeatures) } + finally { setIsLoading(false) } + }, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, getNewAccessToken]) + useEffect(() => { + init() + }, [init]) + if (isLoading || consoleToken) { + return
+ +
} - const { data: github, error: github_error } = useSWR(state.github - ? ({ - url: '/oauth/login/github', - // params: { - // provider: 'github', - // }, - }) - : null, oauth) - - const { data: google, error: google_error } = useSWR(state.google - ? ({ - url: '/oauth/login/google', - // params: { - // provider: 'google', - // }, - }) - : null, oauth) - - useEffect(() => { - if (github_error !== undefined) - dispatch({ type: 'github_login_failed' }) - if (github) - window.location.href = github.redirect_url - }, [github, github_error]) - - useEffect(() => { - if (google_error !== undefined) - dispatch({ type: 'google_login_failed' }) - if (google) - window.location.href = google.redirect_url - }, [google, google_error]) - return ( <> -
-

{t('login.pageTitle')}

-

{t('login.welcome')}

-
-
-
- {!useEmailLogin && ( -
- - + {isInviteLink + ?
+

{t('login.join')}{workspaceName}

+

{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}

+
+ :
+

{t('login.pageTitle')}

+

{t('login.welcome')}

+
} +
+
+ {systemFeatures.enable_social_oauth_login && } + {systemFeatures.sso_enforced_for_signin &&
+ +
} +
+ + {showORLine &&
+ - )} - +
+ {t('login.or')} +
+
} { - useEmailLogin && <> - {/*
- */} - -
{ }}> -
- -
- setEmail(e.target.value)} - id="email" - type="email" - autoComplete="email" - placeholder={t('login.emailPlaceholder') || ''} - className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'} - tabIndex={1} - /> -
-
- -
- -
- setPassword(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') - handleEmailPasswordLogin() - }} - type={showPassword ? 'text' : 'password'} - autoComplete="current-password" - placeholder={t('login.passwordPlaceholder') || ''} - className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'} - tabIndex={2} - /> -
- -
-
-
- -
- -
-
+ (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && <> + {systemFeatures.enable_email_code_login && authType === 'code' && <> + + {systemFeatures.enable_email_password_login &&
{ updateAuthType('password') }}> + {t('login.usePassword')} +
} + } + {systemFeatures.enable_email_password_login && authType === 'password' && <> + + {systemFeatures.enable_email_code_login &&
{ updateAuthType('code') }}> + {t('login.useVerificationCode')} +
} + } } - {/* agree to our Terms and Privacy Policy. */} -
+ {allMethodsAreDisabled && <> +
+
+ +
+

{t('login.noLoginMethod')}

+

{t('login.noLoginMethodTip')}

+
+
+ +
+ } +
{t('login.tosDesc')}   {t('login.tos')}  &  {t('login.pp')}
- - {IS_CE_EDITION &&
+ {IS_CE_EDITION &&
{t('login.goToInit')}   {t('login.setAdminAccount')}
} diff --git a/web/app/signin/oneMoreStep.tsx b/web/app/signin/oneMoreStep.tsx index a4324517a5..8554b364c0 100644 --- a/web/app/signin/oneMoreStep.tsx +++ b/web/app/signin/oneMoreStep.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useReducer } from 'react' import { useTranslation } from 'react-i18next' import Link from 'next/link' import useSWR from 'swr' -import { useRouter } from 'next/navigation' -// import { useContext } from 'use-context-selector' +import { useRouter, useSearchParams } from 'next/navigation' +import Input from '../components/base/input' import Button from '@/app/components/base/button' import Tooltip from '@/app/components/base/tooltip' import { SimpleSelect } from '@/app/components/base/select' @@ -12,7 +12,6 @@ import { timezones } from '@/utils/timezone' import { LanguagesSupported, languages } from '@/i18n/language' import { oneMoreStep } from '@/service/common' import Toast from '@/app/components/base/toast' -// import I18n from '@/context/i18n' type IState = { formState: 'processing' | 'error' | 'success' | 'initial' @@ -46,11 +45,11 @@ const reducer = (state: IState, action: any) => { const OneMoreStep = () => { const { t } = useTranslation() const router = useRouter() - // const { locale } = useContext(I18n) + const searchParams = useSearchParams() const [state, dispatch] = useReducer(reducer, { formState: 'initial', - invitation_code: '', + invitation_code: searchParams.get('invitation_code') || '', interface_language: 'en-US', timezone: 'Asia/Shanghai', }) @@ -77,36 +76,35 @@ const OneMoreStep = () => { return ( <>
-

{t('login.oneMoreStep')}

-

{t('login.createSample')}

+

{t('login.oneMoreStep')}

+

{t('login.createSample')}

-
-