Compare commits

..
Author SHA1 Message Date
AkaraChen 5f93e28d75 fix: gen-icon script phantom deps 2024-10-21 10:41:43 +08:00
AkaraChen 6ffedcd8f0 build: switch to pnpm 2024-10-21 10:41:43 +08:00
非法操作andAkaraChen 26358e971d fix: draft run workflow node with image will raise error (#9406) 2024-10-21 10:41:43 +08:00
effd5449bc refactor wenxin rerank (#9486)
Co-authored-by: cuihz <cuihz@knowbox.cn>
2024-10-21 10:41:43 +08:00
kurokoboandAkaraChen a608e23613 fix: ignore all files except for .gitkeep under docker/nginx/ssl by gitignore (#9518) 2024-10-21 10:41:43 +08:00
ice yaoandAkaraChen be122ac974 feat: Add custom username and avatar define in discord tool (#9514) 2024-10-21 10:41:43 +08:00
Ziyu HuangandAkaraChen 1fe585fcdd Resolve 9508 openai compatible rerank (#9511) 2024-10-21 10:41:43 +08:00
zhuhaoandAkaraChen 2d034c57da fix: resolve the error of docker-compose startup when the storage is baidu-obs (#9502) 2024-10-21 10:41:43 +08:00
Chenhe GuandAkaraChen 2c102c9b34 refine wording in license (#9505) 2024-10-21 10:41:43 +08:00
ZvenandAkaraChen 4f3113febb chore: update the description for storage_type (#9492) 2024-10-21 10:41:43 +08:00
ice yaoandAkaraChen dfd85c6d11 feat: Add volcengine tos storage test (#9495) 2024-10-21 10:41:43 +08:00
3f330c0b80 add yuqye(https://www.yuque.com)tools (#8960)
Co-authored-by: 佐井 <chengwu.lcw@alibaba-inc.com>
2024-10-21 10:41:43 +08:00
Tao WangandAkaraChen c379448673 Added Llama 3.2 Vision Models Speech2Text Models for Groq (#9479) 2024-10-21 10:41:43 +08:00
zhuhaoandAkaraChen 05ed049cc8 feat: add yi custom llm intergration (#9482) 2024-10-21 10:41:43 +08:00
zhuhaoandAkaraChen 37d47983a1 fix: resolve the error with the db-pool-stat endpoint (#9478) 2024-10-21 10:41:43 +08:00
132 changed files with 2820 additions and 17404 deletions
+3 -3
View File
@@ -76,16 +76,16 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 20
cache: pnpm
cache: yarn
cache-dependency-path: ./web/package.json
- name: Web dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm install --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Web style check
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm run lint
run: yarn run lint
superlinter:
+3 -3
View File
@@ -32,10 +32,10 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: ''
cache-dependency-path: 'pnpm-lock.yaml'
cache-dependency-path: 'yarn.lock'
- name: Install Dependencies
run: pnpm install
run: yarn install
- name: Test
run: pnpm test
run: yarn test
@@ -38,7 +38,7 @@ jobs:
- name: Install dependencies
if: env.FILES_CHANGED == 'true'
run: pnpm install --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Run npm script
if: env.FILES_CHANGED == 'true'
+3 -3
View File
@@ -34,13 +34,13 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 20
cache: pnpm
cache: yarn
cache-dependency-path: ./web/package.json
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm install --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Run tests
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm test
run: yarn test
+2
View File
@@ -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
+3 -2
View File
@@ -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.
+1 -1
View File
@@ -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
+1
View File
@@ -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
+2 -1
View File
@@ -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",
)
@@ -18,6 +18,7 @@ help:
en_US: https://console.groq.com/
supported_model_types:
- llm
- speech2text
configurate_methods:
- predefined-model
provider_credential_schema:
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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
@@ -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
@@ -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 上限
@@ -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
@@ -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()
@@ -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,
),
],
)
@@ -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
+1
View File
@@ -61,6 +61,7 @@
- vectorizer
- qrcode
- tianditu
- aliyuque
- google_translate
- hap
- json_process
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -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))
@@ -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
@@ -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
@@ -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"))
@@ -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.
@@ -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}")
)
@@ -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.
@@ -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")
)
@@ -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.
@@ -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"))
@@ -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.
@@ -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))
@@ -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.
@@ -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}")
)
@@ -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.
@@ -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"))
@@ -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).
@@ -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}")
)
@@ -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.
@@ -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:
@@ -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
+1 -1
View File
@@ -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)
@@ -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()
@@ -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())
+4
View File
@@ -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}
+7
View File
@@ -0,0 +1,7 @@
/**/node_modules/*
node_modules/
dist/
build/
out/
.next/
+32
View File
@@ -0,0 +1,32 @@
{
"root": true,
"extends": [
"next",
"@antfu",
"plugin:storybook/recommended"
],
"rules": {
"@typescript-eslint/consistent-type-definitions": [
"error",
"type"
],
"@typescript-eslint/no-var-requires": "off",
"no-console": "off",
"indent": "off",
"@typescript-eslint/indent": [
"error",
2,
{
"SwitchCase": 1,
"flatTernaryExpressions": false,
"ignoredNodes": [
"PropertyDefinition[decorators]",
"TSUnionType",
"FunctionExpression[params]:has(Identifier[decorators])"
]
}
],
"react-hooks/exhaustive-deps": "warn",
"react/display-name": "warn"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
import React from 'react'
import type { Preview } from '@storybook/react'
import { withThemeByDataAttribute } from '@storybook/addon-themes'
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import I18nServer from '../app/components/i18n-server'
import '../app/styles/globals.css'
@@ -16,12 +16,12 @@ export const decorators = [
defaultTheme: 'light',
attributeName: 'data-theme',
}),
(Story) => {
Story => {
return <I18nServer>
<Story />
</I18nServer>
}
]
];
const preview: Preview = {
parameters: {
+6 -8
View File
@@ -6,9 +6,6 @@ LABEL maintainer="takatost@gmail.com"
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache tzdata
RUN npm install -g pnpm@9.12.2
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# install packages
@@ -17,12 +14,12 @@ FROM base AS packages
WORKDIR /app/web
COPY package.json .
COPY pnpm-lock.yaml .
COPY yarn.lock .
# if you located in China, you can use taobao registry to speed up
# RUN pnpm install --frozen-lockfile --registry https://registry.npmmirror.com/
# RUN yarn install --frozen-lockfile --registry https://registry.npmmirror.com/
RUN pnpm install --frozen-lockfile
RUN yarn install --frozen-lockfile
# build resources
FROM base AS builder
@@ -30,7 +27,7 @@ WORKDIR /app/web
COPY --from=packages /app/web/ .
COPY . .
RUN pnpm build
RUN yarn build
# production stage
@@ -60,7 +57,8 @@ COPY docker/entrypoint.sh ./entrypoint.sh
# global runtime packages
RUN pnpm add -g pm2 \
RUN yarn global add pm2 \
&& yarn cache clean \
&& mkdir /.pm2 \
&& chown -R 1001:0 /.pm2 /app/web \
&& chmod -R g=u /.pm2 /app/web
+1 -1
View File
@@ -77,7 +77,7 @@ This project uses [Storybook](https://storybook.js.org/) for UI component develo
To start the storybook server, run:
```bash
pnpm storybook
yarn storybook
```
Open [http://localhost:6006](http://localhost:6006) with your browser to see the result.
@@ -1,5 +1,5 @@
import React from 'react'
import type { Locale } from '@/i18n'
import { type Locale } from '@/i18n'
import DevelopMain from '@/app/components/develop'
export type IDevelopProps = {
@@ -8,11 +8,12 @@ import { useBoolean } from 'ahooks'
import {
Cog8ToothIcon,
// CommandLineIcon,
Squares2X2Icon,
// eslint-disable-next-line sort-imports
PuzzlePieceIcon,
DocumentTextIcon,
PaperClipIcon,
PuzzlePieceIcon,
QuestionMarkCircleIcon,
Squares2X2Icon,
} from '@heroicons/react/24/outline'
import {
Cog8ToothIcon as Cog8ToothSolidIcon,
@@ -4,9 +4,12 @@ import { customTool, extensionDallE, modelGPT4, toolNotion } from '@/app/compone
import PluginItem from '@/app/components/plugins/plugin-item'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import ProviderCard from '@/app/components/plugins/provider-card'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { getLocaleOnServer } from '@/i18n/server'
import Badge from '@/app/components/base/badge'
const PluginList = async () => {
const locale = getLocaleOnServer()
const pluginList = [toolNotion, extensionDallE, modelGPT4, customTool]
return (
@@ -63,6 +66,9 @@ const PluginList = async () => {
))}
</div>
</div>
<PluginDetailPanel
locale={locale}
/>
</div>
)
}
@@ -9,7 +9,7 @@ import ConfirmAddVar from './confirm-add-var'
import s from './style.module.css'
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
import cn from '@/utils/classnames'
import type { PromptVariable } from '@/models/debug'
import { type PromptVariable } from '@/models/debug'
import Tooltip from '@/app/components/base/tooltip'
import type { CompletionParams } from '@/types/app'
import { AppType } from '@/types/app'
@@ -106,22 +106,23 @@ const SettingBuiltInTool: FC<Props> = ({
</div>
{infoSchemas.length > 0 && (
<div className='my-2'>
<div className='pt-3 text-text-secondary system-sm-semibold-uppercase'>
{t('tools.setBuiltInTools.parameters')}
<div className='mt-6'>
<div className='flex items-center mb-4 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>
<div className='mr-3'>{t('tools.setBuiltInTools.parameters')}</div>
<div className='grow w-0 h-px bg-[#f3f4f6]'></div>
</div>
<div className='py-2 space-y-3'>
<div className='space-y-4'>
{infoSchemas.map((item: any, index) => (
<div key={index} className='py-1'>
<div className='flex items-center gap-2'>
<div className='text-text-secondary code-sm-semibold'>{item.label[language]}</div>
<div className='text-text-tertiary system-xs-regular'>{item.type === 'number-input' ? t('tools.setBuiltInTools.number') : t('tools.setBuiltInTools.string')}</div>
<div key={index}>
<div className='flex items-center space-x-2 leading-[18px]'>
<div className='text-[13px] font-semibold text-gray-900'>{item.label[language]}</div>
<div className='text-xs font-medium text-gray-500'>{item.type === 'number-input' ? t('tools.setBuiltInTools.number') : t('tools.setBuiltInTools.string')}</div>
{item.required && (
<div className='text-text-warning-secondary system-xs-medium'>{t('tools.setBuiltInTools.required')}</div>
<div className='text-xs font-medium text-[#EC4A0A]'>{t('tools.setBuiltInTools.required')}</div>
)}
</div>
{item.human_description && (
<div className='mt-0.5 text-text-tertiary system-xs-regular'>
<div className='mt-1 leading-[18px] text-xs font-normal text-gray-600'>
{item.human_description?.[language]}
</div>
)}
@@ -191,9 +192,9 @@ const SettingBuiltInTool: FC<Props> = ({
</>)}
</div>
)}
panelClassName='mt-[64px] mb-2 !w-[420px]'
maxWidthClassName='!max-w-[420px]'
height='calc(100vh - 64px)'
panelClassName='mt-[65px] !w-[405px]'
maxWidthClassName='!max-w-[405px]'
height='calc(100vh - 65px)'
headerClassName='!border-b-black/5'
body={
<div className='h-full pt-3'>
@@ -202,7 +203,7 @@ const SettingBuiltInTool: FC<Props> = ({
<Loading type='app' />
</div>
: (<div className='flex flex-col h-full'>
<div className='grow h-0 overflow-y-auto px-4'>
<div className='grow h-0 overflow-y-auto px-6'>
{isInfoActive ? infoUI : settingUI}
</div>
{!readonly && !isInfoActive && (
@@ -19,7 +19,7 @@ import AgentTools from './agent/agent-tools'
import ConfigContext from '@/context/debug-configuration'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import ConfigVar from '@/app/components/app/configuration/config-var'
import type { CitationConfig, ModelConfig, ModerationConfig, MoreLikeThisConfig, PromptVariable, SpeechToTextConfig, SuggestedQuestionsAfterAnswerConfig, TextToSpeechConfig } from '@/models/debug'
import { type CitationConfig, type ModelConfig, type ModerationConfig, type MoreLikeThisConfig, type PromptVariable, type SpeechToTextConfig, type SuggestedQuestionsAfterAnswerConfig, type TextToSpeechConfig } from '@/models/debug'
import type { AppType } from '@/types/app'
import { ModelModeType } from '@/types/app'
import { useModalContext } from '@/context/modal-context'
@@ -134,11 +134,11 @@ const Config: FC = () => {
annotation: annotationConfig.enabled,
setAnnotation: async (value) => {
if (value) {
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
setIsShowAnnotationConfigInit(true)
}
else {
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await handleDisableAnnotation(annotationConfig.embedding_model)
}
},
@@ -55,7 +55,7 @@ import ModelParameterModal from '@/app/components/header/account-setting/model-p
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { fetchCollectionList } from '@/service/tools'
import type { Collection } from '@/app/components/tools/types'
import { type Collection } from '@/app/components/tools/types'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
getMultipleRetrievalConfig,
@@ -142,7 +142,7 @@ const Configuration: FC = () => {
const setCompletionParams = (value: FormValue) => {
const params = { ...value }
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if ((!params.stop || params.stop.length === 0) && (modeModeTypeRef.current === ModelModeType.completion)) {
params.stop = getTempStop()
setTempStop([])
@@ -331,7 +331,7 @@ const Configuration: FC = () => {
const [canReturnToSimpleMode, setCanReturnToSimpleMode] = useState(true)
const setPromptMode = async (mode: PromptMode) => {
if (mode === PromptMode.advanced) {
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await migrateToDefaultPrompt()
setCanReturnToSimpleMode(true)
}
@@ -523,6 +523,7 @@ const Configuration: FC = () => {
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
external_data_tools: modelConfig.external_data_tools,
dataSets: datasets || [],
// eslint-disable-next-line multiline-ternary
agentConfig: res.mode === 'agent-chat' ? {
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
...modelConfig.agent_mode,
@@ -20,7 +20,7 @@ const Progress: FC<IProgressProps> = ({
className={cn(s.bar, exhausted && s['bar-error'], 'absolute top-0 left-0 right-0 bottom-0')}
style={{ width: `${value}%` }}
/>
{Array.from({ length: 10 }).fill(0).map((i, k) => (
{Array(10).fill(0).map((i, k) => (
<div key={k} className={s['bar-item']} />
))}
</div>
@@ -1,6 +1,6 @@
import AudioPlayer from '@/app/components/base/audio-btn/audio'
declare global {
// eslint-disable-next-line ts/consistent-type-definitions
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface AudioPlayerManager {
instance: AudioPlayerManager
}
@@ -12,7 +12,6 @@ export class AudioPlayerManager {
private audioPlayers: AudioPlayer | null = null
private msgId: string | undefined
// eslint-disable-next-line
private constructor() {
}
+1 -1
View File
@@ -2,7 +2,7 @@ import Toast from '@/app/components/base/toast'
import { textToAudioStream } from '@/service/share'
declare global {
// eslint-disable-next-line ts/consistent-type-definitions
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface Window {
ManagedMediaSource: any
}
@@ -55,7 +55,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
audio.load()
// Delayed generation of waveform data
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const timer = setTimeout(() => generateWaveformData(src), 1000)
return () => {
@@ -49,6 +49,4 @@ const AutoHeightTextarea = forwardRef<HTMLTextAreaElement, AutoHeightTextareaPro
},
)
AutoHeightTextarea.displayName = 'AutoHeightTextarea'
export default AutoHeightTextarea
@@ -12,9 +12,8 @@ import Divider from '@/app/components/base/divider'
import { searchEmoji } from '@/utils/emoji'
declare global {
// eslint-disable-next-line ts/no-namespace
namespace JSX {
// eslint-disable-next-line ts/consistent-type-definitions
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IntrinsicElements {
'em-emoji': React.DetailedHTMLProps< React.HTMLAttributes<HTMLElement>, HTMLElement >
}
@@ -28,6 +28,4 @@ const IconBase = forwardRef<React.MutableRefObject<HTMLOrSVGElement>, IconBasePr
})
})
IconBase.displayName = 'IconBase'
export default IconBase
+3 -2
View File
@@ -75,6 +75,7 @@ export function PreCode(props: { children: any }) {
)
}
// eslint-disable-next-line unused-imports/no-unused-vars
const useLazyLoad = (ref: RefObject<Element>): boolean => {
const [isIntersecting, setIntersecting] = useState<boolean>(false)
@@ -296,11 +297,11 @@ export default class ErrorBoundary extends Component {
}
render() {
// eslint-disable-next-line ts/ban-ts-comment
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (this.state.hasError)
return <div>Oops! An error occurred. This could be due to an ECharts runtime error or invalid SVG content. <br />(see the browser console for more information)</div>
// eslint-disable-next-line ts/ban-ts-comment
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return this.props.children
}
+4 -6
View File
@@ -44,7 +44,7 @@ const Flowchart = React.forwardRef((props: {
const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
const [isLoading, setIsLoading] = useState(true)
const timeRef = useRef<number>()
const timeRef = useRef<NodeJS.Timeout>()
const [errMsg, setErrMsg] = useState('')
const renderFlowchart = async (PrimitiveCode: string) => {
@@ -74,15 +74,15 @@ const Flowchart = React.forwardRef((props: {
return
}
if (timeRef.current)
window.clearTimeout(timeRef.current)
clearTimeout(timeRef.current)
timeRef.current = window.setTimeout(() => {
timeRef.current = setTimeout(() => {
renderFlowchart(props.PrimitiveCode)
}, 300)
}, [props.PrimitiveCode])
return (
// eslint-disable-next-line ts/ban-ts-comment
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
<div ref={ref}>
{
@@ -108,6 +108,4 @@ const Flowchart = React.forwardRef((props: {
)
})
Flowchart.displayName = 'Flowchart'
export default Flowchart
+3 -3
View File
@@ -34,15 +34,15 @@ export default function CustomPopover({
disabled = false,
}: IPopover) {
const buttonRef = useRef<HTMLButtonElement>(null)
const timeOutRef = useRef<number | null>(null)
const timeOutRef = useRef<NodeJS.Timeout | null>(null)
const onMouseEnter = (isOpen: boolean) => {
timeOutRef.current && window.clearTimeout(timeOutRef.current)
timeOutRef.current && clearTimeout(timeOutRef.current)
!isOpen && buttonRef.current?.click()
}
const onMouseLeave = (isOpen: boolean) => {
timeOutRef.current = window.setTimeout(() => {
timeOutRef.current = setTimeout(() => {
isOpen && buttonRef.current?.click()
}, timeoutDuration)
}
@@ -1,4 +1,4 @@
import type { EditorConfig, SerializedTextNode } from 'lexical'
import type { EditorConfig, NodeKey, SerializedTextNode } from 'lexical'
import { $createTextNode, TextNode } from 'lexical'
export class CustomTextNode extends TextNode {
@@ -10,9 +10,9 @@ export class CustomTextNode extends TextNode {
return new CustomTextNode(node.__text, node.__key)
}
// constructor(text: string, key?: NodeKey) {
// super(text, key)
// }
constructor(text: string, key?: NodeKey) {
super(text, key)
}
createDOM(config: EditorConfig) {
const dom = super.createDOM(config)
@@ -1,6 +1,7 @@
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical'
import {
@@ -17,9 +18,9 @@ export class VariableValueBlockNode extends TextNode {
return new VariableValueBlockNode(node.__text, node.__key)
}
// constructor(text: string, key?: NodeKey) {
// super(text, key)
// }
constructor(text: string, key?: NodeKey) {
super(text, key)
}
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config)
@@ -30,7 +30,7 @@ const TagManagementModal = ({ show, type }: TagManagementModalProps) => {
setTagList(res)
}
const [pending, setPending] = useState<boolean>(false)
const [pending, setPending] = useState<Boolean>(false)
const [name, setName] = useState<string>('')
const createNewTag = async () => {
if (!name)
@@ -54,7 +54,7 @@ const Panel = (props: PanelProps) => {
return tagList.filter(tag => tag.type === type && !value.includes(tag.id) && tag.name.includes(keywords))
}, [type, tagList, value, keywords])
const [creating, setCreating] = useState<boolean>(false)
const [creating, setCreating] = useState<Boolean>(false)
const createNewTag = async () => {
if (!keywords)
return
@@ -78,7 +78,7 @@ const TagItemEditor: FC<TagItemEditorProps> = ({
}
}
const [showRemoveModal, setShowRemoveModal] = useState(false)
const [pending, setPending] = useState<boolean>(false)
const [pending, setPending] = useState<Boolean>(false)
const removeTag = async (tagID: string) => {
if (pending)
return
@@ -21,7 +21,7 @@ import VectorSpaceFull from '@/app/components/billing/vector-space-full'
type IStepOneProps = {
datasetId?: string
dataSourceType?: DataSourceType
dataSourceTypeDisable: boolean
dataSourceTypeDisable: Boolean
hasConnection: boolean
onSetting: () => void
files: FileItem[]
@@ -28,7 +28,7 @@ import Loading from '@/app/components/base/loading'
import FloatRightContainer from '@/app/components/base/float-right-container'
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
import type { RetrievalConfig } from '@/types/app'
import { type RetrievalConfig } from '@/types/app'
import { ensureRerankModelSelected, isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import Toast from '@/app/components/base/toast'
import { formatNumber } from '@/utils/format'
@@ -202,7 +202,7 @@ const StepTwo = ({
}
const fetchFileIndexingEstimate = async (docForm = DocForm.TEXT, language?: string) => {
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const res = await didFetchFileIndexingEstimate(getFileIndexingEstimateParams(docForm, language)!)
if (segmentationType === SegmentType.CUSTOM)
setCustomFileIndexingEstimate(res)
@@ -344,7 +344,7 @@ const StepTwo = ({
doc_form: docForm,
doc_language: docLanguage,
process_rule: getProcessRule(),
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
retrieval_model: retrievalConfig, // Readonly. If want to changed, just go to settings page.
embedding_model: embeddingModel.model, // Readonly
embedding_model_provider: embeddingModel.provider, // Readonly
@@ -357,7 +357,7 @@ const StepTwo = ({
rerankDefaultModel,
isRerankDefaultModelValid: !!isRerankDefaultModelValid,
rerankModelList,
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
retrievalConfig,
indexMethod: indexMethod as string,
})
@@ -367,7 +367,7 @@ const StepTwo = ({
}
const postRetrievalConfig = ensureRerankModelSelected({
rerankDefaultModel: rerankDefaultModel!,
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
retrievalConfig,
indexMethod: indexMethod as string,
})
@@ -87,6 +87,4 @@ const Form: FC<FormProps> = React.memo(({
)
})
Form.displayName = 'Form'
export default Form
@@ -18,7 +18,7 @@ import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/d
import { updateDatasetSetting } from '@/service/datasets'
import type { DataSetListResponse } from '@/models/datasets'
import DatasetDetailContext from '@/context/dataset-detail'
import type { RetrievalConfig } from '@/types/app'
import { type RetrievalConfig } from '@/types/app'
import { useAppContext } from '@/context/app-context'
import { ensureRerankModelSelected, isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
@@ -2,8 +2,8 @@
import { useTranslation } from 'react-i18next'
import { useEffect, useRef, useState } from 'react'
import {
RiBrain2Fill,
RiBrain2Line,
RiBrainFill,
RiBrainLine,
RiColorFilterFill,
RiColorFilterLine,
RiDatabase2Fill,
@@ -63,8 +63,8 @@ export default function AccountSetting({
{
key: 'provider',
name: t('common.settings.provider'),
icon: <RiBrain2Line className={iconClassName} />,
activeIcon: <RiBrain2Fill className={iconClassName} />,
icon: <RiBrainLine className={iconClassName} />,
activeIcon: <RiBrainFill className={iconClassName} />,
},
{
key: 'members',
+1 -1
View File
@@ -11,7 +11,7 @@ import {
RiRobot2Line,
} from '@remixicon/react'
import Nav from '../nav'
import type { NavItem } from '../nav/nav-selector'
import { type NavItem } from '../nav/nav-selector'
import { fetchAppList } from '@/service/apps'
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
import CreateAppModal from '@/app/components/app/create-app-modal'
+3 -2
View File
@@ -1,5 +1,6 @@
'use client'
import React from 'react'
import { useContext } from 'use-context-selector'
import { RiVerifiedBadgeLine } from '@remixicon/react'
import type { Plugin } from '../types'
import Icon from '../card/base/card-icon'
@@ -9,7 +10,7 @@ import OrgInfo from './base/org-info'
import Description from './base/description'
import Placeholder from './base/placeholder'
import cn from '@/utils/classnames'
import { useGetLanguage } from '@/context/i18n'
import I18n from '@/context/i18n'
type Props = {
className?: string
@@ -34,7 +35,7 @@ const Card = ({
isLoading = false,
loadingFileName,
}: Props) => {
const locale = useGetLanguage()
const { locale } = useContext(I18n)
const { type, name, org, label, brief, icon } = payload
@@ -5,9 +5,6 @@ import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import type { Item } from '@/app/components/base/select'
import { PortalSelect } from '@/app/components/base/select'
import type { GitHubRepoReleaseResponse } from '@/app/components/plugins/types'
import { installPackageFromGitHub } from '@/service/plugins'
import Toast from '@/app/components/base/toast'
type InstallFromGitHubProps = {
onClose: () => void
@@ -15,104 +12,38 @@ type InstallFromGitHubProps = {
type InstallStep = 'url' | 'version' | 'package' | 'installed'
type GitHubUrlInfo = {
isValid: boolean
owner?: string
repo?: string
}
const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ onClose }) => {
const [step, setStep] = useState<InstallStep>('url')
const [repoUrl, setRepoUrl] = useState('')
const [selectedVersion, setSelectedVersion] = useState('')
const [selectedPackage, setSelectedPackage] = useState('')
const [releases, setReleases] = useState<GitHubRepoReleaseResponse[]>([])
const versions: Item[] = releases.map(release => ({
value: release.tag_name,
name: release.tag_name,
}))
// Mock data - replace with actual data fetched from the backend
const versions: Item[] = [
{ value: '1.0.1', name: '1.0.1' },
{ value: '1.2.0', name: '1.2.0' },
{ value: '1.2.1', name: '1.2.1' },
{ value: '1.3.2', name: '1.3.2' },
]
const packages: Item[] = [
{ value: 'package1', name: 'Package 1' },
{ value: 'package2', name: 'Package 2' },
{ value: 'package3', name: 'Package 3' },
]
const packages: Item[] = selectedVersion
? (releases
.find(release => release.tag_name === selectedVersion)
?.assets.map(asset => ({
value: asset.browser_download_url,
name: asset.name,
})) || [])
: []
const parseGitHubUrl = (url: string): GitHubUrlInfo => {
const githubUrlRegex = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/?$/
const match = url.match(githubUrlRegex)
if (match) {
return {
isValid: true,
owner: match[1],
repo: match[2],
}
}
return { isValid: false }
}
const handleInstall = async () => {
try {
const response = await installPackageFromGitHub({ repo: repoUrl, version: selectedVersion, package: selectedPackage })
if (response.plugin_unique_identifier) {
setStep('installed')
console.log('Package installed:')
}
else {
console.error('Failed to install package:')
}
}
catch (error) {
console.error('Error installing package:')
}
}
const handleNext = async () => {
const handleNext = () => {
switch (step) {
case 'url': {
const { isValid, owner, repo } = parseGitHubUrl(repoUrl)
if (!isValid || !owner || !repo) {
Toast.notify({
type: 'error',
message: 'Invalid GitHub URL. Please enter a valid URL in the format: https://github.com/owner/repo',
})
break
}
try {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`)
if (!res.ok)
throw new Error('Failed to fetch releases')
const data = await res.json()
const formattedReleases = data.map((release: any) => ({
tag_name: release.tag_name,
assets: release.assets.map((asset: any) => ({
browser_download_url: asset.browser_download_url,
id: asset.id,
name: asset.name,
})),
}))
setReleases(formattedReleases)
setStep('version')
}
catch (error) {
Toast.notify({
type: 'error',
message: 'Failed to fetch repository release',
})
}
case 'url':
// TODO: Validate URL and fetch versions
setStep('version')
break
}
case 'version':
// TODO: Validate version and fetch packages
setStep('package')
break
case 'package':
handleInstall()
// TODO: Handle installation
setStep('installed')
break
}
}
@@ -250,7 +181,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ onClose }) => {
className='min-w-[72px]'
onClick={onClose}
>
{step === 'url' ? 'Cancel' : 'Back'}
Cancel
</Button>
<Button
variant='primary'
@@ -1,152 +0,0 @@
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useBoolean } from 'ahooks'
import {
RiBugLine,
RiCloseLine,
RiHardDrive3Line,
RiVerifiedBadgeLine,
} from '@remixicon/react'
import type { PluginDetail } from '../types'
import { PluginSource } from '../types'
import Description from '../card/base/description'
import Icon from '../card/base/card-icon'
import Title from '../card/base/title'
import OrgInfo from '../card/base/org-info'
import OperationDropdown from './operation-dropdown'
import PluginInfo from '@/app/components/plugins/plugin-page/plugin-info'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Badge from '@/app/components/base/badge'
import Confirm from '@/app/components/base/confirm'
import Tooltip from '@/app/components/base/tooltip'
import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin'
import { Github } from '@/app/components/base/icons/src/public/common'
import I18n from '@/context/i18n'
import cn from '@/utils/classnames'
const i18nPrefix = 'plugin.action'
type Props = {
detail: PluginDetail
onHide: () => void
onDelete: () => void
}
const DetailHeader = ({
detail,
onHide,
onDelete,
}: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const hasNewVersion = useMemo(() => {
if (!detail)
return false
return false
// return pluginDetail.latest_version !== pluginDetail.version
}, [detail])
const handleUpdate = () => {}
const [isShowPluginInfo, {
setTrue: showPluginInfo,
setFalse: hidePluginInfo,
}] = useBoolean(false)
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const usedInApps = 3
return (
<div className={cn('shrink-0 p-4 pb-3 border-b border-divider-subtle bg-components-panel-bg')}>
<div className="flex">
<Icon src={detail.declaration.icon} />
<div className="ml-3 w-0 grow">
<div className="flex items-center h-5">
<Title title={detail.declaration.label[locale]} />
{detail.declaration.verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />}
<Badge
className='mx-1'
text={detail.version}
hasRedCornerMark={hasNewVersion}
/>
{hasNewVersion && (
<Button variant='secondary-accent' size='small' className='!h-5' onClick={handleUpdate}>{t('plugin.detailPanel.operation.update')}</Button>
)}
</div>
<div className='mb-1 flex justify-between items-center h-4'>
<div className='flex items-center'>
<OrgInfo
className="mt-0.5"
packageNameClassName='w-auto'
orgName={detail.declaration.author}
packageName={detail.declaration.name}
/>
<div className='ml-1 mr-0.5 text-text-quaternary system-xs-regular'>·</div>
{detail.source === PluginSource.marketplace && (
<Tooltip popupContent={t('plugin.detailPanel.categoryTip.marketplace')} >
<BoxSparkleFill className='w-3.5 h-3.5 text-text-tertiary hover:text-text-accent' />
</Tooltip>
)}
{detail.source === PluginSource.github && (
<Tooltip popupContent={t('plugin.detailPanel.categoryTip.github')} >
<Github className='w-3.5 h-3.5 text-text-secondary hover:text-text-primary' />
</Tooltip>
)}
{detail.source === PluginSource.local && (
<Tooltip popupContent={t('plugin.detailPanel.categoryTip.local')} >
<RiHardDrive3Line className='w-3.5 h-3.5 text-text-tertiary' />
</Tooltip>
)}
{detail.source === PluginSource.debugging && (
<Tooltip popupContent={t('plugin.detailPanel.categoryTip.debugging')} >
<RiBugLine className='w-3.5 h-3.5 text-text-tertiary hover:text-text-warning' />
</Tooltip>
)}
</div>
</div>
</div>
<div className='flex gap-1'>
<OperationDropdown
onInfo={showPluginInfo}
onRemove={showDeleteConfirm}
/>
<ActionButton onClick={onHide}>
<RiCloseLine className='w-4 h-4' />
</ActionButton>
</div>
</div>
<Description className='mt-3' text={detail.declaration.description[locale]} descriptionLineRows={2}></Description>
{isShowPluginInfo && (
<PluginInfo
repository={detail.meta?.repo}
release={detail.version}
packageName={detail.meta?.package}
onHide={hidePluginInfo}
/>
)}
{isShowDeleteConfirm && (
<Confirm
isShow
title={t(`${i18nPrefix}.delete`)}
content={
<div>
{t(`${i18nPrefix}.deleteContentLeft`)}<span className='system-md-semibold'>{detail.declaration.label[locale]}</span>{t(`${i18nPrefix}.deleteContentRight`)}<br />
{usedInApps > 0 && t(`${i18nPrefix}.usedInApps`, { num: usedInApps })}
</div>
}
onCancel={hideDeleteConfirm}
onConfirm={onDelete}
/>
)}
</div>
)
}
export default DetailHeader
@@ -1,198 +0,0 @@
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
import type { EndpointListItem } from '../types'
import EndpointModal from './endpoint-modal'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import ActionButton from '@/app/components/base/action-button'
import CopyBtn from '@/app/components/base/copy-btn'
import Confirm from '@/app/components/base/confirm'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import {
deleteEndpoint,
disableEndpoint,
enableEndpoint,
updateEndpoint,
} from '@/service/plugins'
type Props = {
data: EndpointListItem
}
const EndpointCard = ({
data,
}: Props) => {
const { t } = useTranslation()
const [active, setActive] = useState(data.enabled)
const endpointID = data.id
const [isShowDisableConfirm, {
setTrue: showDisableConfirm,
setFalse: hideDisableConfirm,
}] = useBoolean(false)
const activeEndpoint = async () => {
try {
await enableEndpoint({
url: '/workspaces/current/endpoints/enable',
endpointID,
})
}
catch (error) {
console.error(error)
setActive(false)
}
}
const inactiveEndpoint = async () => {
try {
await disableEndpoint({
url: '/workspaces/current/endpoints/disable',
endpointID,
})
}
catch (error) {
console.error(error)
setActive(true)
}
}
const handleSwitch = (state: boolean) => {
if (state) {
setActive(true)
activeEndpoint()
}
else {
setActive(false)
showDisableConfirm()
}
}
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const handleDelete = async () => {
try {
await deleteEndpoint({
url: '/workspaces/current/endpoints/delete',
endpointID,
})
}
catch (error) {
console.error(error)
}
}
const [isShowEndpointModal, {
setTrue: showEndpointModalConfirm,
setFalse: hideEndpointModalConfirm,
}] = useBoolean(false)
const formSchemas = useMemo(() => {
return toolCredentialToFormSchemas(data.declaration.settings)
}, [data.declaration.settings])
const formValue = useMemo(() => {
return addDefaultValue(data.settings, formSchemas)
}, [data.settings, formSchemas])
const handleUpdate = (state: any) => {
try {
updateEndpoint({
url: '/workspaces/current/endpoints',
body: {
endpoint_id: data.id,
settings: state,
name: state.name,
},
})
}
catch (error) {
console.error(error)
}
}
return (
<div className='p-0.5 bg-background-section-burn rounded-xl'>
<div className='group p-2.5 pl-3 bg-components-panel-on-panel-item-bg rounded-[10px] border-[0.5px] border-components-panel-border'>
<div className='flex items-center'>
<div className='grow mb-1 h-6 flex items-center gap-1 text-text-secondary system-md-semibold'>
<RiLoginCircleLine className='w-4 h-4' />
<div>{data.name}</div>
</div>
<div className='hidden group-hover:flex items-center'>
<ActionButton onClick={showEndpointModalConfirm}>
<RiEditLine className='w-4 h-4' />
</ActionButton>
<ActionButton onClick={showDeleteConfirm} className='hover:bg-state-destructive-hover text-text-tertiary hover:text-text-destructive'>
<RiDeleteBinLine className='w-4 h-4' />
</ActionButton>
</div>
</div>
{data.declaration.endpoints.map((endpoint, index) => (
<div key={index} className='h-6 flex items-center'>
<div className='shrink-0 w-12 text-text-tertiary system-xs-regular'>{endpoint.method}</div>
<div className='group/item grow flex items-center text-text-secondary system-xs-regular truncate'>
<div title={`${data.url}${endpoint.path}`} className='truncate'>{`${data.url}${endpoint.path}`}</div>
<CopyBtn
className='hidden shrink-0 ml-2 group-hover/item:block'
value={`${data.url}${endpoint.path}`}
isPlain
/>
</div>
</div>
))}
</div>
<div className='p-2 pl-3 flex items-center justify-between'>
{active && (
<div className='flex items-center gap-1 system-xs-semibold-uppercase text-util-colors-green-green-600'>
<Indicator color='green' />
{t('plugin.detailPanel.serviceOk')}
</div>
)}
{!active && (
<div className='flex items-center gap-1 system-xs-semibold-uppercase text-text-tertiary'>
<Indicator color='gray' />
{t('plugin.detailPanel.disabled')}
</div>
)}
<Switch
className='ml-3'
defaultValue={active}
onChange={handleSwitch}
size='sm'
/>
</div>
{isShowDisableConfirm && (
<Confirm
isShow
title={t('plugin.detailPanel.endpointDisableTip')}
content={<div>{t('plugin.detailPanel.endpointDisableContent', { name: data.name })}</div>}
onCancel={() => {
hideDisableConfirm()
setActive(true)
}}
onConfirm={inactiveEndpoint}
/>
)}
{isShowDeleteConfirm && (
<Confirm
isShow
title={t('plugin.detailPanel.endpointDeleteTip')}
content={<div>{t('plugin.detailPanel.endpointDeleteContent', { name: data.name })}</div>}
onCancel={hideDeleteConfirm}
onConfirm={handleDelete}
/>
)}
{isShowEndpointModal && (
<EndpointModal
formSchemas={formSchemas}
defaultValues={formValue}
onCancel={hideEndpointModalConfirm}
onSaved={handleUpdate}
/>
)}
</div>
)
}
export default EndpointCard
@@ -1,55 +1,66 @@
import React, { useMemo } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { RiAddLine } from '@remixicon/react'
import type { EndpointListItem, PluginEndpointDeclaration } from '../types'
import EndpointModal from './endpoint-modal'
import EndpointCard from './endpoint-card'
import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { RiAddLine, RiLoginCircleLine } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import CopyBtn from '@/app/components/base/copy-btn'
import Indicator from '@/app/components/header/indicator'
import Tooltip from '@/app/components/base/tooltip'
import {
createEndpoint,
} from '@/service/plugins'
import Switch from '@/app/components/base/switch'
type Props = {
pluginUniqueID: string
declaration: PluginEndpointDeclaration
list: EndpointListItem[]
const EndpointCard = () => {
const { t } = useTranslation()
return (
<div className='p-0.5 bg-background-section-burn rounded-xl'>
<div className='p-2.5 pl-3 bg-components-panel-on-panel-item-bg rounded-[10px] border-[0.5px] border-components-panel-border'>
<div className='mb-1 h-6 flex items-center gap-1 text-text-secondary system-md-semibold'>
<RiLoginCircleLine className='w-4 h-4' />
<div>Endpoint for Unreal workspace</div>
</div>
<div className='h-6 flex items-center'>
<div className='shrink-0 w-24 text-text-tertiary system-xs-regular'>Start Callback</div>
<div className='group grow flex items-center text-text-secondary system-xs-regular truncate'>
<div className='truncate'>https://extension.dify.ai/a1b2c3d4/onStart</div>
<CopyBtn
className='hidden shrink-0 ml-2 group-hover:block'
value={'https://extension.dify.ai/a1b2c3d4/onStart'}
isPlain
/>
</div>
</div>
<div className='h-6 flex items-center'>
<div className='shrink-0 w-24 text-text-tertiary system-xs-regular'>Finish Callback</div>
<div className='group grow flex items-center text-text-secondary system-xs-regular truncate'>
<div className='truncate'>https://extension.dify.ai/a1b2c3d4/onFinish</div>
<CopyBtn
className='hidden shrink-0 ml-2 group-hover:block'
value={'https://extension.dify.ai/a1b2c3d4/onFinish'}
isPlain
/>
</div>
</div>
</div>
<div className='px-3 py-2 flex items-center justify-between'>
<div className='flex items-center gap-1 system-xs-semibold-uppercase text-util-colors-green-green-600'>
<Indicator color='green' />
{t('plugin.detailPanel.serviceOk')}
</div>
{/* <div className='flex items-center gap-1 system-xs-semibold-uppercase text-text-tertiary'>
<Indicator color='gray' />
{t('plugin.detailPanel.disabled')}
</div> */}
<Switch
className='ml-3'
defaultValue={true}
onChange={() => {}}
size='sm'
/>
</div>
</div>
)
}
const EndpointList = ({
pluginUniqueID,
declaration,
list,
}: Props) => {
const EndpointList = () => {
const { t } = useTranslation()
const [isShowEndpointModal, {
setTrue: showEndpointModal,
setFalse: hideEndpointModal,
}] = useBoolean(false)
const formSchemas = useMemo(() => {
return toolCredentialToFormSchemas(declaration.settings)
}, [declaration.settings])
const handleCreate = (state: any) => {
try {
createEndpoint({
url: '/workspaces/current/endpoints',
body: {
plugin_unique_identifier: pluginUniqueID,
settings: state,
name: state.name,
},
})
}
catch (error) {
console.error(error)
}
}
return (
<div className='px-4 py-2 border-t border-divider-subtle'>
<div className='mb-1 h-6 flex items-center justify-between text-text-secondary system-sm-semibold-uppercase'>
@@ -57,32 +68,24 @@ const EndpointList = ({
{t('plugin.detailPanel.endpoints')}
<Tooltip
popupContent={
<div className='w-[180px]'>TODO</div>
<div className='w-[180px]'>
{t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
<ActionButton onClick={showEndpointModal}>
<ActionButton>
<RiAddLine className='w-4 h-4' />
</ActionButton>
</div>
{list.length === 0 && (
<div className='mb-1 p-3 flex justify-center rounded-[10px] bg-background-section text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.endpointsEmpty')}</div>
)}
<div className='mb-1 p-3 flex justify-center rounded-[10px] bg-background-section text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.endpointsEmpty')}</div>
<div className='flex flex-col gap-2'>
{list.map((item, index) => (
<EndpointCard
key={index}
data={item}
/>
))}
<EndpointCard />
<EndpointCard />
<EndpointCard />
</div>
{isShowEndpointModal && (
<EndpointModal
formSchemas={formSchemas}
onCancel={hideEndpointModal}
onSaved={handleCreate}
/>
)}
</div>
)
}
@@ -1,96 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine, RiCloseLine } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Drawer from '@/app/components/base/drawer'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import Toast from '@/app/components/base/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import cn from '@/utils/classnames'
type Props = {
formSchemas: any
defaultValues?: any
onCancel: () => void
onSaved: (value: Record<string, any>) => void
}
const EndpointModal: FC<Props> = ({
formSchemas,
defaultValues = {},
onCancel,
onSaved,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const [tempCredential, setTempCredential] = React.useState<any>(defaultValues)
const handleSave = () => {
for (const field of formSchemas) {
if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: field.label[language] || field.label.en_US }) })
return
}
}
onSaved(tempCredential)
}
return (
<Drawer
isOpen
clickOutsideNotOpen={false}
onClose={onCancel}
footer={null}
mask
positionCenter={false}
panelClassname={cn('justify-start mt-[64px] mr-2 mb-2 !w-[420px] !max-w-[420px] !p-0 !bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl')}
>
<>
<div className='p-4 pb-2'>
<div className='flex items-center justify-between'>
<div className='text-text-primary system-xl-semibold'>{t('plugin.detailPanel.endpointModalTitle')}</div>
<ActionButton onClick={onCancel}>
<RiCloseLine className='w-4 h-4' />
</ActionButton>
</div>
<div className='mt-0.5 text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.endpointModalDesc')}</div>
</div>
<div className='grow overflow-y-auto'>
<div className='px-4 py-2'>
<Form
value={tempCredential}
onChange={(v) => {
setTempCredential(v)
}}
formSchemas={formSchemas}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='!bg-gray-50'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center body-xs-regular text-text-accent-secondary'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>)
: null}
/>
</div>
<div className={cn('p-4 pt-0 flex justify-end')} >
<div className='flex gap-2'>
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
</div>
</div>
</div>
</>
</Drawer>
)
}
export default React.memo(EndpointModal)
@@ -1,60 +1,132 @@
'use client'
import React from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import type { EndpointListItem, PluginDetail } from '../types'
import DetailHeader from './detail-header'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { RiCloseLine, RiVerifiedBadgeLine } from '@remixicon/react'
import type { Plugin } from '../types'
// import { PluginType } from '../types'
import Badge from '../../base/badge'
import Description from '../card/base/description'
import Icon from '../card/base/card-icon'
import Title from '../card/base/title'
import OperationDropdown from './operation-dropdown'
import EndpointList from './endpoint-list'
import ActionList from './action-list'
import ModelList from './model-list'
import type { Locale } from '@/i18n'
import { fetchPluginDetail } from '@/app/(commonLayout)/plugins/test/card/actions'
import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin'
import Button from '@/app/components/base/button'
import ActionButton from '@/app/components/base/action-button'
import Drawer from '@/app/components/base/drawer'
import Loading from '@/app/components/base/loading'
import cn from '@/utils/classnames'
import {
// extensionDallE,
// modelGPT4,
toolNotion,
} from '@/app/components/plugins/card/card-mock'
type Props = {
pluginDetail: PluginDetail | undefined
endpointList: EndpointListItem[]
onHide: () => void
locale: Locale // The component is used in both client and server side, so we can't get the locale from both side(getLocaleOnServer and useContext)
}
const PluginDetailPanel: FC<Props> = ({
pluginDetail,
endpointList = [],
onHide,
locale,
}) => {
const { t } = useTranslation()
const searchParams = useSearchParams()
const org = searchParams.get('org')
const name = searchParams.get('name')
const router = useRouter()
const pathname = usePathname()
const [loading, setLoading] = useState(true)
const [pluginDetail, setPluginDetail] = useState<Plugin>()
const handleDelete = () => {}
const hasNewVersion = useMemo(() => {
if (!pluginDetail)
return false
return pluginDetail.latest_version !== pluginDetail.version
}, [pluginDetail])
if (!pluginDetail)
const getPluginDetail = async (org: string, name: string) => {
console.log('organization: ', org)
console.log('plugin name: ', name)
setLoading(true)
const detail = await fetchPluginDetail(org, name)
setPluginDetail({
...detail,
...toolNotion,
} as any)
setLoading(false)
}
const handleClose = () => {
setPluginDetail(undefined)
router.replace(pathname)
}
const handleUpdate = () => {}
useEffect(() => {
if (org && name)
getPluginDetail(org, name)
}, [org, name])
if (!org || !name)
return null
return (
<Drawer
isOpen={!!pluginDetail}
clickOutsideNotOpen={false}
onClose={onHide}
onClose={handleClose}
footer={null}
mask={false}
positionCenter={false}
panelClassname={cn('justify-start mt-[64px] mr-2 mb-2 !w-[420px] !max-w-[420px] !p-0 !bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl')}
>
{pluginDetail && (
{loading && <Loading type='area' />}
{!loading && pluginDetail && (
<>
<DetailHeader
detail={pluginDetail}
onHide={onHide}
onDelete={handleDelete}
/>
<div className={cn('shrink-0 p-4 pb-3 border-b border-divider-subtle bg-components-panel-bg')}>
<div className="flex">
<Icon src={pluginDetail.icon} />
<div className="ml-3 w-0 grow">
<div className="flex items-center h-5">
<Title title={pluginDetail.label[locale]} />
<RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />
<Badge
className='mx-1'
text={pluginDetail.version}
hasRedCornerMark={hasNewVersion}
/>
{hasNewVersion && (
<Button variant='secondary-accent' size='small' className='!h-5' onClick={handleUpdate}>{t('plugin.detailPanel.operation.update')}</Button>
)}
</div>
<div className='mb-1 flex justify-between items-center h-4'>
<div className='flex items-center'>
<div className='text-text-tertiary system-xs-regular'>{pluginDetail.org}</div>
<div className='ml-1 text-text-quaternary system-xs-regular'>·</div>
<BoxSparkleFill className='w-3.5 h-3.5 text-text-tertiary' />
</div>
</div>
</div>
<div className='flex gap-1'>
<OperationDropdown />
<ActionButton onClick={handleClose}>
<RiCloseLine className='w-4 h-4' />
</ActionButton>
</div>
</div>
<Description className='mt-3' text={pluginDetail.brief[locale]} descriptionLineRows={2}></Description>
</div>
<div className='grow overflow-y-auto'>
{!!pluginDetail.declaration.endpoint && (
<EndpointList
pluginUniqueID={pluginDetail.plugin_unique_identifier}
list={endpointList}
declaration={pluginDetail.declaration.endpoint}
/>
)}
{!!pluginDetail.declaration.tool && <ActionList />}
{!!pluginDetail.declaration.model && <ModelList />}
<ActionList />
<EndpointList />
<ModelList />
</div>
</>
)}
@@ -1,105 +0,0 @@
import { PluginSource, PluginType } from '../types'
export const toolNotion = {
id: 'dlfajkgjdga-dfjalksjfglkds-dfjakld',
created_at: '2024-10-16 16:05:33',
updated_at: '2024-10-16 16:05:33',
name: 'notion page search',
plugin_id: 'Notion/notion-page-search',
plugin_unique_identifier: 'Notion/notion-page-search:1.2.0@fldsjflkdsajfldsakajfkls',
declaration: {
version: '1.2.0',
author: 'Notion',
name: 'notion page search',
category: PluginType.tool,
icon: 'https://via.placeholder.com/150',
label: {
'en-US': 'Notion Page Search',
'zh-Hans': 'Notion 页面搜索',
},
description: {
'en-US': 'Description: Search Notion pages and open visited ones faster. No admin access required.More and more info...More and more info...More and more info...',
'zh-Hans': '搜索 Notion 页面并更快地打开已访问的页面。无需管理员访问权限。More and more info...More and more info...More and more info...',
},
created_at: '2024-10-16 16:05:33',
resource: {},
plugins: {},
endpoint: {
settings: [
{
type: 'secret-input',
name: 'api-key',
required: true,
default: null,
options: null,
label: {
en_US: 'API-key',
zh_Hans: 'API-key',
},
help: null,
url: null,
placeholder: {
en_US: 'Please input your API key',
zh_Hans: '请输入你的 API key',
},
},
],
endpoints: [
{ path: '/duck/<app_id>', method: 'GET' },
{ path: '/neko', method: 'GET' },
],
},
tool: null, // TODO
verified: true,
},
installation_id: 'jflkdsjoewingljlsadjgoijg-dkfjldajglkajglask-dlfkajdg',
tenant_id: 'jflkdsjoewingljlsadjgoijg',
endpoints_setups: 2,
endpoints_active: 1,
version: '1.2.0',
source: PluginSource.marketplace,
meta: null,
}
export const toolNotionEndpoints = [
{
id: 'dlfajkgjdga-dfjalksjfglkds-dfjakld',
created_at: '2024-10-16 16:05:33',
updated_at: '2024-10-16 16:05:33',
settings: {
'api-key': '*******',
},
tenant_id: 'jflkdsjoewingljlsadjgoijg',
plugin_id: 'Notion/notion-page-search',
expired_at: '2024-10-16 16:05:33',
declaration: {
settings: [
{
type: 'secret-input',
name: 'api-key',
required: true,
default: null,
options: null,
label: {
en_US: 'API-key',
zh_Hans: 'API-key',
},
help: null,
url: null,
placeholder: {
en_US: 'Please input your API key',
zh_Hans: '请输入你的 API key',
},
},
],
endpoints: [
{ path: '/duck/<app_id>', method: 'GET' },
{ path: '/neko', method: 'GET' },
],
},
name: 'default',
enabled: true,
url: 'http://localhost:5002/e/45rj9V4TRxAjL0I2wXRZgZdXjdHEKBh8',
hook_id: '45rj9V4TRxAjL0I2wXRZgZdXjdHEKBh8',
},
]
@@ -13,14 +13,9 @@ import {
import cn from '@/utils/classnames'
type Props = {
onInfo: () => void
onRemove: () => void
}
const OperationDropdown: FC<Props> = ({
onInfo,
onRemove,
}) => {
const OperationDropdown: FC<Props> = () => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
@@ -50,16 +45,14 @@ const OperationDropdown: FC<Props> = ({
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[160px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
<div onClick={onInfo} className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.info')}</div>
{/* ##plugin TODO## check update */}
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.info')}</div>
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.checkUpdate')}</div>
{/* ##plugin TODO## router action */}
<div className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>
<div className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</div>
<RiArrowRightUpLine className='shrink-0 w-3.5 h-3.5 text-text-tertiary' />
</div>
<div className='my-1 h-px bg-divider-subtle'></div>
<div onClick={onRemove} className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.remove')}</div>
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.remove')}</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
@@ -1,19 +1,14 @@
'use client'
import { useState } from 'react'
import type { EndpointListItem, PluginDetail } from '../types'
import type { FilterState } from './filter-management'
import FilterManagement from './filter-management'
import List from './list'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { toolNotion, toolNotionEndpoints } from '@/app/components/plugins/plugin-detail-panel/mock'
const PluginsPanel = () => {
const handleFilterChange = (filters: FilterState) => {
//
}
const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginDetail | undefined>(toolNotion as any)
const [currentPluginEndpoints, setCurrentEndpoints] = useState<EndpointListItem[]>(toolNotionEndpoints as any)
return (
<>
<div className='flex flex-col pt-1 pb-3 px-12 justify-center items-start gap-3 self-stretch'>
@@ -27,14 +22,6 @@ const PluginsPanel = () => {
<List />
</div>
</div>
<PluginDetailPanel
pluginDetail={currentPluginDetail}
endpointList={currentPluginEndpoints}
onHide={() => {
setCurrentPluginDetail(undefined)
setCurrentEndpoints([])
}}
/>
</>
)
}
-121
View File
@@ -1,5 +1,4 @@
import type { CredentialFormSchemaBase } from '../header/account-setting/model-provider-page/declarations'
import type { ToolCredential } from '@/app/components/tools/types'
import type { Locale } from '@/i18n'
export enum PluginType {
@@ -8,84 +7,6 @@ export enum PluginType {
extension = 'extension',
}
export enum PluginSource {
marketplace = 'marketplace',
github = 'github',
local = 'package',
debugging = 'remote',
}
export type PluginToolDeclaration = {
identity: {
author: string
name: string
description: Record<Locale, string>
icon: string
label: Record<Locale, string>
tags: string[]
}
credentials_schema: ToolCredential[] // TODO
}
export type PluginEndpointDeclaration = {
settings: ToolCredential[]
endpoints: EndpointItem[]
}
export type EndpointItem = {
path: string
method: string
}
export type EndpointListItem = {
id: string
created_at: string
updated_at: string
settings: Record<string, any>
tenant_id: string
plugin_id: string
expired_at: string
declaration: PluginEndpointDeclaration
name: string
enabled: boolean
url: string
hook_id: string
}
export type PluginDeclaration = {
version: string
author: string
icon: string
name: string
category: PluginType
label: Record<Locale, string>
description: Record<Locale, string>
created_at: string
resource: any // useless in frontend
plugins: any // useless in frontend
verified: boolean
endpoint: PluginEndpointDeclaration
tool: PluginToolDeclaration
model: any // TODO
}
export type PluginDetail = {
id: string
created_at: string
updated_at: string
name: string
plugin_id: string
plugin_unique_identifier: string
declaration: PluginDeclaration
installation_id: string
tenant_id: string
endpoints_setups: number
endpoints_active: number
version: string
source: PluginSource
meta?: any
}
export type Plugin = {
'type': PluginType
'org': string
@@ -115,45 +36,3 @@ export type Permissions = {
canManagement: PermissionType
canDebugger: PermissionType
}
// endpoint
export type CreateEndpointRequest = {
plugin_unique_identifier: string
settings: Record<string, any>
name: string
}
export type EndpointOperationResponse = {
result: 'success' | 'error'
}
export type EndpointsRequest = {
limit: number
page: number
plugin_id: string
}
export type EndpointsResponse = {
endpoints: EndpointListItem[]
has_more: boolean
limit: number
total: number
page: number
}
export type UpdateEndpointRequest = {
endpoint_id: string
settings: Record<string, any>
name: string
}
export type GitHubAsset = {
id: number
name: string
browser_download_url: string
}
export type GitHubRepoReleaseResponse = {
tag_name: string
assets: GitHubAsset[]
}
export type InstallPackageResponse = {
plugin_unique_identifier: string
}
@@ -132,9 +132,9 @@ const TextGeneration: FC<IMainProps> = ({
const handleSend = () => {
setIsCallBatchAPI(false)
setControlSend(Date.now())
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
setAllTaskList([]) // clear batch task running status
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
showResSidebar()
}
@@ -314,7 +314,7 @@ const TextGeneration: FC<IMainProps> = ({
setControlSend(Date.now())
// clear run once task status
setControlStopResponding(Date.now())
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
showResSidebar()
}
const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => {
+19 -10
View File
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import type { Collection } from './types'
import Marketplace from './marketplace'
import cn from '@/utils/classnames'
@@ -8,15 +9,21 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import LabelFilter from '@/app/components/tools/labels/filter'
import SearchInput from '@/app/components/base/search-input'
import { DotsGrid } from '@/app/components/base/icons/src/vender/line/general'
import { Colors } from '@/app/components/base/icons/src/vender/line/others'
import { Route } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
import ProviderDetail from '@/app/components/tools/provider/detail'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import { fetchCollectionList } from '@/service/tools'
import Card from '@/app/components/plugins/card'
import { useGetLanguage } from '@/context/i18n'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import { useSelector as useAppContextSelector } from '@/context/app-context'
const ProviderList = () => {
const { t } = useTranslation()
const language = useGetLanguage()
const containerRef = useRef<HTMLDivElement>(null)
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
@@ -24,9 +31,9 @@ const ProviderList = () => {
defaultTab: 'builtin',
})
const options = [
{ value: 'builtin', text: t('tools.type.builtIn') },
{ value: 'api', text: t('tools.type.custom') },
{ value: 'workflow', text: t('tools.type.workflow') },
{ value: 'builtin', text: t('tools.type.builtIn'), icon: <DotsGrid className='w-[14px] h-[14px] mr-1' /> },
{ value: 'api', text: t('tools.type.custom'), icon: <Colors className='w-[14px] h-[14px] mr-1' /> },
{ value: 'workflow', text: t('tools.type.workflow'), icon: <Route className='w-[14px] h-[14px] mr-1' /> },
]
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
const handleTagsChange = (value: string[]) => {
@@ -91,7 +98,9 @@ const ProviderList = () => {
</div>
<div className={cn(
'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0',
currentProvider && 'pr-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
)}>
{activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList} />}
{filteredCollectionList.map(collection => (
<div
key={collection.id}
@@ -125,13 +134,13 @@ const ProviderList = () => {
)
}
</div>
{currentProvider && (
<ProviderDetail
collection={currentProvider}
onHide={() => setCurrentProvider(undefined)}
onRefreshData={getProviderList}
/>
)}
<div className={cn(
'shrink-0 w-0 border-l-[0.5px] border-black/8 overflow-y-auto transition-all duration-200 ease-in-out',
currentProvider && 'w-[420px]',
)}>
{currentProvider && <ProviderDetail collection={currentProvider} onRefreshData={getProviderList} />}
</div>
<div className='absolute top-5 right-5 p-1 cursor-pointer' onClick={() => setCurrentProvider(undefined)}><RiCloseLine className='w-4 h-4' /></div>
</div>
)
}
+150 -173
View File
@@ -2,9 +2,6 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
RiCloseLine,
} from '@remixicon/react'
import { AuthHeaderPrefix, AuthType, CollectionType } from '../types'
import type { Collection, CustomCollectionBackend, Tool, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '../types'
import ToolItem from './tool-item'
@@ -12,20 +9,14 @@ import cn from '@/utils/classnames'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import Confirm from '@/app/components/base/confirm'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import { LinkExternal02, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import Icon from '@/app/components/plugins/card/base/card-icon'
import Title from '@/app/components/plugins/card/base/title'
import OrgInfo from '@/app/components/plugins/card/base/org-info'
import Description from '@/app/components/plugins/card/base/description'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import WorkflowToolModal from '@/app/components/tools/workflow-tool'
import Toast from '@/app/components/base/toast'
import Drawer from '@/app/components/base/drawer'
import ActionButton from '@/app/components/base/action-button'
import {
deleteWorkflowTool,
fetchBuiltInToolList,
@@ -47,13 +38,11 @@ import { useAppContext } from '@/context/app-context'
type Props = {
collection: Collection
onHide: () => void
onRefreshData: () => void
}
const ProviderDetail = ({
collection,
onHide,
onRefreshData,
}: Props) => {
const { t } = useTranslation()
@@ -224,176 +213,164 @@ const ProviderDetail = ({
}, [collection.name, collection.type, getCustomProvider, getProviderToolList, getWorkflowToolProvider])
return (
<Drawer
isOpen={!!collection}
clickOutsideNotOpen={false}
onClose={onHide}
footer={null}
mask={false}
positionCenter={false}
panelClassname={cn('justify-start mt-[64px] mr-2 mb-2 !w-[420px] !max-w-[420px] !p-0 !bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl')}
>
<div className='p-4'>
<div className='flex'>
<Icon src={collection.icon} />
<div className="ml-3 w-0 grow">
<div className="flex items-center h-5">
<Title title={collection.label[language]} />
</div>
<div className='mb-1 flex justify-between items-center h-4'>
<OrgInfo
className="mt-0.5"
packageNameClassName='w-auto'
orgName={collection.author}
packageName={collection.name}
/>
</div>
</div>
<div className='flex gap-1'>
<ActionButton onClick={onHide}>
<RiCloseLine className='w-4 h-4' />
</ActionButton>
<div className='px-6 py-3'>
<div className='flex items-center py-1 gap-2'>
<div className='relative shrink-0'>
{typeof collection.icon === 'string' && (
<div className='w-8 h-8 bg-center bg-cover bg-no-repeat rounded-md' style={{ backgroundImage: `url(${collection.icon})` }} />
)}
{typeof collection.icon !== 'string' && (
<AppIcon
size='small'
icon={collection.icon.content}
background={collection.icon.background}
/>
)}
</div>
<div className='grow w-0 py-[1px]'>
<div className='flex items-center text-md leading-6 font-semibold text-gray-900'>
<div className='truncate' title={collection.label[language]}>{collection.label[language]}</div>
</div>
</div>
<Description className='mt-3' text={collection.description[language]} descriptionLineRows={2}></Description>
<div className='flex gap-1 border-b-[0.5px] border-black/5'>
{(collection.type === CollectionType.builtIn) && needAuth && (
</div>
<div className='mt-2 min-h-[36px] text-gray-500 text-sm leading-[18px]'>{collection.description[language]}</div>
<div className='flex gap-1 border-b-[0.5px] border-black/5'>
{(collection.type === CollectionType.builtIn) && needAuth && (
<Button
variant={isAuthed ? 'secondary' : 'primary'}
className={cn('shrink-0 my-3 w-full', isAuthed && 'bg-white')}
onClick={() => {
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
showSettingAuthModal()
}}
disabled={!isCurrentWorkspaceManager}
>
{isAuthed && <Indicator className='mr-2' color={'green'} />}
<div className={cn('text-white leading-[18px] text-[13px] font-medium', isAuthed && '!text-gray-700')}>
{isAuthed ? t('tools.auth.authorized') : t('tools.auth.unauthorized')}
</div>
</Button>
)}
{collection.type === CollectionType.custom && !isDetailLoading && (
<Button
className={cn('shrink-0 my-3 w-full')}
onClick={() => setIsShowEditCustomCollectionModal(true)}
>
<Settings01 className='mr-1 w-4 h-4 text-gray-500' />
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
</Button>
)}
{collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
<>
<Button
variant={isAuthed ? 'secondary' : 'primary'}
className={cn('shrink-0 my-3 w-full', isAuthed && 'bg-white')}
onClick={() => {
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
showSettingAuthModal()
}}
variant='primary'
className={cn('shrink-0 my-3 w-[183px]')}
>
<a className='flex items-center text-white' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
<div className='leading-5 text-sm font-medium'>{t('tools.openInStudio')}</div>
<LinkExternal02 className='ml-1 w-4 h-4' />
</a>
</Button>
<Button
className={cn('shrink-0 my-3 w-[183px]')}
onClick={() => setIsShowEditWorkflowToolModal(true)}
disabled={!isCurrentWorkspaceManager}
>
{isAuthed && <Indicator className='mr-2' color={'green'} />}
<div className={cn('text-white leading-[18px] text-[13px] font-medium', isAuthed && '!text-gray-700')}>
{isAuthed ? t('tools.auth.authorized') : t('tools.auth.unauthorized')}
</div>
</Button>
)}
{collection.type === CollectionType.custom && !isDetailLoading && (
<Button
className={cn('shrink-0 my-3 w-full')}
onClick={() => setIsShowEditCustomCollectionModal(true)}
>
<Settings01 className='mr-1 w-4 h-4 text-gray-500' />
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
</Button>
)}
{collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
<>
<Button
variant='primary'
className={cn('shrink-0 my-3 w-[183px]')}
>
<a className='flex items-center text-white' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
<div className='leading-5 text-sm font-medium'>{t('tools.openInStudio')}</div>
<LinkExternal02 className='ml-1 w-4 h-4' />
</a>
</Button>
<Button
className={cn('shrink-0 my-3 w-[183px]')}
onClick={() => setIsShowEditWorkflowToolModal(true)}
disabled={!isCurrentWorkspaceManager}
>
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
</Button>
</>
)}
</div>
{/* Tools */}
<div className='pt-3'>
{isDetailLoading && <div className='flex h-[200px]'><Loading type='app' /></div>}
{!isDetailLoading && (
<div className='text-text-secondary system-sm-semibold-uppercase'>
{collection.type === CollectionType.workflow && <span className=''>{t('tools.createTool.toolInput.title').toLocaleUpperCase()}</span>}
{collection.type !== CollectionType.workflow && <span className=''>{t('tools.includeToolNum', { num: toolList.length }).toLocaleUpperCase()}</span>}
{needAuth && (isBuiltIn || isModel) && !isAuthed && (
<>
<span className='px-1'>·</span>
<span className='text-[#DC6803]'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
</>
)}
</div>
)}
{!isDetailLoading && (
<div className='mt-1 py-2'>
{collection.type !== CollectionType.workflow && toolList.map(tool => (
<ToolItem
key={tool.name}
disabled={needAuth && (isBuiltIn || isModel) && !isAuthed}
collection={collection}
tool={tool}
isBuiltIn={isBuiltIn}
isModel={isModel}
/>
))}
{collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
<div key={item.name} className='mb-1 py-1'>
<div className='mb-1 flex items-center gap-2'>
<span className='text-text-secondary code-sm-semibold'>{item.name}</span>
<span className='text-text-tertiary system-xs-regular'>{item.type}</span>
<span className='text-text-warning-secondary system-xs-medium'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
</div>
<div className='text-text-tertiary system-xs-regular'>{item.llm_description}</div>
</div>
))}
</div>
)}
</div>
{showSettingAuth && (
<ConfigCredential
collection={collection}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential(collection.name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await onRefreshData()
setShowSettingAuth(false)
}}
onRemove={async () => {
await removeBuiltInToolCredential(collection.name)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await onRefreshData()
setShowSettingAuth(false)
}}
/>
)}
{isShowEditCollectionToolModal && (
<EditCustomToolModal
payload={customCollection}
onHide={() => setIsShowEditCustomCollectionModal(false)}
onEdit={doUpdateCustomToolCollection}
onRemove={onClickCustomToolDelete}
/>
)}
{isShowEditWorkflowToolModal && (
<WorkflowToolModal
payload={customCollection}
onHide={() => setIsShowEditWorkflowToolModal(false)}
onRemove={onClickWorkflowToolDelete}
onSave={updateWorkflowToolProvider}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('tools.createTool.deleteToolConfirmTitle')}
content={t('tools.createTool.deleteToolConfirmContent')}
isShow={showConfirmDelete}
onConfirm={handleConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
</>
)}
</div>
</Drawer>
{/* Tools */}
<div className='pt-3'>
{isDetailLoading && <div className='flex h-[200px]'><Loading type='app' /></div>}
{!isDetailLoading && (
<div className='text-xs font-medium leading-6 text-gray-500'>
{collection.type === CollectionType.workflow && <span className=''>{t('tools.createTool.toolInput.title').toLocaleUpperCase()}</span>}
{collection.type !== CollectionType.workflow && <span className=''>{t('tools.includeToolNum', { num: toolList.length }).toLocaleUpperCase()}</span>}
{needAuth && (isBuiltIn || isModel) && !isAuthed && (
<>
<span className='px-1'>·</span>
<span className='text-[#DC6803]'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
</>
)}
</div>
)}
{!isDetailLoading && (
<div className='mt-1'>
{collection.type !== CollectionType.workflow && toolList.map(tool => (
<ToolItem
key={tool.name}
disabled={needAuth && (isBuiltIn || isModel) && !isAuthed}
collection={collection}
tool={tool}
isBuiltIn={isBuiltIn}
isModel={isModel}
/>
))}
{collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
<div key={item.name} className='mb-2 px-4 py-3 rounded-xl bg-gray-25 border-[0.5px] border-gray-200'>
<div className='flex items-center gap-2'>
<span className='font-medium text-sm text-gray-900'>{item.name}</span>
<span className='text-xs leading-[18px] text-gray-500'>{item.type}</span>
<span className='font-medium text-xs leading-[18px] text-[#ec4a0a]'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
</div>
<div className='h-[18px] leading-[18px] text-gray-500 text-xs'>{item.llm_description}</div>
</div>
))}
</div>
)}
</div>
{showSettingAuth && (
<ConfigCredential
collection={collection}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential(collection.name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await onRefreshData()
setShowSettingAuth(false)
}}
onRemove={async () => {
await removeBuiltInToolCredential(collection.name)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await onRefreshData()
setShowSettingAuth(false)
}}
/>
)}
{isShowEditCollectionToolModal && (
<EditCustomToolModal
payload={customCollection}
onHide={() => setIsShowEditCustomCollectionModal(false)}
onEdit={doUpdateCustomToolCollection}
onRemove={onClickCustomToolDelete}
/>
)}
{isShowEditWorkflowToolModal && (
<WorkflowToolModal
payload={customCollection}
onHide={() => setIsShowEditWorkflowToolModal(false)}
onRemove={onClickWorkflowToolDelete}
onSave={updateWorkflowToolProvider}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('tools.createTool.deleteToolConfirmTitle')}
content={t('tools.createTool.deleteToolConfirmContent')}
isShow={showConfirmDelete}
onConfirm={handleConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</div>
)
}
export default ProviderDetail
@@ -29,11 +29,11 @@ const ToolItem = ({
return (
<>
<div
className={cn('mb-2 px-4 py-3 bg-components-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover', disabled && 'opacity-50 !cursor-not-allowed')}
className={cn('mb-2 px-4 py-3 rounded-xl bg-gray-25 border-[0.5px] border-gary-200 shadow-xs cursor-pointer', disabled && 'opacity-50 !cursor-not-allowed')}
onClick={() => !disabled && setShowDetail(true)}
>
<div className='pb-0.5 text-text-secondary system-md-semibold'>{tool.label[language]}</div>
<div className='text-text-tertiary system-xs-regular line-clamp-2' title={tool.description[language]}>{tool.description[language]}</div>
<div className='text-gray-800 font-semibold text-sm leading-5'>{tool.label[language]}</div>
<div className='mt-0.5 text-xs leading-[18px] text-gray-500 line-clamp-2' title={tool.description[language]}>{tool.description[language]}</div>
</div>
{showDetail && (
<SettingBuiltInTool

Some files were not shown because too many files have changed in this diff Show More