Compare commits
28 Commits
v4.9.3
...
v4.9.2-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6efd3318d | ||
|
|
95ffd710aa | ||
|
|
097bb97417 | ||
|
|
4faea8d2b8 | ||
|
|
7ecadb33d1 | ||
|
|
ce61bda223 | ||
|
|
8dba01da73 | ||
|
|
dcdad6fa39 | ||
|
|
11d080d521 | ||
|
|
2f954d2f3f | ||
|
|
a956fbca73 | ||
|
|
db7510c5eb | ||
|
|
b87cc353da | ||
|
|
ff85121546 | ||
|
|
79f9d83349 | ||
|
|
159bf17369 | ||
|
|
4512b23d4d | ||
|
|
5300ddf654 | ||
|
|
f1f0dfc691 | ||
|
|
e5acec8dc7 | ||
|
|
cb832b6305 | ||
|
|
ae9b8a2b8e | ||
|
|
d209255015 | ||
|
|
6eae841e4a | ||
|
|
75c1631670 | ||
|
|
97a182c7fd | ||
|
|
a0ad450032 | ||
|
|
74b36219e1 |
39
.vscode/launch.json
vendored
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "pnpm run dev",
|
||||
"cwd": "${workspaceFolder}/projects/app"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side (Edge)",
|
||||
"type": "msedge",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "pnpm run dev",
|
||||
"cwd": "${workspaceFolder}/projects/app",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithEdge",
|
||||
"killOnServerStop": true,
|
||||
"pattern": "- Local:.+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}/projects/app"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ data:
|
||||
"vectorMaxProcess": 15,
|
||||
"qaMaxProcess": 15,
|
||||
"vlmMaxProcess": 15,
|
||||
"hnswEfSearch": 100
|
||||
"pgHNSWEfSearch": 100
|
||||
},
|
||||
"llmModels": [
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 265 KiB |
@@ -25,7 +25,7 @@ weight: 707
|
||||
"qaMaxProcess": 15, // 问答拆分线程数量
|
||||
"vlmMaxProcess": 15, // 图片理解模型最大处理进程
|
||||
"tokenWorkers": 50, // Token 计算线程保持数,会持续占用内存,不能设置太大。
|
||||
"hnswEfSearch": 100, // 向量搜索参数,仅对 PG 和 OB 生效。越大,搜索越精确,但是速度越慢。设置为100,有99%+精度。
|
||||
"pgHNSWEfSearch": 100, // 向量搜索参数。越大,搜索越精确,但是速度越慢。设置为100,有99%+精度。
|
||||
"customPdfParse": { // 4.9.0 新增配置
|
||||
"url": "", // 自定义 PDF 解析服务地址
|
||||
"key": "", // 自定义 PDF 解析服务密钥
|
||||
|
||||
@@ -31,9 +31,9 @@ weight: 920
|
||||
|
||||
3 个模型代码分别为:
|
||||
|
||||
1. [https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-base](https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-base)
|
||||
2. [https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-large](https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-large)
|
||||
3. [https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-v2-m3](https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-v2-m3)
|
||||
1. [https://github.com/labring/FastGPT/tree/main/plugins/rerank-bge/bge-reranker-base](https://github.com/labring/FastGPT/tree/main/plugins/rerank-bge/bge-reranker-base)
|
||||
2. [https://github.com/labring/FastGPT/tree/main/plugins/rerank-bge/bge-reranker-large](https://github.com/labring/FastGPT/tree/main/plugins/rerank-bge/bge-reranker-large)
|
||||
3. [https://github.com/labring/FastGPT/tree/main/plugins/rerank-bge/bge-reranker-v2-m3](https://github.com/labring/FastGPT/tree/main/plugins/rerank-bge/bge-reranker-v2-m3)
|
||||
|
||||
### 3. 安装依赖
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ ChatGLM2-6B 是开源中英双语对话模型 ChatGLM-6B 的第二代版本,
|
||||
### 源码部署
|
||||
|
||||
1. 根据上面的环境配置配置好环境,具体教程自行 GPT;
|
||||
2. 下载 [python 文件](https://github.com/labring/FastGPT/blob/main/plugins/model/llm-ChatGLM2/openai_api.py)
|
||||
2. 下载 [python 文件](https://github.com/labring/FastGPT/blob/main/files/models/ChatGLM2/openai_api.py)
|
||||
3. 在命令行输入命令 `pip install -r requirements.txt`;
|
||||
4. 打开你需要启动的 py 文件,在代码的 `verify_token` 方法中配置 token,这里的 token 只是加一层验证,防止接口被人盗用;
|
||||
5. 执行命令 `python openai_api.py --model_name 16`。这里的数字根据上面的配置进行选择。
|
||||
|
||||
@@ -71,7 +71,7 @@ Mongo 数据库需要注意,需要注意在连接地址中增加 `directConnec
|
||||
- `vectorMaxProcess`: 向量生成最大进程,根据数据库和 key 的并发数来决定,通常单个 120 号,2c4g 服务器设置 10~15。
|
||||
- `qaMaxProcess`: QA 生成最大进程
|
||||
- `vlmMaxProcess`: 图片理解模型最大进程
|
||||
- `hnswEfSearch`: 向量搜索参数,仅对 PG 和 OB 生效,越大搜索精度越高但是速度越慢。
|
||||
- `pgHNSWEfSearch`: PostgreSQL vector 索引参数,越大搜索精度越高但是速度越慢,具体可看 pgvector 官方说明。
|
||||
|
||||
### 5. 运行
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ OneAPI 的语言识别接口,无法正确的识别其他模型(会始终识
|
||||
"vectorMaxProcess": 15, // 向量处理线程数量
|
||||
"qaMaxProcess": 15, // 问答拆分线程数量
|
||||
"tokenWorkers": 50, // Token 计算线程保持数,会持续占用内存,不能设置太大。
|
||||
"hnswEfSearch": 100 // 向量搜索参数,仅对 PG 和 OB 生效。越大,搜索越精确,但是速度越慢。设置为100,有99%+精度。
|
||||
"pgHNSWEfSearch": 100 // 向量搜索参数。越大,搜索越精确,但是速度越慢。设置为100,有99%+精度。
|
||||
},
|
||||
"llmModels": [
|
||||
{
|
||||
|
||||
@@ -4,38 +4,14 @@ description: 'FastGPT V4.9.2 更新说明'
|
||||
icon: 'upgrade'
|
||||
draft: false
|
||||
toc: true
|
||||
weight: 798
|
||||
weight: 799
|
||||
---
|
||||
## 更新指南
|
||||
|
||||
### 1. 做好数据库备份
|
||||
|
||||
### 2. SSO 迁移
|
||||
|
||||
使用了 SSO 或成员同步的商业版用户,并且是对接`钉钉`、`企微`的,需要迁移已有的 SSO 相关配置:
|
||||
|
||||
参考:[SSO & 外部成员同步](/docs/guide/admin/sso)中的配置进行`sso-service`的部署和配置。
|
||||
|
||||
1. 先将原商业版后台中的相关配置项复制备份出来(以企微为例,将 AppId, Secret 等复制出来)再进行镜像升级。
|
||||
2. 参考上述文档,部署 SSO 服务,配置相关的环境变量
|
||||
3. 如果原先使用企微组织架构同步的用户,升级完镜像后,需要在商业版后台切换团队模式为“同步模式”
|
||||
|
||||
### 3. 配置参数变更
|
||||
|
||||
修改`config.json`文件中`systemEnv.pgHNSWEfSearch`参数名,改成`hnswEfSearch`。
|
||||
商业版用户升级镜像后,直接在后台`系统配置-基础配置`中进行变更。
|
||||
|
||||
### 4. 更新镜像
|
||||
|
||||
- 更新 FastGPT 镜像 tag: v4.9.2
|
||||
- 更新 FastGPT 商业版镜像 tag: v4.9.2
|
||||
- Sandbox 镜像,可以不更新
|
||||
- AIProxy 镜像修改为: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.1.4
|
||||
|
||||
## 重要更新
|
||||
## 重要提示
|
||||
|
||||
- 知识库导入数据 API 变更,增加`chunkSettingMode`,`chunkSplitMode`,`indexSize`可选参数,具体可参考 [知识库导入数据 API](/docs/development/openapi/dataset) 文档。
|
||||
|
||||
|
||||
## 🚀 新增内容
|
||||
|
||||
1. 知识库分块优化:支持单独配置分块大小和索引大小,允许进行超大分块,以更大的输入 Tokens 换取完整分块。
|
||||
@@ -43,9 +19,6 @@ weight: 798
|
||||
3. 外部变量改名:自定义变量。 并且支持在测试时调试,在分享链接中,该变量直接隐藏。
|
||||
4. 集合同步时,支持同步修改标题。
|
||||
5. 团队成员管理重构,抽离主流 IM SSO(企微、飞书、钉钉),并支持通过自定义 SSO 接入 FastGPT。同时完善与外部系统的成员同步。
|
||||
6. 支持 `oceanbase` 向量数据库。填写环境变量`OCEANBASE_URL`即可。
|
||||
7. 基于 mistral-ocr 的 PDF 解析示例。
|
||||
8. 基于 miner-u 的 PDF 解析示例。
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
@@ -57,9 +30,7 @@ weight: 798
|
||||
6. 工作流节点数组字符串类型,自动适配 string 输入。
|
||||
7. 工作流节点数组类型,自动进行 JSON parse 解析 string 输入。
|
||||
8. AI proxy 日志优化,去除重试失败的日志,仅保留最后一份错误日志。
|
||||
9. 个人信息和通知展示优化。
|
||||
10. 模型测试 loading 动画优化。
|
||||
11. 分块算法小调整:
|
||||
9. 分块算法小调整:
|
||||
* 跨处理符号之间连续性更强。
|
||||
* 代码块分割时,用 LLM 模型上下文作为分块大小,尽可能保证代码块完整性。
|
||||
* 表格分割时,用 LLM 模型上下文作为分块大小,尽可能保证表格完整性。
|
||||
|
||||
@@ -1,579 +0,0 @@
|
||||
---
|
||||
title: 'SSO & 外部成员同步'
|
||||
description: 'FastGPT 外部成员系统接入设计与配置'
|
||||
icon: ''
|
||||
draft: false
|
||||
toc: true
|
||||
weight: 707
|
||||
---
|
||||
|
||||
如果你不需要用到 SSO/成员同步功能,或者是只需要用 Github、google、microsoft、公众号的快速登录,可以跳过本章节。本章适合需要接入自己的成员系统或主流 办公IM 的用户。
|
||||
|
||||
## 介绍
|
||||
|
||||
为了方便地接入**外部成员系统**,FastGPT 提供一套接入外部系统的**标准接口**,以及一个 FastGPT-SSO-Service 镜像作为**适配器**。
|
||||
|
||||
通过这套标注接口,你可以可以实现:
|
||||
|
||||
1. SSO 登录。从外部系统回调后,在 FastGPT 中创建一个用户。
|
||||
2. 成员和组织架构同步(下面都简称成员同步)。
|
||||
|
||||
**原理**
|
||||
|
||||
FastGPT-pro 中,有一套标准的SSO 和成员同步接口,系统会根据这套接口进行 SSO 和成员同步操作。
|
||||
|
||||
FastGPT-SSO-Service 是为了聚合不同来源的 SSO 和成员同步接口,将他们转成 fastgpt-pro 可识别的接口。
|
||||
|
||||

|
||||
|
||||
## 系统配置教程
|
||||
|
||||
### 1. 部署 SSO-service 镜像
|
||||
|
||||
使用 docker-compose 部署:
|
||||
|
||||
```yaml
|
||||
fastgpt-sso:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sso-service:v4.9.0
|
||||
container_name: fastgpt-sso
|
||||
restart: always
|
||||
networks:
|
||||
- fastgpt
|
||||
environment:
|
||||
- SSO_PROVIDER=example
|
||||
- AUTH_TOKEN=xxxxx # 鉴权信息,fastgpt-pro 会用到。
|
||||
# 具体对接提供商的环境变量。
|
||||
```
|
||||
|
||||
根据不同的提供商,你需要配置不同的环境变量,下面是内置的通用协议/IM:
|
||||
|
||||
{{< table "table-hover table-striped-columns" >}}
|
||||
| 协议/功能 | SSO | 成员同步支持 |
|
||||
|----------------|----------|--------------|
|
||||
| 飞书 | 是 | 是 |
|
||||
| 企业微信 | 是 | 是 |
|
||||
| 钉钉 | 是 | 否 |
|
||||
| Saml2.0 | 是 | 否 |
|
||||
| Oauth2.0 | 是 | 否 |
|
||||
{{< /table >}}
|
||||
|
||||
### 2. 配置 fastgpt-pro
|
||||
|
||||
#### 1. 配置环境变量
|
||||
|
||||
环境变量中的 `EXTERNAL_USER_SERVICE_BASE_URL` 为内网地址,例如上述例子中的配置,环境变量应该设置为
|
||||
|
||||
```yaml
|
||||
EXTERNAL_USER_SERVICE_BASE_URL=http://fastgpt-sso:3000
|
||||
EXTERNAL_USER_SERVICE_AUTH_TOKEN=xxxxx
|
||||
```
|
||||
|
||||
#### 2. 在商业版后台配置按钮文字,图标等。
|
||||
|
||||
{{< table "table-hover table-striped-columns" >}}
|
||||
| <div style="text-align:center">企业微信</div> | <div style="text-align:center">钉钉</div> | <div style="text-align:center">飞书</div> |
|
||||
|-----------|-----------------|--------------|
|
||||
|  |  |  |
|
||||
{{< /table >}}
|
||||
|
||||
#### 3. 开启成员同步(可选)
|
||||
|
||||
如果需要同步外部系统的成员,可以选择开启成员同步。团队模式具体可参考:[团队模式说明文档](/docs/guide/admin/teamMode)
|
||||
|
||||

|
||||
|
||||
#### 4. 可选配置
|
||||
|
||||
1. 自动定时成员同步
|
||||
|
||||
设置 fastgpt-pro 环境变量则可开启自动成员同步
|
||||
|
||||
```bash
|
||||
SYNC_MEMBER_CRON="0 0 * * *" # Cron 表达式,每天 0 点执行
|
||||
```
|
||||
|
||||
## 内置的通用协议/IM 配置示例
|
||||
|
||||
### 飞书
|
||||
|
||||
#### 1. 参数获取
|
||||
|
||||
App ID和App Secret
|
||||
|
||||
进入开发者后台,点击企业自建应用,在凭证与基础信息页面查看应用凭证。
|
||||
|
||||

|
||||
|
||||
#### 2. 权限配置
|
||||
|
||||
进入开发者后台,点击企业自建应用,在开发配置的权限管理页面开通权限。
|
||||
|
||||

|
||||
|
||||
对于开通用户SSO登录而言,开启用户身份权限的以下内容
|
||||
|
||||
1. ***获取通讯录基本信息***
|
||||
2. ***获取用户基本信息***
|
||||
3. ***获取用户邮箱信息***
|
||||
4. ***获取用户 user ID***
|
||||
|
||||
对于开启企业同步相关内容而言,开启身份权限的内容与上面一致,但要注意是开启应用权限
|
||||
|
||||
#### 3. 重定向URL
|
||||
|
||||
进入开发者后台,点击企业自建应用,在开发配置的安全设置中设置重定向URL
|
||||

|
||||
|
||||
#### 4. yml 配置示例
|
||||
|
||||
```bash
|
||||
fastgpt-sso:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sso-service:v4.9.0
|
||||
container_name: fastgpt-sso
|
||||
restart: always
|
||||
networks:
|
||||
- fastgpt
|
||||
environment:
|
||||
- SSO_PROVIDER=example
|
||||
- AUTH_TOKEN=xxxxx
|
||||
# 飞书 - feishu -如果是私有化部署,这里的配置前缀可能会有变化
|
||||
- SSO_PROVIDER=feishu
|
||||
# oauth 接口(公开的飞书不用改)
|
||||
- SSO_TARGET_URL=https://accounts.feishu.cn/open-apis/authen/v1/authorize
|
||||
# 获取token 接口(公开的飞书不用改)
|
||||
- FEISHU_TOKEN_URL=https://open.feishu.cn/open-apis/authen/v2/oauth/token
|
||||
# 获取用户信息接口(公开的飞书不用改)
|
||||
- FEISHU_GET_USER_INFO_URL=https://open.feishu.cn/open-apis/authen/v1/user_info
|
||||
# 重定向地址,因为飞书获取用户信息要校验所以需要填
|
||||
- FEISHU_REDIRECT_URI=xxx
|
||||
#飞书APP的应用ID,一般以cli开头
|
||||
- FEISHU_APP_ID=xxx
|
||||
#飞书APP的应用密钥
|
||||
- FEISHU_APP_SECRET=xxx
|
||||
```
|
||||
|
||||
### 钉钉
|
||||
|
||||
#### 1. 参数获取
|
||||
|
||||
CLIENT_ID 与 CLIENT_SECRET
|
||||
|
||||
进入钉钉开放平台,点击应用开发,选择自己的应用进入,记录在凭证与基础信息页面下的Client ID与Client secret。
|
||||

|
||||
|
||||
#### 2. 权限配置
|
||||
|
||||
进入钉钉开放平台,点击应用开发,选择自己的应用进入,在开发配置的权限管理页面操作,需要开通的权限包括:
|
||||
|
||||
1. ***个人手机号信息***
|
||||
2. ***通讯录个人信息读权限***
|
||||
3. ***获取钉钉开放接口用户访问凭证的基础权限***
|
||||
|
||||
#### 3. 重定向URL
|
||||
|
||||
进入钉钉开放平台,点击应用开发,选择自己的应用进入,在开发配置的安全设置页面操作
|
||||
需要填写的内容有两个:
|
||||
|
||||
1. 服务器出口IP (调用钉钉服务端API的服务器IP列表)
|
||||
2. 重定向URL(回调域名)
|
||||
|
||||
#### 4. yml 配置示例
|
||||
|
||||
```bash
|
||||
fastgpt-sso:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sso-service:v4.9.0
|
||||
container_name: fastgpt-sso
|
||||
restart: always
|
||||
networks:
|
||||
- fastgpt
|
||||
environment:
|
||||
- SSO_PROVIDER=dingtalk
|
||||
- AUTH_TOKEN=xxxxx
|
||||
#oauth 接口
|
||||
- SSO_TARGET_URL=https://login.dingtalk.com/oauth2/auth
|
||||
#获取token 接口
|
||||
- DINGTALK_TOKEN_URL=https://api.dingtalk.com/v1.0/oauth2/userAccessToken
|
||||
#获取用户信息接口
|
||||
- DINGTALK_GET_USER_INFO_URL=https://oapi.dingtalk.com/v1.0/contact/users/me
|
||||
#钉钉APP的应用ID
|
||||
- DINGTALK_CLIENT_ID=xxx
|
||||
#钉钉APP的应用密钥
|
||||
- DINGTALK_CLIENT_SECRET=xxx
|
||||
```
|
||||
|
||||
### 企业微信
|
||||
|
||||
#### 1. 参数获取
|
||||
|
||||
1. 企业的 CorpID
|
||||
|
||||
a. 使用管理员账号登陆企业微信管理后台 `https://work.weixin.qq.com/wework_admin/loginpage_wx`
|
||||
|
||||
b. 点击 【我的企业】 页面,查看企业的 **企业ID**
|
||||
|
||||

|
||||
|
||||
2. 创建一个供 FastGPT 使用的内部应用:
|
||||
|
||||
a. 获取应用的 AgentID 和 Secret
|
||||
|
||||
b. 保证这个应用的可见范围为全部(也就是根部门)
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
3. 一个域名。并且要求:
|
||||
|
||||
a. 解析到可公网访问的服务器上
|
||||
|
||||
b. 可以在该服务的根目录地址上挂载静态文件(以便进行域名归属认证 ,按照配置处的提示进行操作,只需要挂载一个静态文件,认证后可以删除)
|
||||
|
||||
c. 配置网页授权,JS-SDK以及企业微信授权登陆
|
||||
|
||||
d. 可以在【企业微信授权登陆】页面下方设置“在工作台隐藏应用”
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
4. 获取 “通讯录同步助手” secret
|
||||
|
||||
获取通讯录,组织成员 ID 需要使用 “通讯录同步助手” secret
|
||||
|
||||
【安全与管理】-- 【管理工具】 -- 【通讯录同步】
|
||||
|
||||

|
||||
|
||||
5. 开启接口同步
|
||||
|
||||
6. 获取 Secret
|
||||
|
||||
7. 配置企业可信 IP
|
||||
|
||||

|
||||
|
||||
#### 2. yml 配置示例
|
||||
|
||||
```bash
|
||||
fastgpt-sso:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sso-service:v4.9.0
|
||||
container_name: fastgpt-sso
|
||||
restart: always
|
||||
networks:
|
||||
- fastgpt
|
||||
environment:
|
||||
- AUTH_TOKEN=xxxxx
|
||||
- SSO_PROVIDER=wecom
|
||||
# oauth 接口,在企微终端使用
|
||||
- WECOM_TARGET_URL_OAUTH=https://open.weixin.qq.com/connect/oauth2/authorize
|
||||
# sso 接口,扫码
|
||||
- WECOM_TARGET_URL_SSO=https://login.work.weixin.qq.com/wwlogin/sso/login
|
||||
# 获取用户id(只能拿id)
|
||||
- WECOM_GET_USER_ID_URL=https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo
|
||||
# 获取用户详细信息(除了名字都有)
|
||||
- WECOM_GET_USER_INFO_URL=https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail
|
||||
# 获取用户信息(有名字,没其他信息)
|
||||
- WECOM_GET_USER_NAME_URL=https://qyapi.weixin.qq.com/cgi-bin/user/get
|
||||
# 获取组织 id 列表
|
||||
- WECOM_GET_DEPARTMENT_LIST_URL=https://qyapi.weixin.qq.com/cgi-bin/department/list
|
||||
# 获取用户 id 列表
|
||||
- WECOM_GET_USER_LIST_URL=https://qyapi.weixin.qq.com/cgi-bin/user/list_id
|
||||
# 企微 CorpId
|
||||
- WECOM_CORPID=
|
||||
# 企微 App 的 AgentId 一般是 1000xxx
|
||||
- WECOM_AGENTID=
|
||||
# 企微 App 的 Secret
|
||||
- WECOM_APP_SECRET=
|
||||
# 通讯录同步助手的 Secret
|
||||
- WECOM_SYNC_SECRET=
|
||||
```
|
||||
|
||||
### 标准 OAuth2.0
|
||||
|
||||
#### 参数需求
|
||||
|
||||
我们提供一套标准的 OAuth2.0 接入流程。需要三个地址:
|
||||
|
||||
1. 登陆鉴权地址(登陆后将 code 传入 redirect_uri)
|
||||
- 需要将地址完整写好,除了 redirect_uri 以外(会自动补全)
|
||||
2. 获取 access_token 的地址,请求为 GET 方法,参数 code
|
||||
|
||||
```bash
|
||||
http://example.com/oauth/access_token?code=xxxx
|
||||
```
|
||||
|
||||
3. 获取用户信息的地址
|
||||
|
||||
```bash
|
||||
http://example.com/oauth/user_info
|
||||
|
||||
```
|
||||
|
||||
#### 配置示例
|
||||
|
||||
```bash
|
||||
fastgpt-sso:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sso-service:v4.9.0
|
||||
container_name: fastgpt-sso
|
||||
restart: always
|
||||
networks:
|
||||
- fastgpt
|
||||
environment:
|
||||
# OAuth2.0
|
||||
- AUTH_TOKEN=xxxxx
|
||||
- SSO_PROVIDER=oauth2
|
||||
# OAuth2 重定向地址
|
||||
- OAUTH2_AUTHORIZE_URL=
|
||||
# OAuth2 获取 AccessToken 地址
|
||||
- OAUTH2_TOKEN_URL=
|
||||
# OAuth2 获取用户信息地址
|
||||
- OAUTH2_USER_INFO_URL=
|
||||
# OAuth2 用户名字段映射(必填)
|
||||
- OAUTH2_USERNAME_MAP=
|
||||
# OAuth2 头像字段映射(选填)
|
||||
- OAUTH2_AVATAR_MAP=
|
||||
# OAuth2 成员名字段映射(选填)
|
||||
- OAUTH2_MEMBER_NAME_MAP=
|
||||
# OAuth2 联系方式字段映射(选填)
|
||||
- OAUTH2_CONTACT_MAP=
|
||||
```
|
||||
|
||||
## 标准接口文档
|
||||
|
||||
以下是 FastGPT-pro 中,SSO 和成员同步的标准接口文档,如果需要对接非标准系统,可以参考该章节进行开发。
|
||||
|
||||

|
||||
|
||||
FastGPT 提供如下标准接口支持:
|
||||
|
||||
1. https://example.com/login/oauth/getAuthURL 获取鉴权重定向地址
|
||||
2. https://example.com/login/oauth/getUserInfo?code=xxxxx 消费 code,换取用户信息
|
||||
3. https://example.com/org/list 获取组织列表
|
||||
4. https://example.com/user/list 获取成员列表
|
||||
|
||||
### 获取 SSO 登录重定向地址
|
||||
|
||||
返回一个重定向登录地址,fastgpt 会自动重定向到该地址。redirect_uri 会自动拼接到该地址的 query中。
|
||||
|
||||
{{< tabs tabTotal="2" >}}
|
||||
{{< tab tabName="请求示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
```bash
|
||||
curl -X GET "https://redict.example/login/oauth/getAuthURL?redirect_uri=xxx&state=xxxx" \
|
||||
-H "Authorization: Bearer your_token_here" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="响应示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
成功:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"authURL": "https://example.com/somepath/login/oauth?redirect_uri=https%3A%2F%2Ffastgpt.cn%2Flogin%2Fprovider%0A"
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误信息",
|
||||
"authURL": ""
|
||||
}
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
|
||||
### SSO 获取用户信息
|
||||
|
||||
该接口接受一个 code 参数作为鉴权,消费 code 返回用户信息。
|
||||
|
||||
{{< tabs tabTotal="2" >}}
|
||||
{{< tab tabName="请求示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
```bash
|
||||
curl -X GET "https://oauth.example/login/oauth/getUserInfo?code=xxxxxx" \
|
||||
-H "Authorization: Bearer your_token_here" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="响应示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
成功:
|
||||
```JSON
|
||||
{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"username": "fastgpt-123456789",
|
||||
"avatar": "https://example.webp",
|
||||
"contact": "+861234567890",
|
||||
"memberName": "成员名(非必填)",
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
```JSON
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误信息",
|
||||
"username": "",
|
||||
"avatar": "",
|
||||
"contact": ""
|
||||
}
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
### 获取组织
|
||||
|
||||
{{< tabs tabTotal="2" >}}
|
||||
{{< tab tabName="请求示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
```bash
|
||||
curl -X GET "https://example.com/org/list" \
|
||||
-H "Authorization: Bearer your_token_here" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="响应示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
⚠️注意:只能存在一个根部门。如果你的系统中存在多个根部门,需要先进行处理,加一个虚拟的根部门。返回值类型:
|
||||
|
||||
```ts
|
||||
type OrgListResponseType = {
|
||||
message?: string; // 报错信息
|
||||
success: boolean;
|
||||
orgList: {
|
||||
id: string; // 部门的唯一 id
|
||||
name: string; // 名字
|
||||
parentId: string; // parentId,如果为根部门,传空字符串。
|
||||
}[];
|
||||
}
|
||||
```
|
||||
|
||||
```JSON
|
||||
{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"orgList": [
|
||||
{
|
||||
"id": "od-125151515",
|
||||
"name": "根部门",
|
||||
"parentId": ""
|
||||
},
|
||||
{
|
||||
"id": "od-51516152",
|
||||
"name": "子部门",
|
||||
"parentId": "od-125151515"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
|
||||
### 获取成员
|
||||
|
||||
|
||||
{{< tabs tabTotal="2" >}}
|
||||
{{< tab tabName="请求示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
```bash
|
||||
curl -X GET "https://example.com/user/list" \
|
||||
-H "Authorization: Bearer your_token_here" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab tabName="响应示例" >}}
|
||||
{{< markdownify >}}
|
||||
|
||||
返回值类型:
|
||||
|
||||
```typescript
|
||||
type UserListResponseListType = {
|
||||
message?: string; // 报错信息
|
||||
success: boolean;
|
||||
userList: {
|
||||
username: string; // 唯一 id username 必须与 SSO 接口返回的用户 username 相同。并且必须携带一个前缀,例如: sync-aaaaa,和 sso 接口返回的前缀一致
|
||||
memberName?: string; // 名字,作为 tmbname
|
||||
avatar?: string;
|
||||
contact?: string; // email or phone number
|
||||
orgs?: string[]; // 人员所在组织的 ID。没有组织传 []
|
||||
}[];
|
||||
}
|
||||
```
|
||||
curl示例
|
||||
|
||||
```JSON
|
||||
{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"userList": [
|
||||
{
|
||||
"username": "fastgpt-123456789",
|
||||
"memberName": "张三",
|
||||
"avatar": "https://example.webp",
|
||||
"contact": "+861234567890",
|
||||
"orgs": ["od-125151515", "od-51516152"]
|
||||
},
|
||||
{
|
||||
"username": "fastgpt-12345678999",
|
||||
"memberName": "李四",
|
||||
"avatar": "",
|
||||
"contact": "",
|
||||
"orgs": ["od-125151515"]
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
{{< /markdownify >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
|
||||
|
||||
|
||||
## 如何对接非标准系统
|
||||
|
||||
1. 客户自己开发:按 fastgpt 提供的标准接口进行开发,并将部署后的服务地址填入 fastgpt-pro
|
||||
可以参考该模版库:[fastgpt-sso-template](https://github.com/labring/fastgpt-sso-template) 进行开发
|
||||
2. 由 fastgpt 团队定制开发:
|
||||
a. 提供系统的 SSO 文档、获取成员和组织的文档、以及外网测试地址。
|
||||
b. 在 fastgpt-sso-service 中,增加对应的 provider 和环境变量,并编写代码来对接。
|
||||
44
docSite/content/zh-cn/docs/guide/admin/sso_dingtalk.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
weight: 490
|
||||
title: '钉钉 SSO 配置'
|
||||
description: '钉钉 SSO 登录'
|
||||
icon: 'chat_bubble'
|
||||
draft: false
|
||||
images: []
|
||||
---
|
||||
|
||||
## 1. 注册钉钉应用
|
||||
|
||||
登录 [钉钉开放平台](https://open-dev.dingtalk.com/fe/app?hash=%23%2Fcorp%2Fapp#/corp/app),创建一个应用。
|
||||
|
||||

|
||||
|
||||
## 2. 配置钉钉应用安全设置
|
||||
|
||||
点击进入创建好的应用后,点开`安全设置`,配置出口 IP(服务器 IP),和重定向 URL。重定向 URL 填写逻辑:
|
||||
|
||||
`{{fastgpt 域名}}/login/provider`
|
||||
|
||||

|
||||
|
||||
## 3. 设置钉钉应用权限
|
||||
|
||||
点击进入创建好的应用后,点开`权限设置`,开放两个权限: `个人手机号信息`和`通讯录个人信息读权限`
|
||||
|
||||

|
||||
|
||||
## 4. 发布应用
|
||||
|
||||
点击进入创建好的应用后,点开`版本管理与发布`,随便创建一个新版本即可。
|
||||
|
||||
## 5. 在 FastGPT Admin 配置钉钉应用 id
|
||||
|
||||
名字都是对应上,直接填写即可。
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| |  |
|
||||
|
||||
## 6. 测试
|
||||
|
||||

|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
title: '团队模式说明文档'
|
||||
description: 'FastGPT 团队模式说明文档'
|
||||
icon: ''
|
||||
draft: false
|
||||
toc: true
|
||||
weight: 707
|
||||
---
|
||||
|
||||
## 介绍
|
||||
|
||||
目前支持的团队模式:
|
||||
|
||||
1. 多团队模式(默认模式)
|
||||
2. 单团队模式(全局只有一个团队)
|
||||
3. 成员同步模式(所有成员自外部同步)
|
||||
|
||||
<table class="table-hover table-striped-columns" style="text-align: center;">
|
||||
<tr>
|
||||
<th rowspan="2">团队模式</th>
|
||||
<th colspan="2">短信/邮箱 注册</th>
|
||||
<th colspan="2">管理员直接添加</th>
|
||||
<th colspan="2">SSO 注册</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>是否创建默认团队</th>
|
||||
<th>是否加入 Root 团队</th>
|
||||
<th>是否创建默认团队</th>
|
||||
<th>是否加入 Root 团队</th>
|
||||
<th>是否创建默认团队</th>
|
||||
<th>是否加入 Root 团队</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单团队模式</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多团队模式</td>
|
||||
<td>✅</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>同步模式</td>
|
||||
<td>❌</td>
|
||||
<td>❌</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### 多团队模式(默认模式)
|
||||
|
||||
多团队模式下,每个用户创建时默认创建以自己为所有者的默认团队。
|
||||
|
||||
### 单团队模式
|
||||
|
||||
单团队模式是 v4.9 推出的新功能。为了简化企业进行人员和资源的管理,开启单团队模式后,所有新增的用户都不再创建自己的默认团队,而是加入 root 用户所在的团队。
|
||||
|
||||
### 同步模式
|
||||
|
||||
在完成系统配置,开启同步模式的情况下,外部成员系统的成员会自动同步到 FastGPT 中。
|
||||
|
||||
具体的同步方式和规则请参考 [SSO & 外部成员同步](/docs/guide/admin/sso.md)。
|
||||
|
||||
|
||||
## 配置
|
||||
|
||||
在 `fastgpt-pro` 的`系统配置-成员配置`中,可以配置团队模式。
|
||||
|
||||

|
||||
@@ -112,7 +112,7 @@ export type SystemEnvType = {
|
||||
vectorMaxProcess: number;
|
||||
qaMaxProcess: number;
|
||||
vlmMaxProcess: number;
|
||||
hnswEfSearch: number;
|
||||
pgHNSWEfSearch: number;
|
||||
tokenWorkers: number; // token count max worker
|
||||
|
||||
oneapiUrl?: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
|
||||
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
|
||||
import { replaceVariable, valToStr } from '../../../common/string/tools';
|
||||
import { ChatCompletionChunk } from 'openai/resources';
|
||||
|
||||
export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => {
|
||||
let limit = 10;
|
||||
|
||||
@@ -5,36 +5,10 @@ import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant';
|
||||
import { WorkflowIOValueTypeEnum } from 'core/workflow/constants';
|
||||
import type { ChatCompletionMessageParam } from '../../../../ai/type';
|
||||
|
||||
type InteractiveBasicType = {
|
||||
entryNodeIds: string[];
|
||||
memoryEdges: RuntimeEdgeItemType[];
|
||||
nodeOutputs: NodeOutputItemType[];
|
||||
|
||||
toolParams?: {
|
||||
entryNodeIds: string[]; // 记录工具中,交互节点的 Id,而不是起始工作流的入口
|
||||
memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages
|
||||
toolCallId: string; // 记录对应 tool 的id,用于后续交互节点可以替换掉 tool 的 response
|
||||
};
|
||||
};
|
||||
|
||||
type InteractiveNodeType = {
|
||||
entryNodeIds?: string[];
|
||||
memoryEdges?: RuntimeEdgeItemType[];
|
||||
nodeOutputs?: NodeOutputItemType[];
|
||||
};
|
||||
|
||||
export type UserSelectOptionItemType = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
type UserSelectInteractive = InteractiveNodeType & {
|
||||
type: 'userSelect';
|
||||
params: {
|
||||
description: string;
|
||||
userSelectOptions: UserSelectOptionItemType[];
|
||||
userSelectedVal?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type UserInputFormItemType = {
|
||||
type: FlowNodeInputTypeEnum;
|
||||
@@ -54,7 +28,29 @@ export type UserInputFormItemType = {
|
||||
// select
|
||||
list?: { label: string; value: string }[];
|
||||
};
|
||||
type UserInputInteractive = InteractiveNodeType & {
|
||||
|
||||
type InteractiveBasicType = {
|
||||
entryNodeIds: string[];
|
||||
memoryEdges: RuntimeEdgeItemType[];
|
||||
nodeOutputs: NodeOutputItemType[];
|
||||
|
||||
toolParams?: {
|
||||
entryNodeIds: string[]; // 记录工具中,交互节点的 Id,而不是起始工作流的入口
|
||||
memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages
|
||||
toolCallId: string; // 记录对应 tool 的id,用于后续交互节点可以替换掉 tool 的 response
|
||||
};
|
||||
};
|
||||
|
||||
type UserSelectInteractive = {
|
||||
type: 'userSelect';
|
||||
params: {
|
||||
description: string;
|
||||
userSelectOptions: UserSelectOptionItemType[];
|
||||
userSelectedVal?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type UserInputInteractive = {
|
||||
type: 'userInput';
|
||||
params: {
|
||||
description: string;
|
||||
@@ -62,5 +58,6 @@ type UserInputInteractive = InteractiveNodeType & {
|
||||
submitted?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type InteractiveNodeResponseType = UserSelectInteractive | UserInputInteractive;
|
||||
export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType;
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
export const JS_TEMPLATE = `function main({data1, data2}){
|
||||
|
||||
return {
|
||||
result: data1,
|
||||
data2
|
||||
}
|
||||
}`;
|
||||
|
||||
export const PY_TEMPLATE = `def main(data1, data2):
|
||||
return {
|
||||
"result": data1,
|
||||
"data2": data2
|
||||
result: data1,
|
||||
data2
|
||||
}
|
||||
`;
|
||||
|
||||
export enum SandboxCodeTypeEnum {
|
||||
js = 'js',
|
||||
py = 'py'
|
||||
}
|
||||
export const SNADBOX_CODE_TEMPLATE = {
|
||||
[SandboxCodeTypeEnum.js]: JS_TEMPLATE,
|
||||
[SandboxCodeTypeEnum.py]: PY_TEMPLATE
|
||||
};
|
||||
}`;
|
||||
|
||||
@@ -68,14 +68,12 @@ export const CodeNode: FlowNodeTemplateType = {
|
||||
key: NodeInputKeyEnum.codeType,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.hidden],
|
||||
label: '',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
value: 'js'
|
||||
},
|
||||
{
|
||||
key: NodeInputKeyEnum.code,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.custom],
|
||||
label: '',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
value: JS_TEMPLATE
|
||||
}
|
||||
],
|
||||
|
||||
2
packages/global/core/workflow/type/node.d.ts
vendored
@@ -23,7 +23,6 @@ import { NextApiResponse } from 'next';
|
||||
import { AppDetailType, AppSchema } from '../../app/type';
|
||||
import { ParentIdType } from 'common/parentFolder/type';
|
||||
import { AppTypeEnum } from 'core/app/constants';
|
||||
import { WorkflowInteractiveResponseType } from '../template/system/interactive/type';
|
||||
|
||||
export type FlowNodeCommonType = {
|
||||
parentNodeId?: string;
|
||||
@@ -121,7 +120,6 @@ export type FlowNodeItemType = FlowNodeTemplateType & {
|
||||
showResult?: boolean; // show and hide result modal
|
||||
response?: ChatHistoryItemResType;
|
||||
isExpired?: boolean;
|
||||
workflowInteractiveResponse?: WorkflowInteractiveResponseType;
|
||||
};
|
||||
isFolded?: boolean;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,5 @@ export enum SendInformTemplateCodeEnum {
|
||||
RESET_PASSWORD = 'RESET_PASSWORD',
|
||||
BIND_NOTIFICATION = 'BIND_NOTIFICATION',
|
||||
LACK_OF_POINTS = 'LACK_OF_POINTS',
|
||||
CUSTOM = 'CUSTOM',
|
||||
MANAGE_RENAME = 'MANAGE_RENAME'
|
||||
CUSTOM = 'CUSTOM'
|
||||
}
|
||||
|
||||
13
packages/global/support/user/inform/type.d.ts
vendored
@@ -19,19 +19,6 @@ export type SendInform2User = SendInformProps & {
|
||||
export type UserInformSchema = {
|
||||
_id: string;
|
||||
userId: string;
|
||||
teamId?: string;
|
||||
time: Date;
|
||||
level: `${InformLevelEnum}`;
|
||||
title: string;
|
||||
content: string;
|
||||
read: boolean;
|
||||
};
|
||||
|
||||
export type UserInformType = {
|
||||
_id: string;
|
||||
userId: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
time: Date;
|
||||
level: `${InformLevelEnum}`;
|
||||
title: string;
|
||||
|
||||
@@ -2,6 +2,5 @@ export const DatasetVectorDbName = 'fastgpt';
|
||||
export const DatasetVectorTableName = 'modeldata';
|
||||
|
||||
export const PG_ADDRESS = process.env.PG_URL;
|
||||
export const OCEANBASE_ADDRESS = process.env.OCEANBASE_URL;
|
||||
export const MILVUS_ADDRESS = process.env.MILVUS_ADDRESS;
|
||||
export const MILVUS_TOKEN = process.env.MILVUS_TOKEN;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
/* vector crud */
|
||||
import { PgVectorCtrl } from './pg/class';
|
||||
import { ObVectorCtrl } from './oceanbase/class';
|
||||
import { getVectorsByText } from '../../core/ai/embedding';
|
||||
import { InsertVectorProps } from './controller.d';
|
||||
import { EmbeddingModelItemType } from '@fastgpt/global/core/ai/model.d';
|
||||
import { MILVUS_ADDRESS, PG_ADDRESS, OCEANBASE_ADDRESS } from './constants';
|
||||
import { MILVUS_ADDRESS, PG_ADDRESS } from './constants';
|
||||
import { MilvusCtrl } from './milvus/class';
|
||||
|
||||
const getVectorObj = () => {
|
||||
if (PG_ADDRESS) return new PgVectorCtrl();
|
||||
if (OCEANBASE_ADDRESS) return new ObVectorCtrl();
|
||||
if (MILVUS_ADDRESS) return new MilvusCtrl();
|
||||
|
||||
return new PgVectorCtrl();
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
/* oceanbase vector crud */
|
||||
import { DatasetVectorTableName } from '../constants';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
import { ObClient } from './index';
|
||||
import { RowDataPacket, ResultSetHeader } from 'mysql2/promise';
|
||||
import {
|
||||
DelDatasetVectorCtrlProps,
|
||||
EmbeddingRecallCtrlProps,
|
||||
EmbeddingRecallResponse,
|
||||
InsertVectorControllerProps
|
||||
} from '../controller.d';
|
||||
import dayjs from 'dayjs';
|
||||
import { addLog } from '../../system/log';
|
||||
|
||||
export class ObVectorCtrl {
|
||||
constructor() {}
|
||||
init = async () => {
|
||||
try {
|
||||
await ObClient.query(`
|
||||
CREATE TABLE IF NOT EXISTS ${DatasetVectorTableName} (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
vector VECTOR(1536) NOT NULL,
|
||||
team_id VARCHAR(50) NOT NULL,
|
||||
dataset_id VARCHAR(50) NOT NULL,
|
||||
collection_id VARCHAR(50) NOT NULL,
|
||||
createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await ObClient.query(
|
||||
`CREATE VECTOR INDEX IF NOT EXISTS vector_index ON ${DatasetVectorTableName}(vector) WITH (distance=inner_product, type=hnsw, m=32, ef_construction=128);`
|
||||
);
|
||||
await ObClient.query(
|
||||
`CREATE INDEX IF NOT EXISTS team_dataset_collection_index ON ${DatasetVectorTableName}(team_id, dataset_id, collection_id);`
|
||||
);
|
||||
await ObClient.query(
|
||||
`CREATE INDEX IF NOT EXISTS create_time_index ON ${DatasetVectorTableName}(createtime);`
|
||||
);
|
||||
|
||||
addLog.info('init oceanbase successful');
|
||||
} catch (error) {
|
||||
addLog.error('init oceanbase error', error);
|
||||
}
|
||||
};
|
||||
insert = async (props: InsertVectorControllerProps): Promise<{ insertId: string }> => {
|
||||
const { teamId, datasetId, collectionId, vector, retry = 3 } = props;
|
||||
|
||||
try {
|
||||
const { rowCount, rows } = await ObClient.insert(DatasetVectorTableName, {
|
||||
values: [
|
||||
[
|
||||
{ key: 'vector', value: `[${vector}]` },
|
||||
{ key: 'team_id', value: String(teamId) },
|
||||
{ key: 'dataset_id', value: String(datasetId) },
|
||||
{ key: 'collection_id', value: String(collectionId) }
|
||||
]
|
||||
]
|
||||
});
|
||||
|
||||
if (rowCount === 0) {
|
||||
return Promise.reject('insertDatasetData: no insert');
|
||||
}
|
||||
|
||||
return {
|
||||
insertId: rows[0].id
|
||||
};
|
||||
} catch (error) {
|
||||
if (retry <= 0) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
await delay(500);
|
||||
return this.insert({
|
||||
...props,
|
||||
retry: retry - 1
|
||||
});
|
||||
}
|
||||
};
|
||||
delete = async (props: DelDatasetVectorCtrlProps): Promise<any> => {
|
||||
const { teamId, retry = 2 } = props;
|
||||
|
||||
const teamIdWhere = `team_id='${String(teamId)}' AND`;
|
||||
|
||||
const where = await (() => {
|
||||
if ('id' in props && props.id) return `${teamIdWhere} id=${props.id}`;
|
||||
|
||||
if ('datasetIds' in props && props.datasetIds) {
|
||||
const datasetIdWhere = `dataset_id IN (${props.datasetIds
|
||||
.map((id) => `'${String(id)}'`)
|
||||
.join(',')})`;
|
||||
|
||||
if ('collectionIds' in props && props.collectionIds) {
|
||||
return `${teamIdWhere} ${datasetIdWhere} AND collection_id IN (${props.collectionIds
|
||||
.map((id) => `'${String(id)}'`)
|
||||
.join(',')})`;
|
||||
}
|
||||
|
||||
return `${teamIdWhere} ${datasetIdWhere}`;
|
||||
}
|
||||
|
||||
if ('idList' in props && Array.isArray(props.idList)) {
|
||||
if (props.idList.length === 0) return;
|
||||
return `${teamIdWhere} id IN (${props.idList.map((id) => String(id)).join(',')})`;
|
||||
}
|
||||
return Promise.reject('deleteDatasetData: no where');
|
||||
})();
|
||||
|
||||
if (!where) return;
|
||||
|
||||
try {
|
||||
await ObClient.delete(DatasetVectorTableName, {
|
||||
where: [where]
|
||||
});
|
||||
} catch (error) {
|
||||
if (retry <= 0) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
await delay(500);
|
||||
return this.delete({
|
||||
...props,
|
||||
retry: retry - 1
|
||||
});
|
||||
}
|
||||
};
|
||||
embRecall = async (props: EmbeddingRecallCtrlProps): Promise<EmbeddingRecallResponse> => {
|
||||
const {
|
||||
teamId,
|
||||
datasetIds,
|
||||
vector,
|
||||
limit,
|
||||
forbidCollectionIdList,
|
||||
filterCollectionIdList,
|
||||
retry = 2
|
||||
} = props;
|
||||
|
||||
// Get forbid collection
|
||||
const formatForbidCollectionIdList = (() => {
|
||||
if (!filterCollectionIdList) return forbidCollectionIdList;
|
||||
const list = forbidCollectionIdList
|
||||
.map((id) => String(id))
|
||||
.filter((id) => !filterCollectionIdList.includes(id));
|
||||
return list;
|
||||
})();
|
||||
const forbidCollectionSql =
|
||||
formatForbidCollectionIdList.length > 0
|
||||
? `AND collection_id NOT IN (${formatForbidCollectionIdList.map((id) => `'${id}'`).join(',')})`
|
||||
: '';
|
||||
|
||||
// Filter by collectionId
|
||||
const formatFilterCollectionId = (() => {
|
||||
if (!filterCollectionIdList) return;
|
||||
|
||||
return filterCollectionIdList
|
||||
.map((id) => String(id))
|
||||
.filter((id) => !forbidCollectionIdList.includes(id));
|
||||
})();
|
||||
const filterCollectionIdSql = formatFilterCollectionId
|
||||
? `AND collection_id IN (${formatFilterCollectionId.map((id) => `'${id}'`).join(',')})`
|
||||
: '';
|
||||
// Empty data
|
||||
if (formatFilterCollectionId && formatFilterCollectionId.length === 0) {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await ObClient.query<
|
||||
({
|
||||
id: string;
|
||||
collection_id: string;
|
||||
score: number;
|
||||
} & RowDataPacket)[][]
|
||||
>(
|
||||
`BEGIN;
|
||||
SET ob_hnsw_ef_search = ${global.systemEnv?.hnswEfSearch || 100};
|
||||
SELECT id, collection_id, inner_product(vector, [${vector}]) AS score
|
||||
FROM ${DatasetVectorTableName}
|
||||
WHERE team_id='${teamId}'
|
||||
AND dataset_id IN (${datasetIds.map((id) => `'${String(id)}'`).join(',')})
|
||||
${filterCollectionIdSql}
|
||||
${forbidCollectionSql}
|
||||
ORDER BY score desc APPROXIMATE LIMIT ${limit};
|
||||
COMMIT;`
|
||||
).then(([rows]) => rows[2]);
|
||||
|
||||
return {
|
||||
results: rows.map((item) => ({
|
||||
id: String(item.id),
|
||||
collectionId: item.collection_id,
|
||||
score: item.score
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
if (retry <= 0) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return this.embRecall({
|
||||
...props,
|
||||
retry: retry - 1
|
||||
});
|
||||
}
|
||||
};
|
||||
getVectorDataByTime = async (start: Date, end: Date) => {
|
||||
const rows = await ObClient.query<
|
||||
({
|
||||
id: string;
|
||||
team_id: string;
|
||||
dataset_id: string;
|
||||
} & RowDataPacket)[]
|
||||
>(
|
||||
`SELECT id, team_id, dataset_id
|
||||
FROM ${DatasetVectorTableName}
|
||||
WHERE createtime BETWEEN '${dayjs(start).format('YYYY-MM-DD HH:mm:ss')}' AND '${dayjs(
|
||||
end
|
||||
).format('YYYY-MM-DD HH:mm:ss')}';
|
||||
`
|
||||
).then(([rows]) => rows);
|
||||
|
||||
return rows.map((item) => ({
|
||||
id: String(item.id),
|
||||
teamId: item.team_id,
|
||||
datasetId: item.dataset_id
|
||||
}));
|
||||
};
|
||||
getVectorCountByTeamId = async (teamId: string) => {
|
||||
const total = await ObClient.count(DatasetVectorTableName, {
|
||||
where: [['team_id', String(teamId)]]
|
||||
});
|
||||
|
||||
return total;
|
||||
};
|
||||
getVectorCountByDatasetId = async (teamId: string, datasetId: string) => {
|
||||
const total = await ObClient.count(DatasetVectorTableName, {
|
||||
where: [['team_id', String(teamId)], 'and', ['dataset_id', String(datasetId)]]
|
||||
});
|
||||
|
||||
return total;
|
||||
};
|
||||
getVectorCountByCollectionId = async (
|
||||
teamId: string,
|
||||
datasetId: string,
|
||||
collectionId: string
|
||||
) => {
|
||||
const total = await ObClient.count(DatasetVectorTableName, {
|
||||
where: [
|
||||
['team_id', String(teamId)],
|
||||
'and',
|
||||
['dataset_id', String(datasetId)],
|
||||
'and',
|
||||
['collection_id', String(collectionId)]
|
||||
]
|
||||
});
|
||||
|
||||
return total;
|
||||
};
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import mysql, { Pool, QueryResult, RowDataPacket, ResultSetHeader } from 'mysql2/promise';
|
||||
import { addLog } from '../../system/log';
|
||||
import { OCEANBASE_ADDRESS } from '../constants';
|
||||
|
||||
export const getClient = async (): Promise<Pool> => {
|
||||
if (!OCEANBASE_ADDRESS) {
|
||||
return Promise.reject('OCEANBASE_ADDRESS is not set');
|
||||
}
|
||||
|
||||
if (global.obClient) {
|
||||
return global.obClient;
|
||||
}
|
||||
|
||||
global.obClient = mysql.createPool({
|
||||
uri: OCEANBASE_ADDRESS,
|
||||
waitForConnections: true,
|
||||
connectionLimit: Number(process.env.DB_MAX_LINK || 20),
|
||||
connectTimeout: 20000,
|
||||
idleTimeout: 60000,
|
||||
queueLimit: 0,
|
||||
enableKeepAlive: true,
|
||||
keepAliveInitialDelay: 0
|
||||
});
|
||||
|
||||
addLog.info(`oceanbase connected`);
|
||||
|
||||
return global.obClient;
|
||||
};
|
||||
|
||||
type WhereProps = (string | [string, string | number])[];
|
||||
type GetProps = {
|
||||
fields?: string[];
|
||||
where?: WhereProps;
|
||||
order?: { field: string; mode: 'DESC' | 'ASC' | string }[];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
type DeleteProps = {
|
||||
where: WhereProps;
|
||||
};
|
||||
|
||||
type ValuesProps = { key: string; value?: string | number }[];
|
||||
type UpdateProps = {
|
||||
values: ValuesProps;
|
||||
where: WhereProps;
|
||||
};
|
||||
type InsertProps = {
|
||||
values: ValuesProps[];
|
||||
};
|
||||
|
||||
class ObClass {
|
||||
private getWhereStr(where?: WhereProps) {
|
||||
return where
|
||||
? `WHERE ${where
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
const val = typeof item[1] === 'number' ? item[1] : `'${String(item[1])}'`;
|
||||
return `${item[0]}=${val}`;
|
||||
})
|
||||
.join(' ')}`
|
||||
: '';
|
||||
}
|
||||
private getUpdateValStr(values: ValuesProps) {
|
||||
return values
|
||||
.map((item) => {
|
||||
const val =
|
||||
typeof item.value === 'number'
|
||||
? item.value
|
||||
: `'${String(item.value).replace(/\'/g, '"')}'`;
|
||||
|
||||
return `${item.key}=${val}`;
|
||||
})
|
||||
.join(',');
|
||||
}
|
||||
private getInsertValStr(values: ValuesProps[]) {
|
||||
return values
|
||||
.map(
|
||||
(items) =>
|
||||
`(${items
|
||||
.map((item) =>
|
||||
typeof item.value === 'number'
|
||||
? item.value
|
||||
: `'${String(item.value).replace(/\'/g, '"')}'`
|
||||
)
|
||||
.join(',')})`
|
||||
)
|
||||
.join(',');
|
||||
}
|
||||
async select<T extends QueryResult = any>(table: string, props: GetProps) {
|
||||
const sql = `SELECT ${
|
||||
!props.fields || props.fields?.length === 0 ? '*' : props.fields?.join(',')
|
||||
}
|
||||
FROM ${table}
|
||||
${this.getWhereStr(props.where)}
|
||||
${
|
||||
props.order
|
||||
? `ORDER BY ${props.order.map((item) => `${item.field} ${item.mode}`).join(',')}`
|
||||
: ''
|
||||
}
|
||||
LIMIT ${props.limit || 10} OFFSET ${props.offset || 0}
|
||||
`;
|
||||
|
||||
const client = await getClient();
|
||||
return client.query<T>(sql);
|
||||
}
|
||||
async count(table: string, props: GetProps) {
|
||||
const sql = `SELECT COUNT(${props?.fields?.[0] || '*'})
|
||||
FROM ${table}
|
||||
${this.getWhereStr(props.where)}
|
||||
`;
|
||||
|
||||
const client = await getClient();
|
||||
return client
|
||||
.query<({ count: number } & RowDataPacket)[]>(sql)
|
||||
.then(([rows]) => Number(rows[0]?.count || 0));
|
||||
}
|
||||
async delete(table: string, props: DeleteProps) {
|
||||
const sql = `DELETE FROM ${table} ${this.getWhereStr(props.where)}`;
|
||||
const client = await getClient();
|
||||
return client.query(sql);
|
||||
}
|
||||
async update(table: string, props: UpdateProps) {
|
||||
if (props.values.length === 0) {
|
||||
return {
|
||||
rowCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
const sql = `UPDATE ${table} SET ${this.getUpdateValStr(props.values)} ${this.getWhereStr(
|
||||
props.where
|
||||
)}`;
|
||||
const client = await getClient();
|
||||
return client.query(sql);
|
||||
}
|
||||
async insert(table: string, props: InsertProps) {
|
||||
if (props.values.length === 0) {
|
||||
return {
|
||||
rowCount: 0,
|
||||
rows: []
|
||||
};
|
||||
}
|
||||
|
||||
const fields = props.values[0].map((item) => item.key).join(',');
|
||||
const sql = `INSERT INTO ${table} (${fields}) VALUES ${this.getInsertValStr(props.values)}`;
|
||||
|
||||
const client = await getClient();
|
||||
return client.query<ResultSetHeader>(sql).then(([result]) => {
|
||||
return {
|
||||
rowCount: result.affectedRows,
|
||||
rows: [{ id: String(result.insertId) }]
|
||||
};
|
||||
});
|
||||
}
|
||||
async query<T extends QueryResult = any>(sql: string) {
|
||||
const client = await getClient();
|
||||
const start = Date.now();
|
||||
return client.query<T>(sql).then((res) => {
|
||||
const time = Date.now() - start;
|
||||
|
||||
if (time > 300) {
|
||||
addLog.warn(`oceanbase query time: ${time}ms, sql: ${sql}`);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const ObClient = new ObClass();
|
||||
export const Oceanbase = global.obClient;
|
||||
@@ -187,7 +187,7 @@ export class PgVectorCtrl {
|
||||
try {
|
||||
const results: any = await PgClient.query(
|
||||
`BEGIN;
|
||||
SET LOCAL hnsw.ef_search = ${global.systemEnv?.hnswEfSearch || 100};
|
||||
SET LOCAL hnsw.ef_search = ${global.systemEnv?.pgHNSWEfSearch || 100};
|
||||
SET LOCAL hnsw.iterative_scan = relaxed_order;
|
||||
WITH relaxed_results AS MATERIALIZED (
|
||||
select id, collection_id, vector <#> '[${vector}]' AS score
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { Pool } from 'pg';
|
||||
import { Pool as MysqlPool } from 'mysql2/promise';
|
||||
import { MilvusClient } from '@zilliz/milvus2-sdk-node';
|
||||
|
||||
declare global {
|
||||
var pgClient: Pool | null;
|
||||
var obClient: MysqlPool | null;
|
||||
var milvusClient: MilvusClient | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -296,30 +296,6 @@
|
||||
"showStopSign": true,
|
||||
"responseFormatList": ["text", "json_object"]
|
||||
},
|
||||
{
|
||||
"model": "qwen-long",
|
||||
"name": "qwen-long",
|
||||
"maxContext": 100000,
|
||||
"maxResponse": 6000,
|
||||
"quoteMaxToken": 10000,
|
||||
"maxTemperature": 1,
|
||||
"vision": false,
|
||||
"toolChoice": false,
|
||||
"functionCall": false,
|
||||
"defaultSystemChatPrompt": "",
|
||||
"datasetProcess": false,
|
||||
"usedInClassify": false,
|
||||
"customCQPrompt": "",
|
||||
"usedInExtractFields": false,
|
||||
"usedInQueryExtension": false,
|
||||
"customExtractPrompt": "",
|
||||
"usedInToolCall": false,
|
||||
"defaultConfig": {},
|
||||
"fieldMap": {},
|
||||
"type": "llm",
|
||||
"showTopP": false,
|
||||
"showStopSign": false
|
||||
},
|
||||
{
|
||||
"model": "text-embedding-v3",
|
||||
"name": "text-embedding-v3",
|
||||
|
||||
@@ -4,10 +4,9 @@ import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/ty
|
||||
import axios from 'axios';
|
||||
import { formatHttpError } from '../utils';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { SandboxCodeTypeEnum } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
|
||||
|
||||
type RunCodeType = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.codeType]: string;
|
||||
[NodeInputKeyEnum.codeType]: 'js';
|
||||
[NodeInputKeyEnum.code]: string;
|
||||
[NodeInputKeyEnum.addInputParam]: Record<string, any>;
|
||||
}>;
|
||||
@@ -17,14 +16,6 @@ type RunCodeResponse = DispatchNodeResultType<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
|
||||
function getURL(codeType: string): string {
|
||||
if (codeType == SandboxCodeTypeEnum.py) {
|
||||
return `${process.env.SANDBOX_URL}/sandbox/python`;
|
||||
} else {
|
||||
return `${process.env.SANDBOX_URL}/sandbox/js`;
|
||||
}
|
||||
}
|
||||
|
||||
export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeResponse> => {
|
||||
const {
|
||||
params: { codeType, code, [NodeInputKeyEnum.addInputParam]: customVariables }
|
||||
@@ -36,7 +27,7 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
|
||||
};
|
||||
}
|
||||
|
||||
const sandBoxRequestUrl = getURL(codeType);
|
||||
const sandBoxRequestUrl = `${process.env.SANDBOX_URL}/sandbox/js`;
|
||||
try {
|
||||
const { data: runResult } = await axios.post<{
|
||||
success: boolean;
|
||||
@@ -49,8 +40,6 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
|
||||
variables: customVariables
|
||||
});
|
||||
|
||||
console.log(runResult);
|
||||
|
||||
if (runResult.success) {
|
||||
return {
|
||||
[NodeOutputKeyEnum.rawResponse]: runResult.data.codeReturn,
|
||||
@@ -63,7 +52,7 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
|
||||
...runResult.data.codeReturn
|
||||
};
|
||||
} else {
|
||||
return Promise.reject('Run code failed');
|
||||
throw new Error('Run code failed');
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -44,14 +44,14 @@ import {
|
||||
textAdaptGptResponse,
|
||||
replaceEditorVariable
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
|
||||
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
|
||||
import { dispatchRunTools } from './agent/runTool/index';
|
||||
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import type { DispatchFlowResponse } from './type';
|
||||
import { DispatchFlowResponse } from './type';
|
||||
import { dispatchStopToolCall } from './agent/runTool/stopTool';
|
||||
import { dispatchLafRequest } from './tools/runLaf';
|
||||
import { dispatchIfElse } from './tools/runIfElse';
|
||||
import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { dispatchSystemConfig } from './init/systemConfig';
|
||||
import { dispatchUpdateVariable } from './tools/runUpdateVar';
|
||||
@@ -62,7 +62,7 @@ import { dispatchTextEditor } from './tools/textEditor';
|
||||
import { dispatchCustomFeedback } from './tools/customFeedback';
|
||||
import { dispatchReadFiles } from './tools/readFiles';
|
||||
import { dispatchUserSelect } from './interactive/userSelect';
|
||||
import type {
|
||||
import {
|
||||
WorkflowInteractiveResponseType,
|
||||
InteractiveNodeResponseType
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
@@ -451,11 +451,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
const interactiveResponse = nodeRunResult.result?.[DispatchNodeResponseKeyEnum.interactive];
|
||||
if (interactiveResponse) {
|
||||
pushStore(nodeRunResult.node, nodeRunResult.result);
|
||||
|
||||
if (props.mode === 'debug') {
|
||||
debugNextStepRunNodes = debugNextStepRunNodes.concat([nodeRunResult.node]);
|
||||
}
|
||||
|
||||
nodeInteractiveResponse = {
|
||||
entryNodeIds: [nodeRunResult.node.nodeId],
|
||||
interactiveResponse
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import type {
|
||||
import {
|
||||
DispatchNodeResultType,
|
||||
ModuleDispatchProps
|
||||
} from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import type {
|
||||
import {
|
||||
UserInputFormItemType,
|
||||
UserInputInteractive
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
@@ -32,6 +32,7 @@ export const dispatchFormInput = async (props: Props): Promise<FormInputResponse
|
||||
query
|
||||
} = props;
|
||||
const { isEntry } = node;
|
||||
|
||||
const interactive = getLastInteractiveValue(histories);
|
||||
|
||||
// Interactive node is not the entry node, return interactive result
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import type {
|
||||
import {
|
||||
DispatchNodeResultType,
|
||||
ModuleDispatchProps
|
||||
} from '@fastgpt/global/core/workflow/runtime/type';
|
||||
@@ -30,6 +30,7 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon
|
||||
query
|
||||
} = props;
|
||||
const { nodeId, isEntry } = node;
|
||||
|
||||
const interactive = getLastInteractiveValue(histories);
|
||||
|
||||
// Interactive node is not the entry node, return interactive result
|
||||
|
||||
@@ -106,7 +106,6 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI
|
||||
/* value type format */
|
||||
export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
|
||||
if (value === undefined) return;
|
||||
if (!type || type === WorkflowIOValueTypeEnum.any) return value;
|
||||
|
||||
if (type === 'string') {
|
||||
if (typeof value !== 'object') return String(value);
|
||||
@@ -118,7 +117,7 @@ export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
|
||||
return Boolean(value);
|
||||
}
|
||||
try {
|
||||
if (type === WorkflowIOValueTypeEnum.arrayString && typeof value === 'string') {
|
||||
if (WorkflowIOValueTypeEnum.arrayString && typeof value === 'string') {
|
||||
return [value];
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"mammoth": "^1.6.0",
|
||||
"mongoose": "^8.10.1",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"mysql2": "^3.11.3",
|
||||
"next": "14.2.25",
|
||||
"nextjs-cors": "^2.2.0",
|
||||
"node-cron": "^3.0.3",
|
||||
|
||||
@@ -13,7 +13,6 @@ export const readDocsFile = async ({ buffer }: ReadRawTextByBuffer): Promise<Rea
|
||||
buffer
|
||||
},
|
||||
{
|
||||
ignoreEmptyParagraphs: false,
|
||||
convertImage: images.imgElement(async (image) => {
|
||||
const imageBase64 = await image.readAsBase64String();
|
||||
const uuid = crypto.randomUUID();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import Editor, { Monaco, loader } from '@monaco-editor/react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import MyIcon from '../../Icon';
|
||||
import { getWebReqUrl } from '../../../../common/system/utils';
|
||||
import usePythonCompletion from './usePythonCompletion';
|
||||
|
||||
loader.config({
|
||||
paths: { vs: getWebReqUrl('/js/monaco-editor.0.45.0/vs') }
|
||||
});
|
||||
@@ -21,7 +21,6 @@ export type Props = Omit<BoxProps, 'resize' | 'onChange'> & {
|
||||
onOpenModal?: () => void;
|
||||
variables?: EditorVariablePickerType[];
|
||||
defaultHeight?: number;
|
||||
language?: string;
|
||||
};
|
||||
|
||||
const options = {
|
||||
@@ -54,14 +53,11 @@ const MyEditor = ({
|
||||
variables = [],
|
||||
defaultHeight = 200,
|
||||
onOpenModal,
|
||||
language = 'typescript',
|
||||
...props
|
||||
}: Props) => {
|
||||
const [height, setHeight] = useState(defaultHeight);
|
||||
const initialY = useRef(0);
|
||||
|
||||
const registerPythonCompletion = usePythonCompletion();
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
initialY.current = e.clientY;
|
||||
|
||||
@@ -80,46 +76,34 @@ const MyEditor = ({
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}, []);
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
const monacoRef = useRef<Monaco | null>(null);
|
||||
|
||||
const handleEditorDidMount = useCallback((editor: any, monaco: Monaco) => {
|
||||
editorRef.current = editor;
|
||||
monacoRef.current = monaco;
|
||||
}, []);
|
||||
|
||||
const beforeMount = useCallback(
|
||||
(monaco: Monaco) => {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: false,
|
||||
allowComments: false,
|
||||
schemas: [
|
||||
{
|
||||
uri: 'http://myserver/foo-schema.json', // 一个假设的 URI
|
||||
fileMatch: ['*'], // 匹配所有文件
|
||||
schema: {} // 空的 Schema
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
monaco.editor.defineTheme('JSONEditorTheme', {
|
||||
base: 'vs', // 可以基于已有的主题进行定制
|
||||
inherit: true, // 继承基础主题的设置
|
||||
rules: [{ token: 'variable', foreground: '2B5FD9' }],
|
||||
colors: {
|
||||
'editor.background': '#ffffff00',
|
||||
'editorLineNumber.foreground': '#aaa',
|
||||
'editorOverviewRuler.border': '#ffffff00',
|
||||
'editor.lineHighlightBackground': '#F7F8FA',
|
||||
'scrollbarSlider.background': '#E8EAEC',
|
||||
'editorIndentGuide.activeBackground': '#ddd',
|
||||
'editorIndentGuide.background': '#eee'
|
||||
const beforeMount = useCallback((monaco: Monaco) => {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: false,
|
||||
allowComments: false,
|
||||
schemas: [
|
||||
{
|
||||
uri: 'http://myserver/foo-schema.json', // 一个假设的 URI
|
||||
fileMatch: ['*'], // 匹配所有文件
|
||||
schema: {} // 空的 Schema
|
||||
}
|
||||
});
|
||||
registerPythonCompletion(monaco);
|
||||
},
|
||||
[registerPythonCompletion]
|
||||
);
|
||||
]
|
||||
});
|
||||
|
||||
monaco.editor.defineTheme('JSONEditorTheme', {
|
||||
base: 'vs', // 可以基于已有的主题进行定制
|
||||
inherit: true, // 继承基础主题的设置
|
||||
rules: [{ token: 'variable', foreground: '2B5FD9' }],
|
||||
colors: {
|
||||
'editor.background': '#ffffff00',
|
||||
'editorLineNumber.foreground': '#aaa',
|
||||
'editorOverviewRuler.border': '#ffffff00',
|
||||
'editor.lineHighlightBackground': '#F7F8FA',
|
||||
'scrollbarSlider.background': '#E8EAEC',
|
||||
'editorIndentGuide.activeBackground': '#ddd',
|
||||
'editorIndentGuide.background': '#eee'
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -134,7 +118,7 @@ const MyEditor = ({
|
||||
>
|
||||
<Editor
|
||||
height={'100%'}
|
||||
language={language}
|
||||
defaultLanguage="typescript"
|
||||
options={options as any}
|
||||
theme="JSONEditorTheme"
|
||||
beforeMount={beforeMount}
|
||||
@@ -143,7 +127,6 @@ const MyEditor = ({
|
||||
onChange={(e) => {
|
||||
onChange?.(e || '');
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
{resize && (
|
||||
<Box
|
||||
|
||||
@@ -4,31 +4,15 @@ import { Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react'
|
||||
import MyModal from '../../MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
type Props = Omit<EditorProps, 'resize'> & { language?: string };
|
||||
function getLanguage(language: string | undefined): string {
|
||||
let fullName: string;
|
||||
switch (language) {
|
||||
case 'py':
|
||||
fullName = 'python';
|
||||
break;
|
||||
case 'js':
|
||||
fullName = 'typescript';
|
||||
break;
|
||||
default:
|
||||
fullName = `typescript`;
|
||||
break;
|
||||
}
|
||||
return fullName;
|
||||
}
|
||||
type Props = Omit<EditorProps, 'resize'> & {};
|
||||
|
||||
const CodeEditor = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { language, ...otherProps } = props;
|
||||
const fullName = getLanguage(language);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyEditor {...props} resize onOpenModal={onOpen} language={fullName} />
|
||||
<MyEditor {...props} resize onOpenModal={onOpen} />
|
||||
<MyModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
@@ -39,7 +23,7 @@ const CodeEditor = (props: Props) => {
|
||||
isCentered
|
||||
>
|
||||
<ModalBody flex={'1 0 0'} overflow={'auto'}>
|
||||
<MyEditor {...props} bg={'myGray.50'} height={'100%'} language={fullName} />
|
||||
<MyEditor {...props} bg={'myGray.50'} height={'100%'} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={2} onClick={onClose} px={6}>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Monaco } from '@monaco-editor/react';
|
||||
import { useCallback } from 'react';
|
||||
let monacoInstance: Monaco | null = null;
|
||||
const usePythonCompletion = () => {
|
||||
return useCallback((monaco: Monaco) => {
|
||||
if (monacoInstance === monaco) return;
|
||||
monacoInstance = monaco;
|
||||
|
||||
monaco.languages.registerCompletionItemProvider('python', {
|
||||
provideCompletionItems: (model, position) => {
|
||||
const wordInfo = model.getWordUntilPosition(position);
|
||||
const currentWordPrefix = wordInfo.word;
|
||||
|
||||
const lineContent = model.getLineContent(position.lineNumber);
|
||||
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: wordInfo.startColumn,
|
||||
endColumn: wordInfo.endColumn
|
||||
};
|
||||
|
||||
const baseSuggestions = [
|
||||
{
|
||||
label: 'len',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: 'len()',
|
||||
documentation: 'get length of object',
|
||||
range,
|
||||
sortText: 'a'
|
||||
}
|
||||
];
|
||||
|
||||
const filtered = baseSuggestions.filter((item) =>
|
||||
item.label.toLowerCase().startsWith(currentWordPrefix.toLowerCase())
|
||||
);
|
||||
|
||||
if (lineContent.startsWith('import')) {
|
||||
const importLength = 'import'.length;
|
||||
const afterImport = lineContent.slice(importLength);
|
||||
const spaceMatch = afterImport.match(/^\s*/);
|
||||
const spaceLength = spaceMatch ? spaceMatch[0].length : 0;
|
||||
|
||||
const startReplaceCol = importLength + spaceLength + 1;
|
||||
const currentCol = position.column;
|
||||
|
||||
const replaceRange = new monaco.Range(
|
||||
position.lineNumber,
|
||||
startReplaceCol,
|
||||
position.lineNumber,
|
||||
currentCol
|
||||
);
|
||||
|
||||
const needsSpace = spaceLength === 0;
|
||||
return {
|
||||
suggestions: [
|
||||
{
|
||||
label: 'numpy',
|
||||
kind: monaco.languages.CompletionItemKind.Module,
|
||||
insertText: `${needsSpace ? ' ' : ''}numpy as np`,
|
||||
documentation: 'numerical computing library',
|
||||
range: replaceRange,
|
||||
sortText: 'a'
|
||||
},
|
||||
{
|
||||
label: 'pandas',
|
||||
kind: monaco.languages.CompletionItemKind.Module,
|
||||
insertText: `${needsSpace ? ' ' : ''}pandas as pd`,
|
||||
documentation: 'data analysis library',
|
||||
range: replaceRange
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return { suggestions: filtered };
|
||||
},
|
||||
triggerCharacters: ['.', '_']
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default usePythonCompletion;
|
||||
@@ -17,15 +17,13 @@ export const useEditTextarea = ({
|
||||
tip,
|
||||
placeholder = '',
|
||||
canEmpty = true,
|
||||
valueRule,
|
||||
rows = 10
|
||||
valueRule
|
||||
}: {
|
||||
title: string;
|
||||
tip?: string;
|
||||
placeholder?: string;
|
||||
canEmpty?: boolean;
|
||||
valueRule?: (val: string) => string | void;
|
||||
rows?: number;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
@@ -107,7 +105,7 @@ export const useEditTextarea = ({
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
maxLength={maxLength}
|
||||
rows={rows}
|
||||
rows={10}
|
||||
bg={'myGray.50'}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from 'ahooks';
|
||||
import MyBox from '../components/common/MyBox';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from './useRequest';
|
||||
|
||||
type ItemHeight<T> = (index: number, data: T) => number;
|
||||
const thresholdVal = 100;
|
||||
@@ -184,21 +183,22 @@ export function useScrollPagination<
|
||||
>(
|
||||
api: (data: TParams) => Promise<TData>,
|
||||
{
|
||||
refreshDeps,
|
||||
scrollLoadType = 'bottom',
|
||||
|
||||
pageSize = 10,
|
||||
params = {},
|
||||
EmptyTip,
|
||||
showErrorToast = true,
|
||||
...props
|
||||
showErrorToast = true
|
||||
}: {
|
||||
refreshDeps?: any[];
|
||||
scrollLoadType?: 'top' | 'bottom';
|
||||
|
||||
pageSize?: number;
|
||||
params?: Record<string, any>;
|
||||
EmptyTip?: React.JSX.Element;
|
||||
showErrorToast?: boolean;
|
||||
} & Parameters<typeof useRequest2>[1]
|
||||
}
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
@@ -213,7 +213,6 @@ export function useScrollPagination<
|
||||
const loadData = useLockFn(
|
||||
async (init = false, ScrollContainerRef?: RefObject<HTMLDivElement>) => {
|
||||
if (noMore && !init) return;
|
||||
setTrue();
|
||||
|
||||
if (init) {
|
||||
setData([]);
|
||||
@@ -222,6 +221,8 @@ export function useScrollPagination<
|
||||
|
||||
const offset = init ? 0 : data.length;
|
||||
|
||||
setTrue();
|
||||
|
||||
try {
|
||||
const res = await api({
|
||||
offset,
|
||||
@@ -273,7 +274,7 @@ export function useScrollPagination<
|
||||
({
|
||||
children,
|
||||
ScrollContainerRef,
|
||||
isLoading: isLoadingProp,
|
||||
isLoading,
|
||||
...props
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
@@ -282,7 +283,7 @@ export function useScrollPagination<
|
||||
} & BoxProps) => {
|
||||
const ref = ScrollContainerRef || ScrollRef;
|
||||
const loadText = useMemo(() => {
|
||||
if (isLoading || isLoadingProp) return t('common:common.is_requesting');
|
||||
if (isLoading) return t('common:common.is_requesting');
|
||||
if (noMore) return t('common:common.request_end');
|
||||
return t('common:common.request_more');
|
||||
}, [isLoading, noMore]);
|
||||
@@ -337,13 +338,13 @@ export function useScrollPagination<
|
||||
);
|
||||
|
||||
// Reload data
|
||||
useRequest2(
|
||||
useRequest(
|
||||
async () => {
|
||||
loadData(true);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
...props
|
||||
refreshDeps
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"exchange_success": "Redemption successful",
|
||||
"expiration_time": "Expiration time",
|
||||
"expired": "Expired",
|
||||
"general_info": "General information",
|
||||
"group": "Group",
|
||||
"help_chatbot": "robot assistant",
|
||||
"help_document": "Help documentation",
|
||||
@@ -51,6 +50,7 @@
|
||||
"password_update_error": "Exception when changing password",
|
||||
"password_update_success": "Password changed successfully",
|
||||
"pending_usage": "To be used",
|
||||
"personal_information": "My info",
|
||||
"phone_label": "Phone number",
|
||||
"please_bind_notification_receiving_path": "Please bind the notification receiving method first",
|
||||
"purchase_extra_package": "Upgrade",
|
||||
@@ -60,7 +60,6 @@
|
||||
"standard_package_and_extra_resource_package": "Includes standard and extra plans",
|
||||
"storage_capacity": "Storage capacity",
|
||||
"team_balance": "Balance",
|
||||
"team_info": "Team Information",
|
||||
"token_validity_period": "Points are valid for one year",
|
||||
"tokens": "integral",
|
||||
"type": "type",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"notification_detail": "notification details",
|
||||
"no_notifications": "No notification yet",
|
||||
"read": "Read",
|
||||
"system": "official",
|
||||
"team": "team"
|
||||
"read": "Read"
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"Hunyuan": "Tencent Hunyuan",
|
||||
"api_key": "API key",
|
||||
"azure": "Azure",
|
||||
"base_url": "Base url",
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
"delete": "delete",
|
||||
"delete_org": "Delete organization",
|
||||
"edit_info": "Edit information",
|
||||
"edit_member": "Edit user",
|
||||
"edit_member_tip": "Name",
|
||||
"edit_org_info": "Edit organization information",
|
||||
"expires": "Expiration time",
|
||||
"forbid_hint": "After forbidden, this invitation link will become invalid. This action is irreversible. Are you sure you want to deactivate?",
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
"compliance.compliance.dataset": "Please ensure that your content strictly complies with relevant laws and regulations and avoid containing any illegal or infringing content. \nPlease be careful when uploading materials that may contain sensitive information.",
|
||||
"compliance.dataset": "Please ensure that your content strictly complies with relevant laws and regulations and avoid containing any illegal or infringing content. \nPlease be careful when uploading materials that may contain sensitive information.",
|
||||
"confirm_choice": "Confirm Choice",
|
||||
"contact_way": "Notification Received",
|
||||
"contact_way": "Contact information",
|
||||
"contribute_app_template": "Contribute Template",
|
||||
"core.Chat": "Chat",
|
||||
"core.Max Token": "Max Token",
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
"delete.failed": "Delete failed",
|
||||
"delete.success": "Delete successfully",
|
||||
"has_chosen": "Selected",
|
||||
"login.Dingtalk": "DingTalk Login",
|
||||
"login.error": "Login Error",
|
||||
"login.password_condition": "Password can be up to 60 characters",
|
||||
"login.success": "Login Successful",
|
||||
"login.Dingtalk": "DingTalk Login",
|
||||
"manage_team": "Manage team",
|
||||
"name": "Name",
|
||||
"notification.remind_owner_bind": "Please remind the creator to bind a notification account",
|
||||
@@ -45,8 +45,8 @@
|
||||
"password.retrieved_account": "Retrieve {{account}} Account",
|
||||
"password.to_login": "Go to Login",
|
||||
"password.verification_code": "Verification Code",
|
||||
"permission.Add": "Add Permissions",
|
||||
"permission.Manage": "Admin",
|
||||
"permission.Add": "Add Permissions",
|
||||
"permission.Manage tip": "Team admin with full permissions",
|
||||
"permission.Read": "Read Only",
|
||||
"permission.Read desc": "Members can only read related resources, cannot create new resources",
|
||||
@@ -55,10 +55,10 @@
|
||||
"permission.only_collaborators": "Collaborators Only",
|
||||
"permission.team_read": "Team Read Access",
|
||||
"permission.team_write": "Team Write Access",
|
||||
"permission_add_tip": "After adding, you can check the permissions for them.",
|
||||
"permission_des.manage": "Can create resources, invite, and delete members",
|
||||
"permission_des.read": "Members can only read related resources and cannot create new resources.",
|
||||
"permission_des.write": "In addition to readable resources, you can also create new resources",
|
||||
"permission_add_tip": "After adding, you can check the permissions for them.",
|
||||
"permissions": "Permissions",
|
||||
"personal_information": "Me",
|
||||
"personalization": "Personalization",
|
||||
@@ -67,7 +67,6 @@
|
||||
"register.register_account": "Register {{account}} Account",
|
||||
"register.success": "Registration Successful",
|
||||
"register.to_login": "Already have an account? Go to Login",
|
||||
"search_group_org_user": "Search member/group/org name",
|
||||
"search_user": "Search Username",
|
||||
"sso_auth_failed": "SSO authentication failed",
|
||||
"synchronization.button": "Sync Now",
|
||||
@@ -83,8 +82,8 @@
|
||||
"team.Team Name": "Team name",
|
||||
"team.Update Team": "Update team information",
|
||||
"team.add_collaborator": "Add Collaborator",
|
||||
"team.add_permission": "Add permissions",
|
||||
"team.add_writer": "Add writable members",
|
||||
"team.add_permission": "Add permissions",
|
||||
"team.avatar_and_name": "avatar",
|
||||
"team.belong_to_group": "Member group",
|
||||
"team.group.avatar": "Group avatar",
|
||||
@@ -97,7 +96,7 @@
|
||||
"team.group.group": "group",
|
||||
"team.group.keep_admin": "Keep administrator rights",
|
||||
"team.group.manage_member": "Managing members",
|
||||
"team.group.manage_tip": "Can manage members, create groups, manage all groups, assign permissions to groups and members",
|
||||
"team.group.manage_tip": "You can invite members, delete members, create groups, manage all groups, and assign permissions to groups and members",
|
||||
"team.group.members": "member",
|
||||
"team.group.name": "Group name",
|
||||
"team.group.permission.manage": "administrator",
|
||||
@@ -106,11 +105,12 @@
|
||||
"team.group.role.admin": "administrator",
|
||||
"team.group.role.member": "member",
|
||||
"team.group.role.owner": "owner",
|
||||
"search_group_org_user": "Search member/group/org name",
|
||||
"team.group.set_as_admin": "Set as administrator",
|
||||
"team.group.toast.can_not_delete_owner": "Owner cannot be deleted, please transfer first",
|
||||
"team.group.transfer_owner": "transfer owner",
|
||||
"team.org.org": "Organization",
|
||||
"team.manage_collaborators": "Manage Collaborators",
|
||||
"team.no_collaborators": "No Collaborators",
|
||||
"team.org.org": "Organization",
|
||||
"team.write_role_member": ""
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"classification_result": "Classification Result",
|
||||
"code.Reset template": "Reset Template",
|
||||
"code.Reset template confirm": "Confirm reset code template? This will reset all inputs and outputs to template values. Please save your current code.",
|
||||
"code.Switch language confirm": "Switching the language will reset the code, will it continue?",
|
||||
"code_execution": "Code Sandbox",
|
||||
"collection_metadata_filter": "Collection Metadata Filter",
|
||||
"complete_extraction_result": "Complete Extraction Result",
|
||||
@@ -154,7 +153,6 @@
|
||||
"select_another_application_to_call": "You can choose another application to call",
|
||||
"special_array_format": "Special array format, returns an empty array when the search result is empty.",
|
||||
"start_with": "Starts With",
|
||||
"support_code_language": "Support import list: pandas,numpy",
|
||||
"target_fields_description": "A target field consists of 'description' and 'key'. Multiple target fields can be extracted.",
|
||||
"template.ai_chat": "AI Chat",
|
||||
"template.ai_chat_intro": "AI Large Model Chat",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"exchange_success": "兑换成功",
|
||||
"expiration_time": "到期时间",
|
||||
"expired": "已过期",
|
||||
"general_info": "通用信息",
|
||||
"group": "组",
|
||||
"help_chatbot": "机器人助手",
|
||||
"help_document": "帮助文档",
|
||||
@@ -49,8 +48,8 @@
|
||||
"password_update_error": "修改密码异常",
|
||||
"password_update_success": "修改密码成功",
|
||||
"pending_usage": "待使用",
|
||||
"personal_information": "个人信息",
|
||||
"phone_label": "手机号",
|
||||
"please_bind_contact": "请绑定联系方式",
|
||||
"please_bind_notification_receiving_path": "请先绑定通知接收途径",
|
||||
"purchase_extra_package": "购买额外套餐",
|
||||
"reminder_create_bound_notification_account": "提醒创建者绑定通知账号",
|
||||
@@ -59,7 +58,6 @@
|
||||
"standard_package_and_extra_resource_package": "包含标准套餐与额外资源包",
|
||||
"storage_capacity": "存储量",
|
||||
"team_balance": "团队余额",
|
||||
"team_info": "团队信息",
|
||||
"token_validity_period": "积分有效期一年",
|
||||
"tokens": "积分",
|
||||
"type": "类型",
|
||||
@@ -70,8 +68,9 @@
|
||||
"usage_balance": "使用余额: 使用余额",
|
||||
"usage_balance_notice": "由于系统升级,原“自动续费从余额扣款”模式取消,余额充值入口关闭。您的余额可用于购买积分",
|
||||
"user_account": "账号",
|
||||
"user_team_team_name": "团队",
|
||||
"user_team_team_name": "团队名",
|
||||
"verification_code": "验证码",
|
||||
"you_can_convert": "您可以兑换",
|
||||
"yuan": "元"
|
||||
}
|
||||
"yuan": "元",
|
||||
"please_bind_contact": "请绑定联系方式"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"notification_detail": "通知详情",
|
||||
"no_notifications": "暂无通知",
|
||||
"read": "已读",
|
||||
"system": "官方",
|
||||
"team": "团队"
|
||||
"no_notifications": "暂无通知"
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"Hunyuan": "腾讯混元",
|
||||
"api_key": "API 密钥",
|
||||
"azure": "微软 Azure",
|
||||
"base_url": "代理地址",
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
"delete_from_team": "移出团队",
|
||||
"delete_org": "删除部门",
|
||||
"edit_info": "编辑信息",
|
||||
"edit_member": "编辑用户",
|
||||
"edit_member_tip": "成员名",
|
||||
"edit_org_info": "编辑部门信息",
|
||||
"expires": "过期时间",
|
||||
"export_members": "导出成员",
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
"compliance.chat": "内容由第三方 AI 生成,无法确保真实准确,仅供参考",
|
||||
"compliance.dataset": "请确保您的内容严格遵守相关法律法规,避免包含任何违法或侵权的内容。请谨慎上传可能涉及敏感信息的资料。",
|
||||
"confirm_choice": "确认选择",
|
||||
"contact_way": "通知接收",
|
||||
"contact_way": "联系方式",
|
||||
"contribute_app_template": "贡献模板",
|
||||
"core.Chat": "对话",
|
||||
"core.Max Token": "单条数据上限",
|
||||
@@ -1289,4 +1289,4 @@
|
||||
"yes": "是",
|
||||
"yesterday": "昨天",
|
||||
"yesterday_detail_time": "昨天 {{time}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
"delete.failed": "删除失败",
|
||||
"delete.success": "删除成功",
|
||||
"has_chosen": "已选择",
|
||||
"login.Dingtalk": "钉钉登录",
|
||||
"login.error": "登录异常",
|
||||
"login.password_condition": "密码最多 60 位",
|
||||
"login.success": "登录成功",
|
||||
"login.Dingtalk": "钉钉登录",
|
||||
"manage_team": "管理团队",
|
||||
"name": "名称",
|
||||
"notification.remind_owner_bind": "请提醒创建者绑定通知账号",
|
||||
@@ -45,20 +45,20 @@
|
||||
"password.retrieved_account": "找回 {{account}} 账号",
|
||||
"password.to_login": "去登录",
|
||||
"password.verification_code": "验证码",
|
||||
"permission.Add": "添加权限",
|
||||
"permission.Manage": "管理员",
|
||||
"permission.Manage tip": "团队管理员,拥有全部权限",
|
||||
"permission.Read": "仅读",
|
||||
"permission.Read desc": "成员仅可阅读相关资源,无法新建资源",
|
||||
"permission.Add": "添加权限",
|
||||
"permission.Write": "可写",
|
||||
"permission.Write tip": "除了可读资源外,还可以新建新的资源",
|
||||
"permission.only_collaborators": "仅协作者访问",
|
||||
"permission.team_read": "团队可访问",
|
||||
"permission.team_write": "团队可编辑",
|
||||
"permission_add_tip": "添加后,您可为其勾选权限。",
|
||||
"permission_des.manage": "可创建资源、邀请、删除成员",
|
||||
"permission_des.read": "成员仅可阅读相关资源,无法新建资源",
|
||||
"permission_des.write": "除了可读资源外,还可以新建新的资源",
|
||||
"permission_add_tip": "添加后,您可为其勾选权限。",
|
||||
"permissions": "权限",
|
||||
"personal_information": "个人信息",
|
||||
"personalization": "个性化",
|
||||
@@ -67,7 +67,6 @@
|
||||
"register.register_account": "注册 {{account}} 账号",
|
||||
"register.success": "注册成功",
|
||||
"register.to_login": "已有账号,去登录",
|
||||
"search_group_org_user": "搜索成员/部门/群组名称",
|
||||
"search_user": "搜索用户名",
|
||||
"sso_auth_failed": "SSO 鉴权失败",
|
||||
"synchronization.button": "立即同步",
|
||||
@@ -83,8 +82,8 @@
|
||||
"team.Team Name": "团队名",
|
||||
"team.Update Team": "更新团队信息",
|
||||
"team.add_collaborator": "添加协作者",
|
||||
"team.add_permission": "添加权限",
|
||||
"team.add_writer": "添加可写成员",
|
||||
"team.add_permission": "添加权限",
|
||||
"team.avatar_and_name": "头像 & 名称",
|
||||
"team.belong_to_group": "所属群组",
|
||||
"team.group.avatar": "群头像",
|
||||
@@ -97,7 +96,7 @@
|
||||
"team.group.group": "群组",
|
||||
"team.group.keep_admin": "保留管理员权限",
|
||||
"team.group.manage_member": "管理成员",
|
||||
"team.group.manage_tip": "可以管理成员、创建群组、管理所有群组、为群组和成员分配权限",
|
||||
"team.group.manage_tip": "可以邀请成员、删除成员、创建群组、管理所有群组、为群组和成员分配权限",
|
||||
"team.group.members": "成员",
|
||||
"team.group.name": "群组名称",
|
||||
"team.group.permission.manage": "管理员",
|
||||
@@ -106,11 +105,12 @@
|
||||
"team.group.role.admin": "管理员",
|
||||
"team.group.role.member": "成员",
|
||||
"team.group.role.owner": "所有者",
|
||||
"search_group_org_user": "搜索成员/部门/群组名称",
|
||||
"team.group.set_as_admin": "设为管理员",
|
||||
"team.group.toast.can_not_delete_owner": "不能删除所有者, 请先转让",
|
||||
"team.group.transfer_owner": "转让所有者",
|
||||
"team.org.org": "部门",
|
||||
"team.manage_collaborators": "管理协作者",
|
||||
"team.no_collaborators": "暂无协作者",
|
||||
"team.org.org": "部门",
|
||||
"team.write_role_member": "可写权限"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"classification_result": "分类结果",
|
||||
"code.Reset template": "还原模板",
|
||||
"code.Reset template confirm": "确认还原代码模板?将会重置所有输入和输出至模板值,请注意保存当前代码。",
|
||||
"code.Switch language confirm": "切换语言将重置代码,是否继续?",
|
||||
"code_execution": "代码运行",
|
||||
"collection_metadata_filter": "集合元数据过滤",
|
||||
"complete_extraction_result": "完整提取结果",
|
||||
@@ -103,7 +102,7 @@
|
||||
"laf_function_call_test": "Laf 函数调用(测试)",
|
||||
"length_equal_to": "长度等于",
|
||||
"length_greater_than": "长度大于",
|
||||
"length_greater_than_or_equal_to": "长度大于等于",
|
||||
"length_greater_than_or_equal_to": "长度大于等",
|
||||
"length_less_than": "长度小于",
|
||||
"length_less_than_or_equal_to": "长度小于等于",
|
||||
"length_not_equal_to": "长度不等于",
|
||||
@@ -154,7 +153,6 @@
|
||||
"select_another_application_to_call": "可以选择一个其他应用进行调用",
|
||||
"special_array_format": "特殊数组格式,搜索结果为空时,返回空数组。",
|
||||
"start_with": "开始为",
|
||||
"support_code_language": "支持import列表:pandas,numpy",
|
||||
"target_fields_description": "由 '描述' 和 'key' 组成一个目标字段,可提取多个目标字段",
|
||||
"template.ai_chat": "AI 对话",
|
||||
"template.ai_chat_intro": "AI 大模型对话",
|
||||
@@ -201,4 +199,4 @@
|
||||
"workflow.Switch_success": "切换成功",
|
||||
"workflow.Team cloud": "团队云端",
|
||||
"workflow.exit_tips": "您的更改尚未保存,「直接退出」将不会保存您的编辑记录。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"exchange_success": "兌換成功",
|
||||
"expiration_time": "到期時間",
|
||||
"expired": "已過期",
|
||||
"general_info": "通用信息",
|
||||
"group": "群組",
|
||||
"help_chatbot": "機器人助手",
|
||||
"help_document": "幫助文檔",
|
||||
@@ -51,6 +50,7 @@
|
||||
"password_update_error": "修改密碼異常",
|
||||
"password_update_success": "修改密碼成功",
|
||||
"pending_usage": "待使用",
|
||||
"personal_information": "個人資訊",
|
||||
"phone_label": "手機號",
|
||||
"please_bind_notification_receiving_path": "請先綁定通知接收途徑",
|
||||
"purchase_extra_package": "購買額外套餐",
|
||||
@@ -60,7 +60,6 @@
|
||||
"standard_package_and_extra_resource_package": "包含標準套餐與額外資源包",
|
||||
"storage_capacity": "儲存量",
|
||||
"team_balance": "團隊餘額",
|
||||
"team_info": "團隊信息",
|
||||
"token_validity_period": "積分有效期限一年",
|
||||
"tokens": "積分",
|
||||
"type": "類型",
|
||||
@@ -71,8 +70,8 @@
|
||||
"usage_balance": "使用餘額: 使用餘額",
|
||||
"usage_balance_notice": "由於系統升級,原「自動續費從餘額扣款」模式取消,餘額儲值入口關閉。\n您的餘額可用於購買積分",
|
||||
"user_account": "帳號",
|
||||
"user_team_team_name": "團隊",
|
||||
"user_team_team_name": "團隊名",
|
||||
"verification_code": "驗證碼",
|
||||
"you_can_convert": "您可以兌換",
|
||||
"yuan": "元"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"notification_detail": "通知詳情",
|
||||
"no_notifications": "暫無通知",
|
||||
"read": "已讀",
|
||||
"system": "官方",
|
||||
"team": "團隊"
|
||||
"read": "已讀"
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"Hunyuan": "騰訊混元",
|
||||
"api_key": "API 密鑰",
|
||||
"azure": "Azure",
|
||||
"base_url": "代理地址",
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
"delete": "刪除",
|
||||
"delete_org": "刪除部門",
|
||||
"edit_info": "編輯訊息",
|
||||
"edit_member": "編輯用戶",
|
||||
"edit_member_tip": "成員名",
|
||||
"edit_org_info": "編輯部門資訊",
|
||||
"expires": "過期時間",
|
||||
"forbid_hint": "停用後,該邀請連結將失效。 該操作不可撤銷,是否確認停用?",
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
"compliance.compliance.dataset": "請確保您的內容嚴格遵守相關法律法規,避免包含任何違法或侵權的內容。\n在上傳可能涉及敏感資訊的資料時請務必謹慎。",
|
||||
"compliance.dataset": "請確保您的內容嚴格遵守相關法律法規,避免包含任何違法或侵權的內容。\n在上傳可能涉及敏感資訊的資料時請務必謹慎。",
|
||||
"confirm_choice": "確認選擇",
|
||||
"contact_way": "通知接收",
|
||||
"contact_way": "聯繫方式",
|
||||
"contribute_app_template": "貢獻範本",
|
||||
"core.Chat": "對話",
|
||||
"core.Max Token": "單筆資料上限",
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
"delete.failed": "刪除失敗",
|
||||
"delete.success": "刪除成功",
|
||||
"has_chosen": "已選擇",
|
||||
"login.Dingtalk": "釘釘登入",
|
||||
"login.error": "登入失敗",
|
||||
"login.password_condition": "密碼最多可輸入 60 個字元",
|
||||
"login.success": "登入成功",
|
||||
"login.Dingtalk": "釘釘登入",
|
||||
"manage_team": "管理團隊",
|
||||
"name": "名稱",
|
||||
"notification.remind_owner_bind": "請提醒建立者綁定通知帳號",
|
||||
@@ -45,20 +45,20 @@
|
||||
"password.retrieved_account": "找回 {{account}} 帳號",
|
||||
"password.to_login": "前往登入",
|
||||
"password.verification_code": "驗證碼",
|
||||
"permission.Add": "新增權限",
|
||||
"permission.Manage": "管理員",
|
||||
"permission.Manage tip": "團隊管理員,擁有完整權限",
|
||||
"permission.Read": "唯讀",
|
||||
"permission.Read desc": "成員僅能閱讀相關資源,無法建立新資源",
|
||||
"permission.Add": "新增權限",
|
||||
"permission.Write": "可寫入",
|
||||
"permission.Write tip": "除了可讀取資源外,還可以建立新的資源",
|
||||
"permission.only_collaborators": "僅協作者可存取",
|
||||
"permission.team_read": "團隊可存取",
|
||||
"permission.team_write": "團隊可編輯",
|
||||
"permission_add_tip": "添加後,您可為其勾選權限。",
|
||||
"permission_des.manage": "可建立資源、邀請及刪除成員",
|
||||
"permission_des.read": "成員僅能閱讀相關資源,無法建立新資源",
|
||||
"permission_des.write": "除了可讀取資源外,還可以建立新的資源",
|
||||
"permission_add_tip": "添加後,您可為其勾選權限。",
|
||||
"permissions": "權限",
|
||||
"personal_information": "個人資料",
|
||||
"personalization": "個人化",
|
||||
@@ -67,7 +67,6 @@
|
||||
"register.register_account": "註冊 {{account}} 帳號",
|
||||
"register.success": "註冊成功",
|
||||
"register.to_login": "已有帳號?前往登入",
|
||||
"search_group_org_user": "搜尋成員/部門/群組名稱",
|
||||
"search_user": "搜尋使用者名稱",
|
||||
"sso_auth_failed": "SSO 鑑權失敗",
|
||||
"synchronization.button": "立即同步",
|
||||
@@ -83,8 +82,8 @@
|
||||
"team.Team Name": "團隊名",
|
||||
"team.Update Team": "更新團隊資訊",
|
||||
"team.add_collaborator": "新增協作者",
|
||||
"team.add_permission": "新增權限",
|
||||
"team.add_writer": "新增可寫入成員",
|
||||
"team.add_permission": "新增權限",
|
||||
"team.avatar_and_name": "頭像與名稱",
|
||||
"team.belong_to_group": "所屬成員群組",
|
||||
"team.group.avatar": "群組頭像",
|
||||
@@ -97,7 +96,7 @@
|
||||
"team.group.group": "群組",
|
||||
"team.group.keep_admin": "保留管理員權限",
|
||||
"team.group.manage_member": "管理成員",
|
||||
"team.group.manage_tip": "可以管理成員、創建群組、管理所有群組、為群組和成員分配權限",
|
||||
"team.group.manage_tip": "可以邀請成員、刪除成員、建立群組、管理所有群組,以及為群組和成員分配權限",
|
||||
"team.group.members": "成員",
|
||||
"team.group.name": "群組名稱",
|
||||
"team.group.permission.manage": "管理員",
|
||||
@@ -106,11 +105,12 @@
|
||||
"team.group.role.admin": "管理員",
|
||||
"team.group.role.member": "成員",
|
||||
"team.group.role.owner": "擁有者",
|
||||
"search_group_org_user": "搜尋成員/部門/群組名稱",
|
||||
"team.group.set_as_admin": "設為管理員",
|
||||
"team.group.toast.can_not_delete_owner": "無法刪除擁有者,請先轉移擁有權",
|
||||
"team.group.transfer_owner": "轉移擁有者",
|
||||
"team.org.org": "組織",
|
||||
"team.manage_collaborators": "管理協作者",
|
||||
"team.no_collaborators": "目前沒有協作者",
|
||||
"team.org.org": "組織",
|
||||
"team.write_role_member": "可寫入權限"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"classification_result": "分類結果",
|
||||
"code.Reset template": "重設範本",
|
||||
"code.Reset template confirm": "確定要重設程式碼範本嗎?這將會把所有輸入和輸出重設為範本值。請儲存您目前的程式碼。",
|
||||
"code.Switch language confirm": "切換語言將重置代碼,是否繼續?",
|
||||
"code_execution": "程式碼執行",
|
||||
"collection_metadata_filter": "資料集詮釋資料篩選器",
|
||||
"complete_extraction_result": "完整擷取結果",
|
||||
@@ -154,7 +153,6 @@
|
||||
"select_another_application_to_call": "可以選擇另一個應用程式來呼叫",
|
||||
"special_array_format": "特殊陣列格式,搜尋結果為空時,回傳空陣列。",
|
||||
"start_with": "開頭為",
|
||||
"support_code_language": "支持import列表:pandas,numpy",
|
||||
"target_fields_description": "由「描述」和「鍵值」組成一個目標欄位,可以擷取多個目標欄位",
|
||||
"template.ai_chat": "AI 對話",
|
||||
"template.ai_chat_intro": "AI 大型語言模型對話",
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
|
||||
3 个模型代码分别为:
|
||||
|
||||
1. [https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-base](https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-base)
|
||||
2. [https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-large](https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-large)
|
||||
3. [https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-v2-m3](https://github.com/labring/FastGPT/tree/main/plugins/model/rerank-bge/bge-reranker-v2-m3)
|
||||
1. [https://github.com/labring/FastGPT/tree/main/python/bge-rerank/bge-reranker-base](https://github.com/labring/FastGPT/tree/main/python/bge-rerank/bge-reranker-base)
|
||||
2. [https://github.com/labring/FastGPT/tree/main/python/bge-rerank/bge-reranker-large](https://github.com/labring/FastGPT/tree/main/python/bge-rerank/bge-reranker-large)
|
||||
3. [https://github.com/labring/FastGPT/tree/main/python/bge-rerank/bge-reranker-v2-m3](https://github.com/labring/FastGPT/tree/main/python/bge-rerank/bge-reranker-v2-m3)
|
||||
|
||||
### 3. 安装依赖
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
name: spider
|
||||
version: "2.2"
|
||||
|
||||
services:
|
||||
searxng:
|
||||
container_name: searxng
|
||||
image: docker.io/searxng/searxng:latest
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- spider_net
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./searxng:/etc/searxng:rw
|
||||
environment:
|
||||
- SEARXNG_BASE_URL=https://${SEARXNG_HOSTNAME:-localhost}/
|
||||
- UWSGI_WORKERS=4 # UWSGI 工作进程数
|
||||
- UWSGI_THREADS=4 # UWSGI 线程数
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
mongodb:
|
||||
container_name: mongodb
|
||||
image: mongo:4.4
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- spider_net
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: root # MongoDB 根用户名
|
||||
MONGO_INITDB_ROOT_PASSWORD: example # MongoDB 根用户密码
|
||||
|
||||
nodeapp:
|
||||
container_name: main
|
||||
platform: linux/amd64
|
||||
#build:
|
||||
# context: .
|
||||
image: gggaaallleee/webcrawler-test-new:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
- spider_net
|
||||
depends_on:
|
||||
- mongodb
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "1m"
|
||||
max-file: "1"
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
environment:
|
||||
- ACCESS_TOKEN=webcrawler # 访问令牌
|
||||
- DETECT_WEBSITE=zhuanlan.zhihu.com # 无法处理跳过的网站
|
||||
- STRATEGIES=[{"waitUntil":"networkidle0","timeout":5000},{"waitUntil":"networkidle2","timeout":10000},{"waitUntil":"load","timeout":15000}] # 页面加载策略
|
||||
- PORT=3000
|
||||
- MAX_CONCURRENCY=10 # 最大并发数
|
||||
- NODE_ENV=development
|
||||
- ENGINE_BAIDUURL=https://www.baidu.com/s # 百度搜索引擎 URL
|
||||
- ENGINE_SEARCHXNGURL=http://searxng:8080/search # Searxng 搜索引擎 URL
|
||||
- MONGODB_URI=mongodb://root:example@mongodb:27017 # MongoDB 连接 URI
|
||||
- BLACKLIST=[".gov.cn",".edu.cn"] # 受保护域名
|
||||
- STD_TTL=3600 # 标准 TTL(秒)
|
||||
- EXPIRE_AFTER_SECONDS=9000 # 过期时间(秒)
|
||||
#- VALIDATE_PROXY=[{"ip":"","port":},{"ip":"","port":}] #代理池
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
cpus: '2.0'
|
||||
|
||||
networks:
|
||||
spider_net:
|
||||
|
||||
volumes:
|
||||
mongo-data:
|
||||
@@ -1,6 +0,0 @@
|
||||
# This configuration file updates the default configuration file
|
||||
# See https://github.com/searxng/searxng/blob/master/searx/limiter.toml
|
||||
|
||||
[botdetection.ip_limit]
|
||||
# activate link_token method in the ip_limit method
|
||||
link_token = true
|
||||
@@ -1,122 +0,0 @@
|
||||
general:
|
||||
debug: false
|
||||
instance_name: "searxng"
|
||||
privacypolicy_url: false
|
||||
donation_url: false
|
||||
contact_url: false
|
||||
enable_metrics: true
|
||||
open_metrics: ''
|
||||
|
||||
brand:
|
||||
new_issue_url: https://github.com/searxng/searxng/issues/new
|
||||
docs_url: https://docs.searxng.org/
|
||||
public_instances: https://searx.space
|
||||
wiki_url: https://github.com/searxng/searxng/wiki
|
||||
issue_url: https://github.com/searxng/searxng/issues
|
||||
|
||||
search:
|
||||
safe_search: 0
|
||||
autocomplete: ""
|
||||
autocomplete_min: 4
|
||||
default_lang: "auto"
|
||||
ban_time_on_fail: 5
|
||||
max_ban_time_on_fail: 120
|
||||
formats:
|
||||
- html
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
bind_address: "0.0.0.0"
|
||||
base_url: false
|
||||
limiter: false
|
||||
public_instance: false
|
||||
secret_key: "example"
|
||||
image_proxy: false
|
||||
http_protocol_version: "1.0"
|
||||
method: "POST"
|
||||
default_http_headers:
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Download-Options: noopen
|
||||
X-Robots-Tag: noindex, nofollow
|
||||
Referrer-Policy: no-referrer
|
||||
|
||||
redis:
|
||||
url: false
|
||||
|
||||
ui:
|
||||
static_path: ""
|
||||
static_use_hash: false
|
||||
templates_path: ""
|
||||
default_theme: simple
|
||||
default_locale: ""
|
||||
query_in_title: false
|
||||
infinite_scroll: false
|
||||
center_alignment: false
|
||||
theme_args:
|
||||
simple_style: auto
|
||||
# 启用 cn 分类
|
||||
enabled_categories: [cn,en, general, images,en]
|
||||
# 或者定义分类显示顺序
|
||||
categories_order: [cn, en,general, images]
|
||||
|
||||
outgoing:
|
||||
request_timeout: 30.0
|
||||
max_request_timeout: 40.0
|
||||
pool_connections: 200
|
||||
pool_maxsize: 50
|
||||
enable_http2: false
|
||||
retries: 5
|
||||
|
||||
engines:
|
||||
- name: bing
|
||||
engine: bing
|
||||
disabled: false
|
||||
categories: cn
|
||||
#- name: bilibili
|
||||
# engine: bilibili
|
||||
# shortcut: bil
|
||||
# disabled: false
|
||||
# categories: cn
|
||||
- name : baidu
|
||||
engine : json_engine
|
||||
paging : True
|
||||
first_page_num : 0
|
||||
search_url : https://www.baidu.com/s?tn=json&wd={query}&pn={pageno}&rn=50
|
||||
url_query : url
|
||||
title_query : title
|
||||
content_query : abs
|
||||
categories : cn
|
||||
- name : 360search
|
||||
engine: 360search
|
||||
disabled: false
|
||||
categories: cn
|
||||
- name : sogou
|
||||
disabled: false
|
||||
categories: cn
|
||||
|
||||
- name: google
|
||||
disabled: false
|
||||
categories: en
|
||||
- name: yahoo
|
||||
disabled: false
|
||||
categories: en
|
||||
- name: duckduckgo
|
||||
disabled: false
|
||||
categories: en
|
||||
|
||||
|
||||
|
||||
search:
|
||||
formats:
|
||||
- html
|
||||
- json
|
||||
doi_resolvers:
|
||||
oadoi.org: 'https://oadoi.org/'
|
||||
doi.org: 'https://doi.org/'
|
||||
doai.io: 'https://dissem.in/'
|
||||
sci-hub.se: 'https://sci-hub.se/'
|
||||
sci-hub.st: 'https://sci-hub.st/'
|
||||
sci-hub.ru: 'https://sci-hub.ru/'
|
||||
|
||||
default_doi_resolver: 'oadoi.org'
|
||||
|
||||
9
pnpm-lock.yaml
generated
@@ -220,9 +220,6 @@ importers:
|
||||
multer:
|
||||
specifier: 1.4.5-lts.1
|
||||
version: 1.4.5-lts.1
|
||||
mysql2:
|
||||
specifier: ^3.11.3
|
||||
version: 3.13.0
|
||||
next:
|
||||
specifier: 14.2.25
|
||||
version: 14.2.25(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)
|
||||
@@ -14610,7 +14607,7 @@ snapshots:
|
||||
eslint: 8.56.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.56.0)
|
||||
eslint-plugin-react: 7.37.4(eslint@8.56.0)
|
||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.56.0)
|
||||
@@ -14640,7 +14637,7 @@ snapshots:
|
||||
stable-hash: 0.0.5
|
||||
tinyglobby: 0.2.12
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -14655,7 +14652,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0):
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
|
||||
@@ -2,15 +2,5 @@
|
||||
|
||||
该目录为 FastGPT 主项目。
|
||||
|
||||
- app fastgpt 核心应用。
|
||||
- sandbox 沙盒项目,用于运行工作流里的代码执行 (需求python环境为python:3.11,额外安装的包请于requirements.txt填写,同时注意个别包可能额外安装库(如pandas需要安装libffi))。
|
||||
- 新加入python包遇见超时或者权限拦截的问题(确定不是自己的语法问题),请进入docker容器内部执行以下指令:
|
||||
|
||||
```shell
|
||||
docker exec -it 《替换成容器名》 /bin/bash
|
||||
chmod -x testSystemCall.sh
|
||||
bash ./testSystemCall.sh
|
||||
```
|
||||
|
||||
然后将新的数组替换src下sandbox的constants.py中的SYSTEM_CALLS数组即可
|
||||
|
||||
- app 前端项目,用于展示和使用 FastGPT。
|
||||
- sandbox 沙盒项目,用于测试和开发。
|
||||
|
||||
@@ -23,11 +23,9 @@ MULTIPLE_DATA_TO_BASE64=true
|
||||
# mongo 数据库连接参数,本地开发连接远程数据库时,可能需要增加 directConnection=true 参数,才能连接上。
|
||||
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/fastgpt?authSource=admin
|
||||
|
||||
# 向量库优先级: pg > oceanbase > milvus
|
||||
# 向量库优先级: pg > milvus
|
||||
# PG 向量库连接参数
|
||||
PG_URL=postgresql://username:password@host:port/postgres
|
||||
# OceanBase 向量库连接参数
|
||||
OCEANBASE_URL=
|
||||
# milvus 向量库连接参数
|
||||
MILVUS_ADDRESS=
|
||||
MILVUS_TOKEN=
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"qaMaxProcess": 10, // 问答拆分线程数量
|
||||
"vlmMaxProcess": 10, // 图片理解模型最大处理进程
|
||||
"tokenWorkers": 30, // Token 计算线程保持数,会持续占用内存,不能设置太大。
|
||||
"hnswEfSearch": 100, // 向量搜索参数,仅对 PG 和 OB 生效。越大,搜索越精确,但是速度越慢。设置为100,有99%+精度。
|
||||
"pgHNSWEfSearch": 100, // 向量搜索参数。越大,搜索越精确,但是速度越慢。设置为100,有99%+精度。
|
||||
"customPdfParse": {
|
||||
"url": "", // 自定义 PDF 解析服务地址
|
||||
"key": "", // 自定义 PDF 解析服务密钥
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"vectorMaxProcess": 15, // 向量处理线程数量
|
||||
"qaMaxProcess": 15, // 问答拆分线程数量
|
||||
"tokenWorkers": 30, // Token 计算线程保持数,会持续占用内存,不能设置太大。
|
||||
"hnswEfSearch": 100 // 向量搜索参数,仅对 PG 和 OB 生效。越大,搜索越精确,但是速度越慢。设置为100,有99%+精度。
|
||||
"pgHNSWEfSearch": 100 // 向量搜索参数。越大,搜索越精确,但是速度越慢。设置为100,有99%+精度。
|
||||
},
|
||||
"llmModels": [
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ const FolderPath = (props: {
|
||||
borderRadius={'md'}
|
||||
maxW={'45vw'}
|
||||
className={'textEllipsis'}
|
||||
{...(i === concatPaths.length - 1 && concatPaths.length > 1
|
||||
{...(i === concatPaths.length - 1
|
||||
? {
|
||||
cursor: 'default',
|
||||
color: 'myGray.700',
|
||||
|
||||
@@ -8,80 +8,43 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
HStack
|
||||
HStack,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import type {
|
||||
import {
|
||||
AIChatItemValueItemType,
|
||||
ToolModuleResponseItemType,
|
||||
UserChatItemValueItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import type {
|
||||
import {
|
||||
InteractiveBasicType,
|
||||
UserInputInteractive,
|
||||
UserSelectInteractive
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { isEqual } from 'lodash';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
|
||||
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
|
||||
import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents';
|
||||
|
||||
const accordionButtonStyle = {
|
||||
w: 'auto',
|
||||
bg: 'white',
|
||||
borderRadius: 'md',
|
||||
borderWidth: '1px',
|
||||
borderColor: 'myGray.200',
|
||||
boxShadow: '1',
|
||||
pl: 3,
|
||||
pr: 2.5,
|
||||
_hover: {
|
||||
bg: 'auto'
|
||||
}
|
||||
type props = {
|
||||
value: UserChatItemValueItemType | AIChatItemValueItemType;
|
||||
isLastResponseValue: boolean;
|
||||
isChatting: boolean;
|
||||
};
|
||||
|
||||
const RenderResoningContent = React.memo(function RenderResoningContent({
|
||||
content,
|
||||
isChatting,
|
||||
isLastResponseValue
|
||||
}: {
|
||||
content: string;
|
||||
isChatting: boolean;
|
||||
isLastResponseValue: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const showAnimation = isChatting && isLastResponseValue;
|
||||
const onSendPrompt: SendPromptFnType = (e) => eventBus.emit(EventNameEnum.sendQuestion, e);
|
||||
|
||||
return (
|
||||
<Accordion allowToggle defaultIndex={isLastResponseValue ? 0 : undefined}>
|
||||
<AccordionItem borderTop={'none'} borderBottom={'none'}>
|
||||
<AccordionButton {...accordionButtonStyle} py={1}>
|
||||
<HStack mr={2} spacing={1}>
|
||||
<MyIcon name={'core/chat/think'} w={'0.85rem'} />
|
||||
<Box fontSize={'sm'}>{t('chat:ai_reasoning')}</Box>
|
||||
</HStack>
|
||||
|
||||
{showAnimation && <MyIcon name={'common/loading'} w={'0.85rem'} />}
|
||||
<AccordionIcon color={'myGray.600'} ml={5} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel
|
||||
py={0}
|
||||
pr={0}
|
||||
pl={3}
|
||||
mt={2}
|
||||
borderLeft={'2px solid'}
|
||||
borderColor={'myGray.300'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Markdown source={content} showAnimation={showAnimation} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
});
|
||||
const RenderText = React.memo(function RenderText({
|
||||
showAnimation,
|
||||
text
|
||||
@@ -95,7 +58,6 @@ const RenderText = React.memo(function RenderText({
|
||||
|
||||
return <Markdown source={source} showAnimation={showAnimation} />;
|
||||
});
|
||||
|
||||
const RenderTool = React.memo(
|
||||
function RenderTool({
|
||||
showAnimation,
|
||||
@@ -107,20 +69,37 @@ const RenderTool = React.memo(
|
||||
return (
|
||||
<Box>
|
||||
{tools.map((tool) => {
|
||||
const formatJson = (string: string) => {
|
||||
const toolParams = (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(string), null, 2);
|
||||
return JSON.stringify(JSON.parse(tool.params), null, 2);
|
||||
} catch (error) {
|
||||
return string;
|
||||
return tool.params;
|
||||
}
|
||||
};
|
||||
const toolParams = formatJson(tool.params);
|
||||
const toolResponse = formatJson(tool.response);
|
||||
})();
|
||||
const toolResponse = (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tool.response), null, 2);
|
||||
} catch (error) {
|
||||
return tool.response;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<Accordion key={tool.id} allowToggle _notLast={{ mb: 2 }}>
|
||||
<AccordionItem borderTop={'none'} borderBottom={'none'}>
|
||||
<AccordionButton {...accordionButtonStyle}>
|
||||
<AccordionButton
|
||||
w={'auto'}
|
||||
bg={'white'}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'1'}
|
||||
pl={3}
|
||||
pr={2.5}
|
||||
_hover={{
|
||||
bg: 'auto'
|
||||
}}
|
||||
>
|
||||
<Avatar src={tool.toolAvatar} w={'1.25rem'} h={'1.25rem'} borderRadius={'sm'} />
|
||||
<Box mx={2} fontSize={'sm'} color={'myGray.900'}>
|
||||
{tool.toolName}
|
||||
@@ -161,24 +140,99 @@ ${toolResponse}`}
|
||||
},
|
||||
(prevProps, nextProps) => isEqual(prevProps, nextProps)
|
||||
);
|
||||
const RenderResoningContent = React.memo(function RenderResoningContent({
|
||||
content,
|
||||
isChatting,
|
||||
isLastResponseValue
|
||||
}: {
|
||||
content: string;
|
||||
isChatting: boolean;
|
||||
isLastResponseValue: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const showAnimation = isChatting && isLastResponseValue;
|
||||
|
||||
const onSendPrompt = (e: { text: string; isInteractivePrompt: boolean }) =>
|
||||
eventBus.emit(EventNameEnum.sendQuestion, e);
|
||||
return (
|
||||
<Accordion allowToggle defaultIndex={isLastResponseValue ? 0 : undefined}>
|
||||
<AccordionItem borderTop={'none'} borderBottom={'none'}>
|
||||
<AccordionButton
|
||||
w={'auto'}
|
||||
bg={'white'}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'1'}
|
||||
pl={3}
|
||||
pr={2.5}
|
||||
py={1}
|
||||
_hover={{
|
||||
bg: 'auto'
|
||||
}}
|
||||
>
|
||||
<HStack mr={2} spacing={1}>
|
||||
<MyIcon name={'core/chat/think'} w={'0.85rem'} />
|
||||
<Box fontSize={'sm'}>{t('chat:ai_reasoning')}</Box>
|
||||
</HStack>
|
||||
|
||||
{showAnimation && <MyIcon name={'common/loading'} w={'0.85rem'} />}
|
||||
<AccordionIcon color={'myGray.600'} ml={5} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel
|
||||
py={0}
|
||||
pr={0}
|
||||
pl={3}
|
||||
mt={2}
|
||||
borderLeft={'2px solid'}
|
||||
borderColor={'myGray.300'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Markdown source={content} showAnimation={showAnimation} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
});
|
||||
const RenderUserSelectInteractive = React.memo(function RenderInteractive({
|
||||
interactive
|
||||
}: {
|
||||
interactive: InteractiveBasicType & UserSelectInteractive;
|
||||
}) {
|
||||
return (
|
||||
<SelectOptionsComponent
|
||||
interactiveParams={interactive.params}
|
||||
onSelect={(value) => {
|
||||
onSendPrompt({
|
||||
text: value,
|
||||
isInteractivePrompt: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{interactive?.params?.description && <Markdown source={interactive.params.description} />}
|
||||
<Flex flexDirection={'column'} gap={2} w={'250px'}>
|
||||
{interactive.params.userSelectOptions?.map((option) => {
|
||||
const selected = option.value === interactive?.params?.userSelectedVal;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={option.key}
|
||||
variant={'whitePrimary'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
isDisabled={interactive?.params?.userSelectedVal !== undefined}
|
||||
{...(selected
|
||||
? {
|
||||
_disabled: {
|
||||
cursor: 'default',
|
||||
borderColor: 'primary.300',
|
||||
bg: 'primary.50 !important',
|
||||
color: 'primary.600'
|
||||
}
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
onSendPrompt({
|
||||
text: option.value,
|
||||
isInteractivePrompt: true
|
||||
});
|
||||
}}
|
||||
>
|
||||
{option.value}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
});
|
||||
const RenderUserFormInteractive = React.memo(function RenderFormInput({
|
||||
@@ -187,52 +241,110 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
|
||||
interactive: InteractiveBasicType & UserInputInteractive;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { register, setValue, handleSubmit: handleSubmitChat, control, reset } = useForm();
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
if (interactive.type === 'userInput') {
|
||||
return interactive.params.inputForm?.reduce((acc: Record<string, any>, item) => {
|
||||
acc[item.label] = !!item.value ? item.value : item.defaultValue;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return {};
|
||||
}, [interactive]);
|
||||
|
||||
const handleFormSubmit = useCallback((data: Record<string, any>) => {
|
||||
const onSubmit = useCallback((data: any) => {
|
||||
onSendPrompt({
|
||||
text: JSON.stringify(data),
|
||||
isInteractivePrompt: true
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (interactive.type === 'userInput') {
|
||||
const defaultValues = interactive.params.inputForm?.reduce(
|
||||
(acc: Record<string, any>, item) => {
|
||||
acc[item.label] = !!item.value ? item.value : item.defaultValue;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
reset(defaultValues);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} gap={2} w={'250px'}>
|
||||
<FormInputComponent
|
||||
interactiveParams={interactive.params}
|
||||
defaultValues={defaultValues}
|
||||
SubmitButton={({ onSubmit }) => (
|
||||
<Button onClick={() => onSubmit(handleFormSubmit)()}>{t('common:Submit')}</Button>
|
||||
)}
|
||||
/>
|
||||
{interactive.params.description && <Markdown source={interactive.params.description} />}
|
||||
{interactive.params.inputForm?.map((input) => (
|
||||
<Box key={input.label}>
|
||||
<FormLabel mb={1} required={input.required} whiteSpace={'pre-wrap'}>
|
||||
{input.label}
|
||||
{input.description && <QuestionTip ml={1} label={input.description} />}
|
||||
</FormLabel>
|
||||
{input.type === FlowNodeInputTypeEnum.input && (
|
||||
<MyTextarea
|
||||
isDisabled={interactive.params.submitted}
|
||||
{...register(input.label, {
|
||||
required: input.required
|
||||
})}
|
||||
bg={'white'}
|
||||
autoHeight
|
||||
minH={40}
|
||||
maxH={100}
|
||||
/>
|
||||
)}
|
||||
{input.type === FlowNodeInputTypeEnum.textarea && (
|
||||
<Textarea
|
||||
isDisabled={interactive.params.submitted}
|
||||
bg={'white'}
|
||||
{...register(input.label, {
|
||||
required: input.required
|
||||
})}
|
||||
rows={5}
|
||||
maxLength={input.maxLength || 4000}
|
||||
/>
|
||||
)}
|
||||
{input.type === FlowNodeInputTypeEnum.numberInput && (
|
||||
<MyNumberInput
|
||||
min={input.min}
|
||||
max={input.max}
|
||||
defaultValue={input.defaultValue}
|
||||
isDisabled={interactive.params.submitted}
|
||||
bg={'white'}
|
||||
register={register}
|
||||
name={input.label}
|
||||
isRequired={input.required}
|
||||
/>
|
||||
)}
|
||||
{input.type === FlowNodeInputTypeEnum.select && (
|
||||
<Controller
|
||||
key={input.label}
|
||||
control={control}
|
||||
name={input.label}
|
||||
rules={{ required: input.required }}
|
||||
render={({ field: { ref, value } }) => {
|
||||
if (!input.list) return <></>;
|
||||
return (
|
||||
<MySelect
|
||||
ref={ref}
|
||||
width={'100%'}
|
||||
list={input.list}
|
||||
value={value}
|
||||
isDisabled={interactive.params.submitted}
|
||||
onChange={(e) => setValue(input.label, e)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!interactive.params.submitted && (
|
||||
<Flex w={'full'} justifyContent={'end'}>
|
||||
<Button onClick={handleSubmitChat(onSubmit)}>{t('common:Submit')}</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
const AIResponseBox = ({
|
||||
value,
|
||||
isLastResponseValue,
|
||||
isChatting
|
||||
}: {
|
||||
value: UserChatItemValueItemType | AIChatItemValueItemType;
|
||||
isLastResponseValue: boolean;
|
||||
isChatting: boolean;
|
||||
}) => {
|
||||
if (value.type === ChatItemValueTypeEnum.text && value.text) {
|
||||
const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => {
|
||||
if (value.type === ChatItemValueTypeEnum.text && value.text)
|
||||
return (
|
||||
<RenderText showAnimation={isChatting && isLastResponseValue} text={value.text.content} />
|
||||
);
|
||||
}
|
||||
if (value.type === ChatItemValueTypeEnum.reasoning && value.reasoning) {
|
||||
if (value.type === ChatItemValueTypeEnum.reasoning && value.reasoning)
|
||||
return (
|
||||
<RenderResoningContent
|
||||
isChatting={isChatting}
|
||||
@@ -240,18 +352,14 @@ const AIResponseBox = ({
|
||||
content={value.reasoning.content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
|
||||
if (value.type === ChatItemValueTypeEnum.tool && value.tools)
|
||||
return <RenderTool showAnimation={isChatting} tools={value.tools} />;
|
||||
}
|
||||
if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) {
|
||||
if (value.interactive.type === 'userSelect') {
|
||||
if (value.interactive.type === 'userSelect')
|
||||
return <RenderUserSelectInteractive interactive={value.interactive} />;
|
||||
}
|
||||
if (value.interactive?.type === 'userInput') {
|
||||
if (value.interactive?.type === 'userInput')
|
||||
return <RenderUserFormInteractive interactive={value.interactive} />;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default React.memo(AIResponseBox);
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
|
||||
import { Controller, useForm, UseFormHandleSubmit } from 'react-hook-form';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import {
|
||||
UserInputFormItemType,
|
||||
UserInputInteractive,
|
||||
UserSelectInteractive,
|
||||
UserSelectOptionItemType
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
|
||||
const DescriptionBox = React.memo(function DescriptionBox({
|
||||
description
|
||||
}: {
|
||||
description?: string;
|
||||
}) {
|
||||
if (!description) return null;
|
||||
return (
|
||||
<Box mb={4}>
|
||||
<Markdown source={description} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export const SelectOptionsComponent = React.memo(function SelectOptionsComponent({
|
||||
interactiveParams,
|
||||
onSelect
|
||||
}: {
|
||||
interactiveParams: UserSelectInteractive['params'];
|
||||
onSelect: (value: string) => void;
|
||||
}) {
|
||||
const { description, userSelectOptions, userSelectedVal } = interactiveParams;
|
||||
|
||||
return (
|
||||
<Box maxW={'100%'}>
|
||||
<DescriptionBox description={description} />
|
||||
<Flex flexDirection={'column'} gap={3} w={'250px'}>
|
||||
{userSelectOptions.map((option: UserSelectOptionItemType) => {
|
||||
const selected = option.value === userSelectedVal;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={option.key}
|
||||
variant={'whitePrimary'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
isDisabled={!!userSelectedVal}
|
||||
{...(selected
|
||||
? {
|
||||
_disabled: {
|
||||
cursor: 'default',
|
||||
borderColor: 'primary.300',
|
||||
bg: 'primary.50 !important',
|
||||
color: 'primary.600'
|
||||
}
|
||||
}
|
||||
: {})}
|
||||
onClick={() => onSelect(option.value)}
|
||||
>
|
||||
{option.value}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export const FormInputComponent = React.memo(function FormInputComponent({
|
||||
interactiveParams,
|
||||
defaultValues = {},
|
||||
SubmitButton
|
||||
}: {
|
||||
interactiveParams: UserInputInteractive['params'];
|
||||
defaultValues?: Record<string, any>;
|
||||
SubmitButton: (e: { onSubmit: UseFormHandleSubmit<Record<string, any>> }) => React.JSX.Element;
|
||||
}) {
|
||||
const { description, inputForm, submitted } = interactiveParams;
|
||||
|
||||
const { register, setValue, handleSubmit, control } = useForm({
|
||||
defaultValues
|
||||
});
|
||||
|
||||
const FormItemLabel = useCallback(
|
||||
({
|
||||
label,
|
||||
required,
|
||||
description
|
||||
}: {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<FormLabel required={required} mb={0} fontWeight="medium" color="gray.700">
|
||||
{label}
|
||||
</FormLabel>
|
||||
{description && <QuestionTip ml={1} label={description} />}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const RenderFormInput = useCallback(
|
||||
({ input }: { input: UserInputFormItemType }) => {
|
||||
const { type, label, required, maxLength, min, max, defaultValue, list } = input;
|
||||
|
||||
switch (type) {
|
||||
case FlowNodeInputTypeEnum.input:
|
||||
return (
|
||||
<MyTextarea
|
||||
isDisabled={submitted}
|
||||
{...register(label, {
|
||||
required: required
|
||||
})}
|
||||
bg={'white'}
|
||||
autoHeight
|
||||
minH={40}
|
||||
maxH={100}
|
||||
/>
|
||||
);
|
||||
case FlowNodeInputTypeEnum.textarea:
|
||||
return (
|
||||
<Textarea
|
||||
isDisabled={submitted}
|
||||
bg={'white'}
|
||||
{...register(label, {
|
||||
required: required
|
||||
})}
|
||||
rows={5}
|
||||
maxLength={maxLength || 4000}
|
||||
/>
|
||||
);
|
||||
case FlowNodeInputTypeEnum.numberInput:
|
||||
return (
|
||||
<MyNumberInput
|
||||
min={min}
|
||||
max={max}
|
||||
defaultValue={defaultValue}
|
||||
isDisabled={submitted}
|
||||
bg={'white'}
|
||||
register={register}
|
||||
name={label}
|
||||
isRequired={required}
|
||||
/>
|
||||
);
|
||||
case FlowNodeInputTypeEnum.select:
|
||||
return (
|
||||
<Controller
|
||||
key={label}
|
||||
control={control}
|
||||
name={label}
|
||||
rules={{ required: required }}
|
||||
render={({ field: { ref, value } }) => {
|
||||
if (!list) return <></>;
|
||||
return (
|
||||
<MySelect
|
||||
ref={ref}
|
||||
width={'100%'}
|
||||
list={list}
|
||||
value={value}
|
||||
isDisabled={submitted}
|
||||
onChange={(e) => setValue(label, e)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[control, register, setValue, submitted]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<DescriptionBox description={description} />
|
||||
<Flex flexDirection={'column'} gap={3}>
|
||||
{inputForm.map((input) => (
|
||||
<Box key={input.label}>
|
||||
<FormItemLabel
|
||||
label={input.label}
|
||||
required={input.required}
|
||||
description={input.description}
|
||||
/>
|
||||
<RenderFormInput input={input} />
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
{!submitted && (
|
||||
<Flex justifyContent={'flex-end'} mt={4}>
|
||||
<SubmitButton onSubmit={handleSubmit} />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -79,10 +79,7 @@ function MemberModal({
|
||||
withOrgs: true,
|
||||
status: 'active',
|
||||
searchKey
|
||||
},
|
||||
throttleWait: 500,
|
||||
debounceWait: 200,
|
||||
refreshDeps: [searchKey]
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -103,6 +100,13 @@ function MemberModal({
|
||||
}
|
||||
);
|
||||
|
||||
const search = _.debounce(() => {
|
||||
refreshList();
|
||||
refreshGroups();
|
||||
}, 200);
|
||||
|
||||
useEffect(search, [searchKey]);
|
||||
|
||||
const [selectedOrgList, setSelectedOrgIdList] = useState<OrgListItemType[]>([]);
|
||||
|
||||
const [selectedMemberList, setSelectedMemberList] = useState<
|
||||
|
||||
@@ -5,7 +5,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { CloseIcon } from '@chakra-ui/icons';
|
||||
import { readInform } from '@/web/support/user/inform/api';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import Markdown from '@/components/Markdown';
|
||||
|
||||
const ImportantInform = ({
|
||||
informs,
|
||||
refetch
|
||||
@@ -28,61 +28,32 @@ const ImportantInform = ({
|
||||
{informs.map((inform) => (
|
||||
<Flex
|
||||
key={inform._id}
|
||||
py={4}
|
||||
bg={'primary.015'}
|
||||
py={3}
|
||||
px={5}
|
||||
fontSize={'md'}
|
||||
borderRadius={'lg'}
|
||||
boxShadow={'4'}
|
||||
borderWidth={'1px'}
|
||||
backgroundColor={'white'}
|
||||
borderColor={'borderColor.base'}
|
||||
minW={['200px', '400px']}
|
||||
alignItems={'flex-start'}
|
||||
mb={3}
|
||||
backdropFilter={'blur(30px)'}
|
||||
>
|
||||
<MyIcon
|
||||
name={'support/user/informLight'}
|
||||
w={5}
|
||||
h={5}
|
||||
mr={2}
|
||||
mt={'2px'}
|
||||
color="blue.600"
|
||||
/>
|
||||
<MyIcon name={'support/user/informLight'} w={'16px'} mr={2} />
|
||||
<Box flex={'1 0 0'}>
|
||||
<Box
|
||||
fontWeight="bold"
|
||||
fontSize="16px"
|
||||
lineHeight="24px"
|
||||
letterSpacing="0.15px"
|
||||
fontFamily="'PingFang SC', sans-serif"
|
||||
color=" #24282C"
|
||||
>
|
||||
{inform.title}
|
||||
</Box>
|
||||
<Box
|
||||
pt={1}
|
||||
fontSize="14px"
|
||||
lineHeight="20px"
|
||||
letterSpacing="0.25px"
|
||||
fontFamily="'PingFang SC', sans-serif"
|
||||
fontWeight="400"
|
||||
color="#24282C"
|
||||
>
|
||||
<Markdown source={inform?.content} />
|
||||
</Box>
|
||||
<Box fontWeight={'bold'}>{inform.title}</Box>
|
||||
<Box fontSize={'sm'}>{inform.content}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
<CloseIcon
|
||||
cursor={'pointer'}
|
||||
p={1}
|
||||
pt={0}
|
||||
borderRadius={'4px'}
|
||||
_hover={{
|
||||
backgroundColor: 'rgba(17, 24, 36, 0.05)'
|
||||
color: 'primary.700'
|
||||
}}
|
||||
w={'12px'}
|
||||
onClick={() => onClickClose(inform._id)}
|
||||
>
|
||||
<CloseIcon w={'12px'} />
|
||||
</Box>
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -130,7 +130,7 @@ export const aiproxyIdMap: Record<
|
||||
provider: 'Ollama'
|
||||
},
|
||||
23: {
|
||||
label: i18nT('account_model:Hunyuan'),
|
||||
label: 'OneAPI',
|
||||
provider: 'Hunyuan'
|
||||
},
|
||||
44: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChatHistoryItemResType, ChatItemType } from '@fastgpt/global/core/chat/
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
export const isLLMNode = (item: ChatHistoryItemResType) =>
|
||||
const isLLMNode = (item: ChatHistoryItemResType) =>
|
||||
item.moduleType === FlowNodeTypeEnum.chatNode || item.moduleType === FlowNodeTypeEnum.tools;
|
||||
|
||||
export function transformPreviewHistories(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
||||
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
|
||||
@@ -10,8 +9,6 @@ export type PostWorkflowDebugProps = {
|
||||
edges: RuntimeEdgeItemType[];
|
||||
variables: Record<string, any>;
|
||||
appId: string;
|
||||
query?: UserChatItemValueItemType[];
|
||||
history?: ChatItemType[];
|
||||
};
|
||||
|
||||
export type PostWorkflowDebugResponse = {
|
||||
@@ -19,6 +16,5 @@ export type PostWorkflowDebugResponse = {
|
||||
finishedEdges: RuntimeEdgeItemType[];
|
||||
nextStepRunNodes: RuntimeNodeItemType[];
|
||||
flowResponses: ChatHistoryItemResType[];
|
||||
workflowInteractiveResponse?: WorkflowInteractiveResponseType;
|
||||
newVariables: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import React from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import MyDivider from '@fastgpt/web/components/common/MyDivider';
|
||||
|
||||
const NotificationDetailsModal = ({ inform, onClose }: { inform: any; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const textStyles = {
|
||||
title: {
|
||||
color: 'grayModern.900',
|
||||
fontSize: '20px',
|
||||
fontWeight: 'medium',
|
||||
lineHeight: 6,
|
||||
letterSpacing: '0.15px'
|
||||
},
|
||||
time: {
|
||||
color: 'grayModern.500',
|
||||
fontSize: '12px',
|
||||
lineHeight: 5,
|
||||
letterSpacing: '0.25px'
|
||||
}
|
||||
};
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={!!inform}
|
||||
iconSrc={'support/user/informLight'}
|
||||
title={t('account_inform:notification_detail')}
|
||||
onClose={onClose}
|
||||
iconColor="blue.600"
|
||||
maxW="680px"
|
||||
maxH="80vh"
|
||||
>
|
||||
<Flex flexDirection="column" p={8}>
|
||||
<Flex
|
||||
{...textStyles.time}
|
||||
fontFamily="PingFang SC"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
alignSelf="stretch"
|
||||
>
|
||||
<Box {...textStyles.title} fontFamily="PingFang SC">
|
||||
{inform.title}
|
||||
</Box>
|
||||
<Box {...textStyles.time} ml={3} flex={1} fontFamily="PingFang SC">
|
||||
{t(formatTimeToChatTime(inform.time) as any).replace('#', ':')}
|
||||
</Box>
|
||||
<MyTag
|
||||
colorSchema={inform.teamId ? 'green' : 'blue'}
|
||||
mr={2}
|
||||
fontSize="xs"
|
||||
fontWeight="medium"
|
||||
showDot={false}
|
||||
type="fill"
|
||||
>
|
||||
{inform.teamId ? t('account_inform:team') : t('account_inform:system')}
|
||||
</MyTag>
|
||||
</Flex>
|
||||
<MyDivider my={4} />
|
||||
|
||||
<Box fontSize="sm" lineHeight={1.8}>
|
||||
<Markdown source={inform?.content} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NotificationDetailsModal);
|
||||
@@ -33,7 +33,6 @@ type ModelTestItem = {
|
||||
status: 'waiting' | 'running' | 'success' | 'error';
|
||||
message?: string;
|
||||
duration?: number;
|
||||
loading?: boolean; // 单个模型的loading状态
|
||||
};
|
||||
|
||||
const ModelTest = ({
|
||||
@@ -86,8 +85,7 @@ const ModelTest = ({
|
||||
</HStack>
|
||||
),
|
||||
model: modelData.model,
|
||||
status: 'waiting',
|
||||
loading: false
|
||||
status: 'waiting'
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as ModelTestItem[];
|
||||
@@ -95,65 +93,64 @@ const ModelTest = ({
|
||||
}
|
||||
});
|
||||
|
||||
const { runAsync: onStartTest, loading: isAnyModelLoading } = useRequest2(
|
||||
const { runAsync: onStartTest, loading: isTesting } = useRequest2(
|
||||
async () => {
|
||||
let errorNum = 0;
|
||||
setTestModelList((prev) => prev.map((item) => ({ ...item, loading: true })));
|
||||
{
|
||||
let errorNum = 0;
|
||||
const testModel = async (model: string) => {
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model ? { ...item, status: 'running', message: '' } : item
|
||||
)
|
||||
);
|
||||
const start = Date.now();
|
||||
try {
|
||||
await getTestModel({ model, channelId });
|
||||
const duration = Date.now() - start;
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model
|
||||
? { ...item, status: 'success', duration: duration / 1000 }
|
||||
: item
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model
|
||||
? { ...item, status: 'error', message: getErrText(error) }
|
||||
: item
|
||||
)
|
||||
);
|
||||
errorNum++;
|
||||
}
|
||||
};
|
||||
|
||||
const testModel = async (model: string) => {
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model ? { ...item, status: 'running', message: '' } : item
|
||||
)
|
||||
await batchRun(
|
||||
testModelList.map((item) => item.model),
|
||||
testModel,
|
||||
5
|
||||
);
|
||||
const start = Date.now();
|
||||
try {
|
||||
await getTestModel({ model, channelId });
|
||||
const duration = Date.now() - start;
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model
|
||||
? { ...item, status: 'success', duration: duration / 1000, loading: false }
|
||||
: item
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model
|
||||
? { ...item, status: 'error', message: getErrText(error), loading: false }
|
||||
: item
|
||||
)
|
||||
);
|
||||
errorNum++;
|
||||
|
||||
if (errorNum > 0) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('account_model:test_failed', { num: errorNum })
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await batchRun(
|
||||
testModelList.map((item) => item.model),
|
||||
testModel,
|
||||
5
|
||||
);
|
||||
|
||||
if (errorNum > 0) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('account_model:test_failed', { num: errorNum })
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
refreshDeps: [testModelList]
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onTestOneModel, loading: testingOneModel } = useRequest2(
|
||||
async (model: string) => {
|
||||
const start = Date.now();
|
||||
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model ? { ...item, status: 'running', message: '', loading: true } : item
|
||||
item.model === model ? { ...item, status: 'running', message: '' } : item
|
||||
)
|
||||
);
|
||||
|
||||
@@ -163,17 +160,13 @@ const ModelTest = ({
|
||||
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model
|
||||
? { ...item, status: 'success', duration: duration / 1000, loading: false }
|
||||
: item
|
||||
item.model === model ? { ...item, status: 'success', duration: duration / 1000 } : item
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
setTestModelList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.model === model
|
||||
? { ...item, status: 'error', message: getErrText(error), loading: false }
|
||||
: item
|
||||
item.model === model ? { ...item, status: 'error', message: getErrText(error) } : item
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -183,7 +176,7 @@ const ModelTest = ({
|
||||
}
|
||||
);
|
||||
|
||||
const isTesting = isAnyModelLoading || testingOneModel;
|
||||
const isTestLoading = testingOneModel || isTesting;
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
@@ -228,14 +221,14 @@ const ModelTest = ({
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>
|
||||
{(!isAnyModelLoading || item.loading) && (
|
||||
<MyIconButton
|
||||
isLoading={item.loading}
|
||||
icon={'core/chat/sendLight'}
|
||||
tip={t('account:model.test_model')}
|
||||
onClick={() => onTestOneModel(item.model)}
|
||||
/>
|
||||
)}
|
||||
<MyIconButton
|
||||
isLoading={isTestLoading}
|
||||
icon={'core/chat/sendLight'}
|
||||
tip={t('account:model.test_model')}
|
||||
onClick={() => {
|
||||
onTestOneModel(item.model);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
@@ -248,7 +241,7 @@ const ModelTest = ({
|
||||
<Button mr={4} variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button isLoading={isTesting} variant={'primary'} onClick={onStartTest}>
|
||||
<Button isLoading={isTestLoading} variant={'primary'} onClick={onStartTest}>
|
||||
{t('account_model:start_test', { num: testModelList.length })}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -62,11 +62,11 @@ function GroupEditModal({
|
||||
status: 'active',
|
||||
withOrgs: true,
|
||||
searchKey
|
||||
},
|
||||
throttleWait: 500,
|
||||
debounceWait: 200,
|
||||
refreshDeps: [searchKey]
|
||||
}
|
||||
});
|
||||
const refetchMemberList = _.debounce(refreshList, 200);
|
||||
|
||||
useEffect(() => refetchMemberList, [searchKey]);
|
||||
|
||||
const groupId = useMemo(() => String(group._id), [group._id]);
|
||||
|
||||
|
||||
@@ -48,14 +48,14 @@ export function ChangeOwnerModal({
|
||||
{
|
||||
pageSize: 20,
|
||||
params: {
|
||||
searchKey
|
||||
},
|
||||
refreshDeps: [searchKey],
|
||||
debounceWait: 200,
|
||||
throttleWait: 500
|
||||
searchKey: searchKey
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const search = _.debounce(refreshList, 500);
|
||||
useEffect(() => search, [searchKey]);
|
||||
|
||||
const {
|
||||
isOpen: isOpenMemberListMenu,
|
||||
onClose: onCloseMemberListMenu,
|
||||
|
||||