Compare commits

..

32 Commits
v3.4 ... v3.5

Author SHA1 Message Date
archer
1226c3efb7 feat: chat ui 2023-05-12 00:09:10 +08:00
archer
39f9080eb2 perf: style 2023-05-11 21:19:43 +08:00
archer
3e4b165ed9 perf: ui 2023-05-11 17:29:46 +08:00
archer
451f234f68 perf: ssr and iphone daapt 2023-05-11 09:50:40 +08:00
archer
1ab45651e0 docs 2023-05-10 21:35:43 +08:00
archer
16f2ad7615 docs 2023-05-10 21:09:53 +08:00
archer
4ba4a99935 docs 2023-05-10 20:15:44 +08:00
archer
6f4471d2a0 merge 2023-05-10 19:59:19 +08:00
archer
250399a1aa docs 2023-05-10 18:28:20 +08:00
archer
1c8ce369b6 README 2023-05-10 15:29:17 +08:00
kense
875f78b42c fix: filter system prompt 字符串判空
修复 filterSystemPrompt为'\n'时, 判断有问题
2023-05-10 15:19:42 +08:00
archer
1e262a2198 docs 2023-05-10 15:18:16 +08:00
Archer
25067a14a6 Create LICENSE 2023-05-10 14:24:14 +08:00
archer
591cc21ff4 deploy docs 2023-05-10 14:14:36 +08:00
archer
5a21eb9bc1 perf: docs 2023-05-10 14:14:19 +08:00
archer
cdf4b9f324 perf: prompt and gpt4 2023-05-10 12:03:54 +08:00
archer
e3c9b8179e perf: message filter 2023-05-09 23:40:02 +08:00
archer
9b683884cc perf: overflow;load history 2023-05-09 22:54:39 +08:00
archer
4dc541e0a6 perf: search prompt 2023-05-09 21:54:07 +08:00
archer
ef2de489be perf: paging position 2023-05-09 21:30:51 +08:00
archer
cb3b9efc6e perf: filter prompt 2023-05-09 19:11:21 +08:00
archer
a745993829 perf: search prompt 2023-05-09 19:04:42 +08:00
archer
a837552b56 perf: search prompt 2023-05-09 16:32:02 +08:00
archer
f52f514f5f feat: callback training data 2023-05-09 13:20:52 +08:00
archer
fac53923dd perf: embedding price and img 2023-05-09 12:45:23 +08:00
archer
b200731d17 feat: cookie expired time 2023-05-09 11:26:11 +08:00
archer
de6ac0f589 perf: refresh page.img mode.collection filter 2023-05-09 11:13:33 +08:00
archer
18e0212d27 perf: ui show;fix: avatar 2023-05-08 18:44:10 +08:00
archer
f20a5fe9a6 perf: new chat modal 2023-05-08 14:19:28 +08:00
archer
d351084688 fix: openapi key unsave 2023-05-08 11:13:20 +08:00
archer
d807f9d097 deploy file 2023-05-08 10:46:33 +08:00
archer
3ef6d3fe63 deploy docs 2023-05-08 10:37:52 +08:00
91 changed files with 1593 additions and 787 deletions

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

291
README.md
View File

@@ -2,282 +2,43 @@
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,目前集成了 gpt35 和 embedding. 可构建自己的知识库。
## 知识库原理
## 🛸 在线体验
🎉 [fastgpt.run](https://fastgpt.run/) (国内版)
🎉 [ai.fastgpt.run](https://ai.fastgpt.run/) (海外版)
![Demo](docs/imgs/demo.png?raw=true 'demo')
#### 知识库原理图
![KBProcess](docs/imgs/KBProcess.jpg?raw=true 'KBProcess')
## 开发
## 👨‍💻 开发
**配置环境变量**
复制.env.template 文件,生成一个.env.local 环境变量文件夹,修改.env.local 内容,参考下方:
项目技术栈: NextJs + TS + ChakraUI + Mongo + PostgresVector 插件)
这是一个平台项目,非单机项目,除了模型调用外还涉及非常多用户的内容。
[本地开发 Quick Start](docs/dev/README.md)
```bash
# proxy可选
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT=7890
# openai 中转连接(可选)
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_BASE_URL_AUTH=可选的安全凭证
# 是否开启队列任务。 1-开启0-关闭(请求 parentUrl 去执行任务,单机时直接填1
queueTask=1
parentUrl=https://hostname/api/openapi/startEvents
# 发送邮箱验证码配置。参考 nodeMail 获取参数,自行百度。
MY_MAIL=xxx@qq.com
MAILE_CODE=xxx
# 阿里短信服务(邮箱和短信至少二选一)
aliAccessKeyId=xxx
aliAccessKeySecret=xxx
aliSignName=xxx
aliTemplateCode=SMS_xxx
# token随便填作为登录凭证
TOKEN_KEY=xxx
# openai key
OPENAIKEY=sk-xxx
# mongo连接地址
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
# mongo数据库名称
MONGODB_NAME=xxx
# pg 数据库相关内容,和 docker-compose pg 部分对上
PG_HOST=0.0.0.0
PG_PORT=8102
PG_USER=fastgpt
PG_PASSWORD=1234
PG_DB_NAME=fastgpt
```
## 🚀 私有化部署
**运行**
[docker-compose 部署教程](docs/deploy/docker.md)
```
pnpm dev
```
## :point_right: RoadMap
## 部署
- [FastGpt RoadMap](https://kjqvjse66l.feishu.cn/docx/RVUxdqE2WolDYyxEKATcM0XXnte)
### 代理环境(国外服务器可忽略)
## 🏘️ 交流群
1. [clash 方案](./docs/proxy/clash.md) - 仅需一台服务器(需要有 clash
2. [nginx 方案](./docs/proxy/nginx.md) - 需要一台国外服务器
3. [cloudflare 方案](./docs/proxy/cloudflare.md) - 需要有域名(每日免费 10w 次代理请求)
wx: fastgpt123
![Demo](docs/imgs/wx300.jpg?raw=true 'wx')
### docker 部署
## 👀 其他
#### 1. 安装 docker 和 docker-compose
- [FastGpt 常见问题](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
- [FastGpt + Laf 最佳实践,将知识库装入公众号,点击去 Laf 公众号体验效果](https://hnvacz-laf-upload-ai.oss.laf.run/3ffd528ee2f9ae1dcd3508fe9994dd9.png)
- [FastGpt V3.4 更新集合](https://www.bilibili.com/video/BV1Lo4y147Qh/?vd_source=92041a1a395f852f9d89158eaa3f61b4)
- [FastGpt 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
这个不同系统略有区别,百度安装下。验证安装成功后进行下一步。下面给出一个例子:
## 🌟 Star History
```bash
# 安装docker
curl -L https://get.daocloud.io/docker | sh
sudo systemctl start docker
# 安装 docker-compose
curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# 验证安装
docker -v
docker-compose -v
```
#### 2. 创建 3 个初始化文件
手动创建或者直接把 deploy 里内容复制过去
**/root/fast-gpt/pg/init.sql**
```sql
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE EXTENSION vector;
-- init table
CREATE TABLE modelData (
id BIGSERIAL PRIMARY KEY,
vector VECTOR(1536),
status VARCHAR(50) NOT NULL,
user_id VARCHAR(50) NOT NULL,
model_id VARCHAR(50) NOT NULL,
q TEXT NOT NULL,
a TEXT NOT NULL
);
-- create index
CREATE INDEX modelData_status_index ON modelData USING HASH (status);
CREATE INDEX modelData_userId_index ON modelData USING HASH (user_id);
CREATE INDEX modelData_modelId_index ON modelData USING HASH (model_id);
EOSQL
```
**/root/fast-gpt/nginx/nginx.conf**
```conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 51200;
events {
worker_connections 1024;
}
http {
resolver 8.8.8.8;
proxy_ssl_server_name on;
access_log off;
server_names_hash_bucket_size 512;
client_header_buffer_size 64k;
large_client_header_buffers 4 64k;
client_max_body_size 50M;
proxy_connect_timeout 240s;
proxy_read_timeout 240s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 8k;
gzip_http_version 1.1;
gzip_comp_level 6;
gzip_vary on;
gzip_types text/plain application/x-javascript text/css application/javascript application/json application/xml;
gzip_disable "MSIE [1-6]\.";
open_file_cache max=1000 inactive=1d;
open_file_cache_valid 30s;
open_file_cache_min_uses 8;
open_file_cache_errors off;
server {
listen 443 ssl;
server_name docgpt.ahapocket.cn;
ssl_certificate /ssl/docgpt.pem;
ssl_certificate_key /ssl/docgpt.key;
ssl_session_timeout 5m;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name docgpt.ahapocket.cn;
rewrite ^(.*) https://$server_name$1 permanent;
}
}
```
**/root/fast-gpt/docker-compose.yml**
```yml
version: '3.3'
services:
fast-gpt:
image: c121914yu/fast-gpt:latest
network_mode: host
restart: always
container_name: fast-gpt
environment:
# - AXIOS_PROXY_HOST=127.0.0.1
# - AXIOS_PROXY_PORT=7890
# - OPENAI_BASE_URL=https://api.openai.com/v1
# - OPENAI_BASE_URL_AUTH=可选的安全凭证
- MY_MAIL=xxxx@qq.com
- MAILE_CODE=xxxx
- aliAccessKeyId=xxxx
- aliAccessKeySecret=xxxx
- aliSignName=xxxxx
- aliTemplateCode=SMS_xxxx
- TOKEN_KEY=xxxx
- queueTask=1
- parentUrl=https://hostname/api/openapi/startEvents
- MONGODB_URI=mongodb://username:passsword@0.0.0.0:27017/?authSource=admin
- MONGODB_NAME=xxx
- PG_HOST=0.0.0.0
- PG_PORT=8100
- PG_USER=fastgpt
- PG_PASSWORD=1234
- PG_DB_NAME=fastgpt
- OPENAIKEY=sk-xxxxx
nginx:
image: nginx:alpine3.17
container_name: nginx
restart: always
network_mode: host
volumes:
- /root/fast-gpt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /root/fast-gpt/nginx/logs:/var/log/nginx
- /root/fast-gpt/nginx/ssl/docgpt.key:/ssl/docgpt.key
- /root/fast-gpt/nginx/ssl/docgpt.pem:/ssl/docgpt.pem
pg:
image: ankane/pgvector
container_name: pg
restart: always
ports:
- 8100:5432
environment:
- POSTGRES_USER=fastgpt
- POSTGRES_PASSWORD=1234
- POSTGRES_DB=fastgpt
volumes:
- /root/fast-gpt/pg/data:/var/lib/postgresql/data
- /root/fast-gpt/pg/init.sql:/docker-entrypoint-initdb.d/init.sh
- /etc/localtime:/etc/localtime:ro
mongodb:
image: mongo:6.0.4
container_name: mongo
restart: always
ports:
- 27017:27017
environment:
- MONGO_INITDB_ROOT_USERNAME=username
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- /root/fast-gpt/mongo/data:/data/db
- /root/fast-gpt/mongo/logs:/var/log/mongodb
- /etc/localtime:/etc/localtime:ro
```
#### 3. 运行 docker-compose
下面是一个辅助脚本,也可以直接 docker-compose up -d
**run.sh 运行文件**
```bash
#!/bin/bash
docker-compose pull
docker-compose up -d
echo "Docker Compose 重新拉取镜像完成!"
# 删除本地旧镜像
images=$(docker images --format "{{.ID}} {{.Repository}}" | grep fast-gpt)
# 将镜像 ID 和名称放入数组中
IFS=$'\n' read -rd '' -a image_array <<<"$images"
# 遍历数组并删除所有旧的镜像
for ((i=1; i<${#image_array[@]}; i++))
do
image=${image_array[$i]}
image_id=${image%% *}
docker rmi $image_id
done
```
## 其他优化点
### Git Action 自动打包镜像
.github 里拥有一个 git 提交到 main 分支时自动打包 amd64 和 arm64 镜像的 actions。你仅需要提前在 git 配置好 session。
1. 创建账号 session: 头像 -> settings -> 最底部 Developer settings -> Personal access tokens -> tokens(classic) -> 创建新 session把一些看起来需要的权限勾上。
2. 添加 session 到仓库: 仓库 -> settings -> Secrets and variables -> Actions -> 创建 secret
3. 填写 secret: Name-GH_PAT, Secret-第一步的 tokens
## 其他问题
### Mac 可能的问题
> 因为教程有部分镜像不兼容 arm64所以写个文档指导新手如何快速在 mac 上面搭建 fast-gpt[如何在 mac 上面部署 fastgpt](./docs/mac.md)
[![Star History Chart](https://api.star-history.com/svg?repos=c121914yu/FastGPT&type=Date)](https://star-history.com/#c121914yu/FastGPT&Date)

View File

@@ -1,66 +0,0 @@
version: '3.3'
services:
fast-gpt:
image: c121914yu/fast-gpt:latest
network_mode: host
restart: always
container_name: fast-gpt
environment:
# - AXIOS_PROXY_HOST=127.0.0.1
# - AXIOS_PROXY_PORT=7890
# - OPENAI_BASE_URL=https://api.openai.com/v1
# - OPENAI_BASE_URL_AUTH=可选的安全凭证
- MY_MAIL=xxxx@qq.com
- MAILE_CODE=xxxx
- aliAccessKeyId=xxxx
- aliAccessKeySecret=xxxx
- aliSignName=xxxxx
- aliTemplateCode=SMS_xxxx
- TOKEN_KEY=xxxx
- queueTask=1
- parentUrl=https://hostname/api/openapi/startEvents
- MONGODB_URI=mongodb://username:passsword@0.0.0.0:27017/?authSource=admin
- MONGODB_NAME=xxx
- PG_HOST=0.0.0.0
- PG_PORT=8100
- PG_USER=xxx
- PG_PASSWORD=xxx
- PG_DB_NAME=xxx
- OPENAIKEY=sk-xxxxx
nginx:
image: nginx:alpine3.17
container_name: nginx
restart: always
network_mode: host
volumes:
- /root/fast-gpt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /root/fast-gpt/nginx/logs:/var/log/nginx
- /root/fast-gpt/nginx/ssl/docgpt.key:/ssl/docgpt.key
- /root/fast-gpt/nginx/ssl/docgpt.pem:/ssl/docgpt.pem
pg:
image: ankane/pgvector
container_name: pg
restart: always
ports:
- 8100:5432
environment:
- POSTGRES_USER=xxx
- POSTGRES_PASSWORD=xxx
- POSTGRES_DB=xxx
volumes:
- /root/fast-gpt/pg/data:/var/lib/postgresql/data
- /root/fast-gpt/pg/init.sql:/docker-entrypoint-initdb.d/init.sh
- /etc/localtime:/etc/localtime:ro
mongodb:
image: mongo:6.0.4
container_name: mongo
restart: always
ports:
- 27017:27017
environment:
- MONGO_INITDB_ROOT_USERNAME=username
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- /root/fast-gpt/mongo/data:/data/db
- /root/fast-gpt/mongo/logs:/var/log/mongodb
- /etc/localtime:/etc/localtime:ro

View File

@@ -1 +0,0 @@
sudo chmod +x /usr/local/bin/docker-compose

255
docs/deploy/docker.md Normal file
View File

@@ -0,0 +1,255 @@
# Docker 部署 FastGpt
## 代理环境(国外服务器可忽略)
选择一个即可。
1. [sealos nginx 方案](./proxy/sealos.md) - 推荐。约等于不用钱,不需要额外准备任何东西。
2. [clash 方案](./proxy/clash.md) - 仅需一台服务器(需要有 clash
3. [nginx 方案](./proxy/nginx.md) - 需要一台国外服务器
4. [cloudflare 方案](./proxy/cloudflare.md) - 需要有域名(每日免费 10w 次代理请求)
### 1. 准备一些内容
> 1. 服务器开通 80 端口。用代理的话,对应的代理端口也需要打开。
> 2. QQ 邮箱 Code进入 QQ 邮箱 -> 账号 -> 申请 SMTP 账号
> 3. 有域名的准备好 SSL 证书
### 2. 安装 docker 和 docker-compose
这个不同系统略有区别,百度安装下。验证安装成功后进行下一步。下面给出一个例子:
```bash
# 安装docker
curl -L https://get.daocloud.io/docker | sh
sudo systemctl start docker
# 安装 docker-compose
curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# 验证安装
docker -v
docker-compose -v
# 如果docker-compose运行不了可以把 deploy/fastgpt/docker-compose 文件复制到服务器,然后在 docker-compose 文件夹里执行 sh init.sh。会把docker-compose文件复制到对应目录。
```
### 2. 创建 3 个初始化文件
手动创建或者直接把 fastgpt 文件夹复制过去。
**/root/fastgpt/pg/init.sql PG 数据库初始化**
```sql
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE EXTENSION vector;
-- init table
CREATE TABLE modelData (
id BIGSERIAL PRIMARY KEY,
vector VECTOR(1536),
status VARCHAR(50) NOT NULL,
user_id VARCHAR(50) NOT NULL,
model_id VARCHAR(50) NOT NULL,
q TEXT NOT NULL,
a TEXT NOT NULL
);
-- create index
CREATE INDEX modelData_status_index ON modelData USING HASH (status);
CREATE INDEX modelData_userId_index ON modelData USING HASH (user_id);
CREATE INDEX modelData_modelId_index ON modelData USING HASH (model_id);
EOSQL
```
**/root/fastgpt/nginx/nginx.conf Nginx 配置**
```conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 51200;
events {
worker_connections 1024;
}
http {
resolver 8.8.8.8;
proxy_ssl_server_name on;
access_log off;
server_names_hash_bucket_size 512;
client_header_buffer_size 64k;
large_client_header_buffers 4 64k;
client_max_body_size 50M;
proxy_connect_timeout 240s;
proxy_read_timeout 240s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 8k;
gzip_http_version 1.1;
gzip_comp_level 6;
gzip_vary on;
gzip_types text/plain application/x-javascript text/css application/javascript application/json application/xml;
gzip_disable "MSIE [1-6]\.";
open_file_cache max=1000 inactive=1d;
open_file_cache_valid 30s;
open_file_cache_min_uses 8;
open_file_cache_errors off;
server {
listen 443 ssl;
# 改成自己的域名和证书
server_name docgpt.ahapocket.cn;
ssl_certificate /ssl/docgpt.pem;
ssl_certificate_key /ssl/docgpt.key;
ssl_session_timeout 5m;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name docgpt.ahapocket.cn;
rewrite ^(.*) https://$server_name$1 permanent;
}
}
```
**/root/fastgpt/docker-compose.yml 核心部署文件**
环境变量内容和开发时的环境变量基本相同,除了数据库的地址。
```yml
version: '3.3'
services:
pg:
image: ankane/pgvector:v0.4.1
container_name: pg
restart: always
ports:
- 8100:5432
environment:
# 这里的配置只有首次运行生效。修改后,重启镜像是不会生效的。需要把持久化数据删除再重启,才有效果
- POSTGRES_USER=fastgpt
- POSTGRES_PASSWORD=1234
- POSTGRES_DB=fastgpt
volumes:
# 刚创建的文件
- /root/fastgpt/pg/init.sql:/docker-entrypoint-initdb.d/init.sh
- /root/fastgpt/pg/data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
mongodb:
image: mongo:6.0.4
container_name: mongo
restart: always
ports:
- 27017:27017
environment:
# 这里的配置只有首次运行生效。修改后,重启镜像是不会生效的。需要把持久化数据删除再重启,才有效果
- MONGO_INITDB_ROOT_USERNAME=username
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- /root/fastgpt/mongo/data:/data/db
- /root/fastgpt/mongo/logs:/var/log/mongodb
- /etc/localtime:/etc/localtime:ro
fast-gpt:
image: c121914yu/fast-gpt:latest
network_mode: host
restart: always
container_name: fast-gpt
environment:
# proxy可选
- AXIOS_PROXY_HOST=127.0.0.1
- AXIOS_PROXY_PORT=7890
# openai 中转连接(可选)
- OPENAI_BASE_URL=https://api.openai.com/v1
- OPENAI_BASE_URL_AUTH=可选的安全凭证
# 是否开启队列任务。 1-开启0-关闭(请求 parentUrl 去执行任务,单机时直接填1
- queueTask=1
- parentUrl=https://hostname/api/openapi/startEvents
# 发送邮箱验证码配置。用的是QQ邮箱。参考 nodeMail 获取MAILE_CODE自行百度。
- MY_MAIL=xxxx@qq.com
- MAILE_CODE=xxxx
# 阿里短信服务(邮箱和短信至少二选一)
- aliAccessKeyId=xxxx
- aliAccessKeySecret=xxxx
- aliSignName=xxxxx
- aliTemplateCode=SMS_xxxx
# token加密凭证随便填作为登录凭证
- TOKEN_KEY=xxxx
# 和上方mongo镜像的username,password对应
- MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin
- MONGODB_NAME=fastgpt
- PG_HOST=0.0.0.0
- PG_PORT=8100
# 和上方PG镜像对应.
- PG_USER=fastgpt # POSTGRES_USER
- PG_PASSWORD=1234 # POSTGRES_PASSWORD
- PG_DB_NAME=fastgpt # POSTGRES_DB
# openai api key
- OPENAIKEY=sk-xxxxx
nginx:
image: nginx:alpine3.17
container_name: nginx
restart: always
network_mode: host
volumes:
# 刚创建的文件
- /root/fastgpt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /root/fastgpt/nginx/logs:/var/log/nginx
# https证书没有的话不填对应的nginx.conf也要修改
- /root/fastgpt/nginx/ssl/docgpt.key:/ssl/docgpt.key
- /root/fastgpt/nginx/ssl/docgpt.pem:/ssl/docgpt.pem
```
### 3. 运行 docker-compose
下面是一个辅助脚本,也可以直接 docker-compose up -d
**run.sh 运行文件**
```bash
#!/bin/bash
docker-compose pull
docker-compose up -d
echo "Docker Compose 重新拉取镜像完成!"
# 删除本地旧镜像
images=$(docker images --format "{{.ID}} {{.Repository}}" | grep fast-gpt)
# 将镜像 ID 和名称放入数组中
IFS=$'\n' read -rd '' -a image_array <<<"$images"
# 遍历数组并删除所有旧的镜像
for ((i=1; i<${#image_array[@]}; i++))
do
image=${image_array[$i]}
image_id=${image%% *}
docker rmi $image_id
done
```
## 其他优化点
# Git Action 自动打包镜像
.github 里拥有一个 git 提交到 main 分支时自动打包 amd64 和 arm64 镜像的 actions。你仅需要提前在 git 配置好 session。
1. 创建账号 session: 头像 -> settings -> 最底部 Developer settings -> Personal access tokens -> tokens(classic) -> 创建新 session把一些看起来需要的权限勾上。
2. 添加 session 到仓库: 仓库 -> settings -> Secrets and variables -> Actions -> 创建 secret
3. 填写 secret: Name-GH_PAT, Secret-第一步的 tokens
## 其他问题
### Mac 可能的问题
> 因为教程有部分镜像不兼容 arm64所以写个文档指导新手如何快速在 mac 上面搭建 fast-gpt[在 mac 上面部署 fastgpt 可能存在的问题](./mac.md)

View File

@@ -0,0 +1,81 @@
version: '3.3'
services:
pg:
image: ankane/pgvector:v0.4.1
container_name: pg
restart: always
ports:
- 8100:5432
environment:
# 这里的配置只有首次运行生效。修改后,重启镜像是不会生效的。需要把持久化数据删除再重启,才有效果
- POSTGRES_USER=fastgpt
- POSTGRES_PASSWORD=1234
- POSTGRES_DB=fastgpt
volumes:
# 刚创建的文件
- /root/fastgpt/pg/init.sql:/docker-entrypoint-initdb.d/init.sh
- /root/fastgpt/pg/data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
mongodb:
image: mongo:6.0.4
container_name: mongo
restart: always
ports:
- 27017:27017
environment:
# 这里的配置只有首次运行生效。修改后,重启镜像是不会生效的。需要把持久化数据删除再重启,才有效果
- MONGO_INITDB_ROOT_USERNAME=username
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- /root/fastgpt/mongo/data:/data/db
- /root/fastgpt/mongo/logs:/var/log/mongodb
- /etc/localtime:/etc/localtime:ro
fast-gpt:
image: c121914yu/fast-gpt:latest
network_mode: host
restart: always
container_name: fast-gpt
environment:
# proxy可选
- AXIOS_PROXY_HOST=127.0.0.1
- AXIOS_PROXY_PORT=7890
# openai 中转连接(可选)
- OPENAI_BASE_URL=https://api.openai.com/v1
- OPENAI_BASE_URL_AUTH=可选的安全凭证
# 是否开启队列任务。 1-开启0-关闭(请求 parentUrl 去执行任务,单机时直接填1
- queueTask=1
- parentUrl=https://hostname/api/openapi/startEvents
# 发送邮箱验证码配置。用的是QQ邮箱。参考 nodeMail 获取MAILE_CODE自行百度。
- MY_MAIL=xxxx@qq.com
- MAILE_CODE=xxxx
# 阿里短信服务(邮箱和短信至少二选一)
- aliAccessKeyId=xxxx
- aliAccessKeySecret=xxxx
- aliSignName=xxxxx
- aliTemplateCode=SMS_xxxx
# token加密凭证随便填作为登录凭证
- TOKEN_KEY=xxxx
# 和上方mongo镜像的username,password对应
- MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin
- MONGODB_NAME=fastgpt
- PG_HOST=0.0.0.0
- PG_PORT=8100
# 和上方PG镜像对应.
- PG_USER=fastgpt # POSTGRES_USER
- PG_PASSWORD=1234 # POSTGRES_PASSWORD
- PG_DB_NAME=fastgpt # POSTGRES_DB
# openai api key
- OPENAIKEY=sk-xxxxx
nginx:
image: nginx:alpine3.17
container_name: nginx
restart: always
network_mode: host
volumes:
# 刚创建的文件
- /root/fastgpt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /root/fastgpt/nginx/logs:/var/log/nginx
# https证书没有的话不填对应的nginx.conf也要修改
- /root/fastgpt/nginx/ssl/docgpt.key:/ssl/docgpt.key
- /root/fastgpt/nginx/ssl/docgpt.pem:/ssl/docgpt.pem

View File

@@ -0,0 +1,2 @@
cp ./docker-compose /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

View File

@@ -54,4 +54,16 @@ http {
server_name docgpt.ahapocket.cn;
rewrite ^(.*) https://$server_name$1 permanent;
}
server {
listen 3000;
server_name 120.0.0.0;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

View File

@@ -1,22 +1,22 @@
## 怎么在mac上面部署fastgpt
## Mac 上部署可能遇到的问题
### 前置条件
1、可以 curl api.openai.com
1、可以 curl api.openai.com
2、有openai key
2、有 openai key
3、有邮箱MAILE_CODE
3、有邮箱 MAILE_CODE
4、有docker
4、有 docker
```
docker -v
```
5、有pnpm ,可以使用`brew install pnpm`安装
5、有 pnpm ,可以使用`brew install pnpm`安装
6、需要创建一个放置pgmongo数据的文件夹这里创建在`~/fastgpt`目录中,里面有`pg``mongo `两个文件夹
6、需要创建一个放置 pgmongo 数据的文件夹,这里创建在`~/fastgpt`目录中,里面有`pg``mongo `两个文件夹
```
➜ fast-gpt pwd
@@ -25,11 +25,9 @@ docker -v
mongo pg
```
### docker 部署方式
### docker部署方式
这种方式主要是为了方便调试,可以使用`pnpm dev ` 运行fast-gpt项目
这种方式主要是为了方便调试,可以使用`pnpm dev ` 运行 fast-gpt 项目
**1、.env.local 文件**
@@ -60,19 +58,19 @@ PG_PASSWORD=xxx
PG_DB_NAME=xxx
```
**2、部署mongo**
**2、部署 mongo**
```
docker run --name mongo -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=username -e MONGO_INITDB_ROOT_PASSWORD=password -v ~/fast-gpt/mongo/data:/data/db -d mongo:4.0.1
```
**3、部署pgsql**
**3、部署 pgsql**
```
docker run -it --name pg -e "POSTGRES_PASSWORD=xxx" -e POSTGRES_USER=xxx -p 8100:5432 -v ~/fast-gpt/pg/data:/var/lib/postgresql/data -d octoberlan/pgvector:v0.4.1
```
进pgsql容器运行
pgsql 容器运行
```
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
@@ -95,6 +93,4 @@ CREATE INDEX modelData_userId_index ON modelData (userId);
EOSQL
```
4、**最后在FASTGPT项目里面运行pnpm dev 运行项目然后进入localhost:3000 看项目是否跑起来了**
4、**最后在 FASTGPT 项目里面运行 pnpm dev 运行项目,然后进入 localhost:3000 看项目是否跑起来了**

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

View File

@@ -1,4 +1,5 @@
# nginx 反向代理 openai 接口
如果你有国外的服务器,可以通过配置 nginx 反向代理,转发 openai 相关的请求,从而让国内的服务器可以通过访问该 nginx 去访问 openai 接口。
```conf
@@ -19,7 +20,7 @@ http {
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
client_max_body_size 50M;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 8k;
@@ -43,10 +44,10 @@ http {
location ~ /openai/(.*) {
# auth check
if ($http_authkey != "xxxxxx") {
if ($auth != "xxxxxx") {
return 403;
}
proxy_pass https://api.openai.com/$1$is_args$args;
proxy_set_header Host api.openai.com;
proxy_set_header X-Real-IP $remote_addr;
@@ -69,4 +70,4 @@ http {
rewrite ^(.*) https://$server_name$1 permanent;
}
}
```
```

100
docs/deploy/proxy/sealos.md Normal file
View File

@@ -0,0 +1,100 @@
# sealos 部署 openai 中转
## 登录 sealos cloud
[sealos cloud](https://cloud.sealos.io/)
## 创建应用
打开 App Launchpad -> 新建应用
![step1](./imgs//sealos1.png)
![step2](./imgs//sealos2.png)
### 开启外网访问
![step3](./imgs//sealos3.png)
### 添加 configmap 文件
1. 复制下面这段代码,注意 `server_name` 后面的内容替换成上图的地址。
```
user nginx;
worker_processes auto;
worker_rlimit_nofile 51200;
events {
worker_connections 1024;
}
http {
resolver 8.8.8.8;
proxy_ssl_server_name on;
access_log off;
server_names_hash_bucket_size 512;
client_header_buffer_size 64k;
large_client_header_buffers 4 64k;
client_max_body_size 50M;
proxy_connect_timeout 240s;
proxy_read_timeout 240s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
server {
listen 80;
server_name tgohwtdlrmer.cloud.sealos.io; # 这个地方替换成 sealos 提供的内容
location ~ /openai/(.*) {
# auth check
if ($http_auth != "auth") { # 安全凭证
return 403;
}
proxy_pass https://api.openai.com/$1$is_args$args;
proxy_set_header Host api.openai.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 如果响应是流式的
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
# 如果响应是一般的
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
}
```
2. 点开高级配置
3. 点击新增 configmap
4. 文件名写: `/etc/nginx/nginx.conf`
5. 文件值为刚刚复制的那段代码
6. 点击确认
![step4](./imgs//sealos4.png)
### 部署应用
填写完毕后,点击右上角的 `部署应用`,即可完成。
## 修改 FastGpt 环境变量
1. 进入刚刚部署应用的详情,复制外网地址
![step5](./imgs//sealos5.png)
2. 修改环境变量:
```
OPENAI_BASE_URL=https://tgohwtdlrmer.cloud.sealos.io/openai/v1
OPENAI_BASE_URL_AUTH=auth
```
**Done!**

47
docs/dev/README.md Normal file
View File

@@ -0,0 +1,47 @@
# FastGpt 本地开发
第一次开发,请先[部署教程](docs/deploy/docker.md),需要部署数据库.
## 环境变量配置
复制.env.template 文件,生成一个.env.local 环境变量文件夹,修改.env.local 里内容。
```bash
# proxy可选
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT=7890
# openai 中转连接(可选)
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_BASE_URL_AUTH=可选的安全凭证
# 是否开启队列任务。 1-开启0-关闭(请求 parentUrl 去执行任务,单机时直接填1
queueTask=1
parentUrl=https://hostname/api/openapi/startEvents
# 发送邮箱验证码配置。用的是 QQ 邮箱。参考 nodeMail 获取MAILE_CODE自行百度。
MY_MAIL=xxxx@qq.com
MAILE_CODE=xxxx
# 阿里短信服务(邮箱和短信至少二选一)
aliAccessKeyId=xxxx
aliAccessKeySecret=xxxx
aliSignName=xxxxx
aliTemplateCode=SMS_xxxx
# token加密凭证随便填作为登录凭证
TOKEN_KEY=xxxx
queueTask=1
parentUrl=https://hostname/api/openapi/startEvents
# 和mongo镜像的username,password对应
MONGODB_URI=mongodb://username:passsword@0.0.0.0:27017/?authSource=admin
MONGODB_NAME=xxx
PG_HOST=0.0.0.0
PG_PORT=8100
# 和PG镜像对应.
PG_USER=fastgpt # POSTGRES_USER
PG_PASSWORD=1234 # POSTGRES_PASSWORD
PG_DB_NAME=fastgpt # POSTGRES_DB
OPENAIKEY=sk-xxxxx
```
## 运行
```
pnpm dev
```

BIN
docs/imgs/demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

BIN
docs/imgs/wx300.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -11,4 +11,6 @@ chatgpt 上下文最长 4096 tokens, 会自动截取上下文,超过 4096 部
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/)
**其他问题**
请 WX 联系: fastgpt123
![FastGpt](/imgs/wx300.jpg)
| 交流群 | 小助手 |
| ----------------------- | -------------------- |
| ![](/imgs/wxqun300.jpg) | ![](/imgs/wx300.jpg) |

View File

@@ -1,43 +1,34 @@
## 欢迎使用 Fast GPT
[Git 仓库](https://github.com/c121914yu/FastGPT)
### 项目开源
### 交流群/问题反馈
FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑。项目地址:[Git 仓库](https://github.com/c121914yu/FastGPT)
扫码满了,加个小号,定时拉
wx 号: fastgpt123
![](/imgs/wx300.jpg)
### 开始使用知识库
### 快速开始
1. AI 助手详情里,有一个模型效果。打开知识库搜索开关即可使用知识库搜索功能。
2. 导入知识库数据。可以手动输入或文件导入。
3. 开始对话。
4. 对话结束后,会看到聊天下方有一个“查看提示词”,可以看到搜索到了哪些内容。
1. 使用手机号注册账号
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
3. 如果填写了自己的 openai 账号,使用时会直接用你的账号。如果没有填写,需要付费使用平台的账号。
4. 进入模型页,创建一个模型,建议直接用 ChatGPT。
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
注意使用知识库模型对话时tokens 消耗会加快
### 价格表
如果使用了自己的 Api Key不会计费。可以在账号页看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。
如果使用了自己的 Api Key不会计费。可以在账号页看到详细账单。
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| claude - 对话 | 免费 |
| chatgpt - 对话 | 0.03 |
| 知识库 - 对话 | 0.03 |
| 知识库 - 索引 | 0.004 |
| gpt4 - 对话 | 0.5 |
| 知识库 - 索引 | 免费 |
| 文件拆分 | 0.03 |
### 定制 prompt
### 交流群/问题反馈
1. 进入模型编辑页
2. 调整温度和提示词
3. 使用该模型对话。每次对话时,提示词和温度都会自动注入,方便管理个人的模型。建议把自己日常经常需要使用的 5~10 个方向预设好。
如果群满了,可加个小助手,定时拉
wx 号: fastgpt123
### 知识库
1. 创建模型时选择【知识库】
2. 进入模型编辑页
3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。
4. 使用该模型对话。
注意使用知识库模型对话时tokens 消耗会加快。
| 交流群 | 小助手 |
| ----------------------- | -------------------- |
| ![](/imgs/wxqun300.jpg) | ![](/imgs/wx300.jpg) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

BIN
public/imgs/wxqun300.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

9
public/js/particles.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -52,7 +52,10 @@ export const getExportDataList = (modelId: string) =>
* 获取模型正在拆分数据的数量
*/
export const getModelSplitDataListLen = (modelId: string) =>
GET<number>(`/model/data/getSplitData?modelId=${modelId}`);
GET<{
splitDataQueue: number;
embeddingQueue: number;
}>(`/model/data/getTrainingData?modelId=${modelId}`);
/**
* 获取 web 页面内容

View File

@@ -1,5 +1,5 @@
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { clearToken } from '@/utils/user';
import { clearCookie } from '@/utils/user';
import { TOKEN_ERROR_CODE } from '@/service/errorCode';
interface ConfigType {
@@ -58,7 +58,7 @@ function responseError(err: any) {
// 有报错响应
const res = err.response;
if (res.data.code in TOKEN_ERROR_CODE) {
clearToken();
clearCookie();
return Promise.reject({ message: 'token过期重新登录' });
}
}

3
src/api/system.ts Normal file
View File

@@ -0,0 +1,3 @@
import { GET, POST, PUT } from './request';
export const getFilling = () => GET<{ beianText: string }>('/system/getFiling');

View File

@@ -64,6 +64,8 @@ export const postLogin = ({ username, password }: { username: string; password:
password: createHashPassword(password)
});
export const loginOut = () => GET('/user/loginout');
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data);
export const getUserBills = (data: RequestPaging) =>

View File

@@ -1,14 +1,19 @@
import React, { useCallback, useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { Box, useColorMode, Flex } from '@chakra-ui/react';
import Navbar from './navbar';
import NavbarPhone from './navbarPhone';
import { useRouter } from 'next/router';
import { useScreen } from '@/hooks/useScreen';
import { useLoading } from '@/hooks/useLoading';
import Auth from './auth';
import { useGlobalStore } from '@/store/global';
import Auth from './auth';
import Navbar from './navbar';
import NavbarPhone from './navbarPhone';
const pcUnShowLayoutRoute: Record<string, boolean> = {
'/': true,
'/login': true
};
const phoneUnShowLayoutRoute: Record<string, boolean> = {
'/': true,
'/login': true
};
@@ -19,56 +24,48 @@ const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: b
const { Loading } = useLoading({ defaultLoading: true });
const { loading } = useGlobalStore();
const isChatPage = useMemo(
() => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0,
[router.pathname, router.query]
);
useEffect(() => {
if (colorMode === 'dark' && router.pathname !== '/chat') {
setColorMode('light');
}
}, [colorMode, router.pathname, setColorMode]);
const RenderPc = useCallback(
() =>
pcUnShowLayoutRoute[router.pathname] ? (
<Auth>{children}</Auth>
) : (
<>
<Box h={'100%'} position={'fixed'} left={0} top={0} w={'60px'}>
<Navbar />
</Box>
<Box h={'100%'} ml={'60px'} overflow={'overlay'}>
<Auth>{children}</Auth>
</Box>
</>
),
[children, router.pathname]
);
const RenderPhone = useCallback(() => {
const phoneUnShowLayoutRoute: Record<string, boolean> = {
'/login': true
};
const isChatPage =
router.pathname === '/chat' && Object.values(router.query).join('').length !== 0;
if (phoneUnShowLayoutRoute[router.pathname] || isChatPage) {
return <Auth>{children}</Auth>;
}
return (
<Flex h={'100%'} flexDirection={'column'}>
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
<Auth>{children}</Auth>
</Box>
<Box h={'50px'} borderTop={'1px solid rgba(0,0,0,0.1)'}>
<NavbarPhone />
</Box>
</Flex>
);
}, [children, router]);
return (
<>
<Box h={'100%'} overflow={'overlay'} bg={'gray.100'}>
{isPc ? <RenderPc /> : <RenderPhone />}
<Box
h={'100%'}
bgGradient={'linear(to-t,rgba(173, 206, 255, 0.05) 0%, rgba(173, 206, 255, 0.12) 100%)'}
>
{isPc ? (
pcUnShowLayoutRoute[router.pathname] ? (
<Auth>{children}</Auth>
) : (
<>
<Box h={'100%'} position={'fixed'} left={0} top={0} w={'60px'}>
<Navbar />
</Box>
<Box h={'100%'} ml={'60px'} overflow={'overlay'}>
<Auth>{children}</Auth>
</Box>
</>
)
) : phoneUnShowLayoutRoute[router.pathname] || isChatPage ? (
<Auth>{children}</Auth>
) : (
<Flex h={'100%'} flexDirection={'column'}>
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
<Auth>{children}</Auth>
</Box>
<Box h={'50px'} borderTop={'1px solid rgba(0,0,0,0.1)'}>
<NavbarPhone />
</Box>
</Flex>
)}
</Box>
{loading && <Loading />}
</>

View File

@@ -102,9 +102,7 @@ const Navbar = () => {
justifyContent={'center'}
onClick={() => {
if (item.link === router.asPath) return;
router.push(item.link, undefined, {
shallow: true
});
router.push(item.link);
}}
cursor={'pointer'}
w={'60px'}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Spinner, Flex } from '@chakra-ui/react';
const Loading = ({ fixed = true }: { fixed?: boolean }) => {
return (
<Flex
position={fixed ? 'fixed' : 'absolute'}
zIndex={100}
backgroundColor={'rgba(255,255,255,0.5)'}
top={0}
left={0}
right={0}
bottom={0}
alignItems={'center'}
justifyContent={'center'}
>
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="myBlue.500" size="xl" />
</Flex>
);
};
export default Loading;

View File

@@ -337,10 +337,10 @@
}
.markdown {
text-align: justify;
overflow-y: hidden;
tab-size: 4;
word-spacing: normal;
word-break: break-all;
width: 100%;
p {
white-space: pre-line;
@@ -353,13 +353,13 @@
margin: 0;
border: none;
border-radius: 0;
background-color: #222 !important;
background-color: #292b33 !important;
overflow-x: auto;
color: #fff;
}
pre code {
background-color: #222 !important;
background-color: #292b33 !important;
width: 100%;
}

View File

@@ -29,7 +29,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
const code = String(children);
return !inline || match ? (
<Box my={3} borderRadius={'md'} overflow={'hidden'} backgroundColor={'#222'}>
<Box my={3} borderRadius={'md'} overflow={'overlay'} backgroundColor={'#222'}>
<Flex
className="code-header"
py={2}

View File

@@ -28,31 +28,31 @@ export const ChatModelMap = {
chatModel: OpenAiChatEnum.GPT35,
name: 'ChatGpt',
contextMaxToken: 4096,
systemMaxToken: 2500,
maxTemperature: 1.5,
systemMaxToken: 2400,
maxTemperature: 1.2,
price: 3
},
[OpenAiChatEnum.GPT4]: {
chatModel: OpenAiChatEnum.GPT4,
name: 'Gpt4',
contextMaxToken: 8000,
systemMaxToken: 3500,
maxTemperature: 1.5,
price: 30
systemMaxToken: 3000,
maxTemperature: 1.2,
price: 50
},
[OpenAiChatEnum.GPT432k]: {
chatModel: OpenAiChatEnum.GPT432k,
name: 'Gpt4-32k',
contextMaxToken: 32000,
systemMaxToken: 6000,
maxTemperature: 1.5,
price: 30
systemMaxToken: 3000,
maxTemperature: 1.2,
price: 90
},
[ClaudeEnum.Claude]: {
chatModel: ClaudeEnum.Claude,
name: 'Claude(免费体验)',
contextMaxToken: 9000,
systemMaxToken: 2500,
systemMaxToken: 2400,
maxTemperature: 1,
price: 0
}
@@ -60,6 +60,7 @@ export const ChatModelMap = {
export const chatModelList: ChatModelItemType[] = [
ChatModelMap[OpenAiChatEnum.GPT35],
ChatModelMap[OpenAiChatEnum.GPT4],
ChatModelMap[ClaudeEnum.Claude]
];

View File

@@ -1,32 +1,12 @@
import { useState, useCallback } from 'react';
import { Spinner, Flex } from '@chakra-ui/react';
import LoadingComponent from '@/components/Loading';
export const useLoading = (props?: { defaultLoading: boolean }) => {
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
const Loading = useCallback(
({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
return isLoading || loading ? (
<Flex
position={fixed ? 'fixed' : 'absolute'}
zIndex={100}
backgroundColor={'rgba(255,255,255,0.5)'}
top={0}
left={0}
right={0}
bottom={0}
alignItems={'center'}
justifyContent={'center'}
>
<Spinner
thickness="4px"
speed="0.65s"
emptyColor="gray.200"
color="myBlue.500"
size="xl"
/>
</Flex>
) : null;
return isLoading || loading ? <LoadingComponent fixed={fixed} /> : null;
},
[isLoading]
);

View File

@@ -45,15 +45,17 @@ export default function App({ Component, pageProps }: AppProps) {
<Head>
<title>Fast GPT</title>
<meta name="description" content="Generated by Fast GPT" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no, viewport-fit=cover"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
<Script src="/js/pdf.js" strategy="lazyOnload"></Script>
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
<Script src="/js/particles.js" strategy="lazyOnload"></Script>
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />

View File

@@ -55,7 +55,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 使用了知识库搜索
if (model.chat.useKb) {
const { code, searchPrompt } = await searchKb({
const { code, searchPrompts } = await searchKb({
userOpenAiKey,
prompts,
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
@@ -65,14 +65,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// search result is empty
if (code === 201) {
return res.send(searchPrompt?.value);
return res.send(searchPrompts[0]?.value);
}
searchPrompt && prompts.unshift(searchPrompt);
prompts.splice(prompts.length - 3, 0, ...searchPrompts);
} else {
// 没有用知识库搜索,仅用系统提示词
model.chat.systemPrompt &&
prompts.unshift({
prompts.splice(prompts.length - 3, 0, {
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
});
@@ -103,8 +103,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
stream,
chatResponse: streamResponse,
prompts,
systemPrompt:
showModelDetail && prompts[0].obj === ChatRoleEnum.System ? prompts[0].value : ''
systemPrompt: showModelDetail
? prompts
.filter((item) => item.obj === ChatRoleEnum.System)
.map((item) => item.value)
.join('\n')
: ''
});
// 只有使用平台的 key 才计费

View File

@@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { ModelDataStatusEnum } from '@/constants/model';
import { PgClient } from '@/service/pg';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -14,15 +16,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const userId = await authToken(req);
// 找到长度大于0的数据
// split queue data
const data = await SplitData.find({
userId,
modelId,
textList: { $exists: true, $not: { $size: 0 } }
});
// embedding queue data
const where: any = [
['user_id', userId],
'AND',
['model_id', modelId],
'AND',
['status', ModelDataStatusEnum.waiting]
];
jsonRes(res, {
data: data.map((item) => item.textList).flat().length
data: {
splitDataQueue: data.map((item) => item.textList).flat().length,
embeddingQueue: await PgClient.count('modelData', {
where
})
}
});
} catch (err) {
jsonRes(res, {

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { connectToDatabase, SplitData } from '@/service/mongo';
import { authModel, authToken } from '@/service/utils/auth';
import { generateVector } from '@/service/events/generateVector';
import { generateQA } from '@/service/events/generateQA';
import { PgClient } from '@/service/pg';
@@ -23,15 +23,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const userId = await authToken(req);
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
await authModel({
modelId,
userId
});
if (!model) {
throw new Error('无权操作该模型');
}
if (mode === 'qa') {
// 批量QA拆分插入数据
await SplitData.create({
@@ -69,7 +65,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
export const config = {
api: {
bodyParser: {
sizeLimit: '10mb'
sizeLimit: '100mb'
}
}
};

View File

@@ -22,9 +22,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
).sort({
_id: -1
}),
Collection.find({
userId
}).populate('modelId', '_id avatar name chat.systemPrompt')
Collection.find({ userId })
.populate({
path: 'modelId',
select: '_id avatar name chat.systemPrompt',
match: { 'share.isShare': true }
})
.then((res) => res.filter((item) => item.modelId))
]);
jsonRes<ModelListResponse>(res, {

View File

@@ -73,7 +73,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (model.chat.useKb) {
const similarity = ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22;
const { code, searchPrompt } = await searchKb({
const { code, searchPrompts } = await searchKb({
prompts,
similarity,
model,
@@ -82,18 +82,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// search result is empty
if (code === 201) {
return res.send(searchPrompt?.value);
return res.send(searchPrompts[0]?.value);
}
searchPrompt && prompts.unshift(searchPrompt);
prompts.splice(prompts.length - 1, 0, ...searchPrompts);
} else {
// 没有用知识库搜索,仅用系统提示词
if (model.chat.systemPrompt) {
prompts.unshift({
model.chat.systemPrompt &&
prompts.splice(prompts.length - 1, 0, {
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
});
}
}
// 计算温度

View File

@@ -122,14 +122,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const prompts = [prompt];
// 获取向量匹配到的提示词
const { searchPrompt } = await searchKb({
const { searchPrompts } = await searchKb({
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
prompts,
model,
userId
});
searchPrompt && prompts.unshift(searchPrompt);
prompts.splice(prompts.length - 1, 0, ...searchPrompts);
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(

View File

@@ -0,0 +1,11 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
jsonRes(res, {
data: {
beianText: process.env.SAFE_BEIAN_TEXT || ''
}
});
}

View File

@@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { User } from '@/service/models/user';
import { generateToken } from '@/service/utils/tools';
import { setCookie } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -32,7 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('密码错误');
}
res.setHeader('Set-Cookie', `token=${generateToken(user._id)}; Path=/; HttpOnly`);
setCookie(res, user._id);
jsonRes(res, {
data: {

View File

@@ -0,0 +1,16 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { clearCookie } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
clearCookie(res);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,7 +4,7 @@ import { jsonRes } from '@/service/response';
import { User } from '@/service/models/user';
import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
import { generateToken } from '@/service/utils/tools';
import { setCookie } from '@/service/utils/tools';
import { UserAuthTypeEnum } from '@/constants/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -56,7 +56,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
username
});
res.setHeader('Set-Cookie', `token=${generateToken(user._id)}; Path=/; HttpOnly`);
setCookie(res, user._id);
jsonRes(res, {
data: {

View File

@@ -14,7 +14,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const userId = await authToken(req);
await connectToDatabase();
// 更新对应的记录
await User.updateOne(
{
@@ -22,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
},
{
...(avatar && { avatar }),
...(openaiKey && { openaiKey })
...(openaiKey !== undefined && { openaiKey })
}
);

View File

@@ -4,8 +4,8 @@ import { jsonRes } from '@/service/response';
import { User } from '@/service/models/user';
import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
import { generateToken } from '@/service/utils/tools';
import { UserAuthTypeEnum } from '@/constants/common';
import { setCookie } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('获取用户信息异常');
}
res.setHeader('Set-Cookie', `token=${generateToken(user._id)}; Path=/; HttpOnly`);
setCookie(res, user._id);
jsonRes(res, {
data: {

View File

@@ -28,7 +28,13 @@ const Empty = ({
>
<Card p={4} mb={10}>
<Flex mb={2} alignItems={'center'} justifyContent={'center'}>
<Image src={avatar || LOGO_ICON} w={'32px'} h={'32px'} alt={''} />
<Image
src={avatar || LOGO_ICON}
w={'32px'}
maxH={'40px'}
objectFit={'contain'}
alt={''}
/>
<Box ml={3} fontSize={'3xl'} fontWeight={'bold'}>
{name}
</Box>

View File

@@ -115,14 +115,22 @@ const PcSliderBar = ({
</Button>
{models.length > 1 && (
<Box
className={styles.modelList}
className={styles.modelListContainer}
position={'absolute'}
transition={'0.15s ease-out'}
w={'110%'}
left={0}
top={'45px'}
top={'40px'}
transition={'0.15s ease-out'}
bg={'white'}
>
<ModelList models={models} modelId={modelId} />
<Box
className={styles.modelList}
mt={'6px'}
h={'calc(100% - 6px)'}
overflow={'overlay'}
>
<ModelList models={models} modelId={modelId} />
</Box>
</Box>
)}
</Box>

View File

@@ -7,7 +7,7 @@ const ModelList = ({ models, modelId }: { models: ModelListItemType[]; modelId:
const router = useRouter();
return (
<Box w={'100%'} h={'100%'} bg={'white'} overflow={'overlay'}>
<>
{models.map((item) => (
<Box key={item._id}>
<Flex
@@ -18,6 +18,7 @@ const ModelList = ({ models, modelId }: { models: ModelListItemType[]; modelId:
cursor={'pointer'}
transition={'background-color .2s ease-in'}
borderLeft={['', '5px solid transparent']}
zIndex={0}
_hover={{
backgroundColor: ['', '#dee0e3']
}}
@@ -28,7 +29,6 @@ const ModelList = ({ models, modelId }: { models: ModelListItemType[]; modelId:
}
: {})}
onClick={() => {
if (item._id === modelId) return;
router.replace(`/chat?modelId=${item._id}`);
}}
>
@@ -50,7 +50,7 @@ const ModelList = ({ models, modelId }: { models: ModelListItemType[]; modelId:
</Flex>
</Box>
))}
</Box>
</>
);
};

View File

@@ -115,7 +115,7 @@ const PhoneSliderBar = ({
onClose();
}}
>
<Image src={item.avatar} mr={2} alt={''} w={'16px'} h={'16px'} />
<Image src={item.avatar || '/icon/logo.png'} mr={2} alt={''} w={'16px'} h={'16px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>

View File

@@ -11,14 +11,18 @@
}
.newChat {
.modelList {
.modelListContainer {
height: 0;
border-radius: 6px;
overflow: hidden;
}
.modelList {
border-radius: 6px;
}
&:hover {
.modelListContainer {
height: 60vh;
}
.modelList {
height: 50vh;
box-shadow: 0 0 5px rgba($color: #000000, $alpha: 0.05);
border: 1px solid #dee0e2;
}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
import React, { useCallback, useState, useRef, useMemo, useEffect, MouseEvent } from 'react';
import { useRouter } from 'next/router';
import {
getInitChatSiteInfo,
@@ -26,7 +26,10 @@ import {
useDisclosure,
Drawer,
DrawerOverlay,
DrawerContent
DrawerContent,
Card,
Tooltip,
useOutsideClick
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useScreen } from '@/hooks/useScreen';
@@ -44,10 +47,20 @@ import { useLoading } from '@/hooks/useLoading';
import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common';
import { useUserStore } from '@/store/user';
import Loading from '@/components/Loading';
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'));
const History = dynamic(() => import('./components/History'));
const Empty = dynamic(() => import('./components/Empty'));
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const History = dynamic(() => import('./components/History'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const Empty = dynamic(() => import('./components/Empty'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
import styles from './index.module.scss';
@@ -66,6 +79,8 @@ const Chat = ({
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
const ContextMenuRef = useRef(null);
const PhoneContextShow = useRef(false);
// 中断请求
const controller = useRef(new AbortController());
@@ -73,6 +88,12 @@ const Chat = ({
const [inputVal, setInputVal] = useState(''); // user input prompt
const [showSystemPrompt, setShowSystemPrompt] = useState('');
const [messageContextMenuData, setMessageContextMenuData] = useState<{
// message messageContextMenuData
left: number;
top: number;
message: ChatSiteItemType;
}>();
const {
lastChatModelId,
@@ -97,6 +118,24 @@ const Chat = ({
const { Loading, setIsLoading } = useLoading();
const { userInfo } = useUserStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
// close contextMenu
useOutsideClick({
ref: ContextMenuRef,
handler: () => {
// 移动端长按后会将其设置为true松手时候也会触发一次松手的时候需要忽略一次。
if (PhoneContextShow.current) {
PhoneContextShow.current = false;
} else {
messageContextMenuData &&
setTimeout(() => {
setMessageContextMenuData(undefined);
window.getSelection?.()?.empty?.();
window.getSelection?.()?.removeAllRanges?.();
document?.getSelection()?.empty();
});
}
}
});
// 滚动到底部
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
@@ -198,7 +237,6 @@ const Chat = ({
if (newChatId) {
setForbidLoadChatData(true);
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
loadHistory({ pageNum: 1, init: true });
}
} catch (err) {
toast({
@@ -222,6 +260,12 @@ const Chat = ({
};
})
}));
// refresh history
loadHistory({ pageNum: 1, init: true });
setTimeout(() => {
generatingMessage();
}, 100);
},
[
chatId,
@@ -315,24 +359,26 @@ const Chat = ({
]);
// 删除一句话
const delChatRecord = useCallback(
async (index: number, id: string) => {
setIsLoading(true);
try {
// 删除数据库最后一句
await delChatRecordByIndex(chatId, id);
const delChatRecord = useCallback(async () => {
if (!messageContextMenuData) return;
setIsLoading(true);
const index = chatData.history.findIndex(
(item) => item._id === messageContextMenuData.message._id
);
setChatData((state) => ({
...state,
history: state.history.filter((_, i) => i !== index)
}));
} catch (err) {
console.log(err);
}
setIsLoading(false);
},
[chatId, setChatData, setIsLoading]
);
try {
// 删除数据库最后一句
await delChatRecordByIndex(chatId, messageContextMenuData.message._id);
setChatData((state) => ({
...state,
history: state.history.filter((_, i) => i !== index)
}));
} catch (err) {
console.log(err);
}
setIsLoading(false);
}, [chatData.history, chatId, messageContextMenuData, setChatData, setIsLoading]);
// 复制内容
const onclickCopy = useCallback(
@@ -421,6 +467,34 @@ const Chat = ({
[loadHistory]
);
// onclick chat message context
const onclickContextMenu = useCallback(
(e: MouseEvent<HTMLDivElement>, message: ChatSiteItemType) => {
e.preventDefault(); // 阻止默认右键菜单
// select all text
const range = document.createRange();
range.selectNodeContents(e.currentTarget as HTMLDivElement);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
navigator.vibrate?.(50); // 震动 50 毫秒
if (!isPcDevice) {
PhoneContextShow.current = true;
}
setMessageContextMenuData({
left: e.clientX,
top: e.clientY,
message
});
return false;
},
[isPcDevice]
);
// 获取对话信息
const loadChatInfo = useCallback(
async ({
@@ -488,13 +562,6 @@ const Chat = ({
modelId && setLastChatModelId(modelId);
setLastChatId(chatId);
// focus scroll bottom
chatId && scrollToBottom('auto');
/* get mode and chat into ↓ */
// phone: history page
if (!isPc && Object.keys(router.query).length === 0) return null;
if (forbidLoadChatData) {
setForbidLoadChatData(false);
return null;
@@ -506,18 +573,19 @@ const Chat = ({
});
});
// abort stream
useEffect(() => {
return () => {
isLeavePage.current = true;
controller.current?.abort();
};
}, []);
}, [modelId, chatId]);
return (
<Flex
h={'100%'}
flexDirection={['column', 'row']}
backgroundColor={useColorModeValue('white', '')}
backgroundColor={useColorModeValue('#fefefe', '')}
>
{/* pc always show history. phone is only show when modelId is present */}
{isPc || !modelId ? (
@@ -550,38 +618,40 @@ const Chat = ({
onClick={onOpenSlider}
/>
<Box>{chatData.model.name}</Box>
<Menu autoSelect={false}>
<MenuButton lineHeight={1}>
<MyIcon
name={'more'}
w={'16px'}
h={'16px'}
color={useColorModeValue('blackAlpha.700', 'white')}
/>
</MenuButton>
<MenuList minW={`90px !important`}>
<MenuItem onClick={() => router.replace(`/chat?modelId=${modelId}`)}>
</MenuItem>
<MenuItem
onClick={async () => {
try {
setIsLoading(true);
await onclickDelHistory(chatData.chatId);
router.replace(`/chat`);
} catch (err) {
console.log(err);
}
setIsLoading(false);
}}
>
</MenuItem>
<MenuItem onClick={() => onclickExportChat('html')}>HTML格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('pdf')}>PDF格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('md')}>Markdown格式</MenuItem>
</MenuList>
</Menu>
{chatId && (
<Menu autoSelect={false}>
<MenuButton lineHeight={1}>
<MyIcon
name={'more'}
w={'16px'}
h={'16px'}
color={useColorModeValue('blackAlpha.700', 'white')}
/>
</MenuButton>
<MenuList minW={`90px !important`}>
<MenuItem onClick={() => router.replace(`/chat?modelId=${modelId}`)}>
</MenuItem>
<MenuItem
onClick={async () => {
try {
setIsLoading(true);
await onclickDelHistory(chatData.chatId);
router.replace(`/chat?modelId=${modelId}`);
} catch (err) {
console.log(err);
}
setIsLoading(false);
}}
>
</MenuItem>
<MenuItem onClick={() => onclickExportChat('html')}>HTML格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('pdf')}>PDF格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('md')}>Markdown格式</MenuItem>
</MenuList>
</Menu>
)}
</Flex>
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
@@ -598,6 +668,7 @@ const Chat = ({
position={'relative'}
h={[0, '100%']}
w={['100%', 0]}
pt={[2, 4]}
flex={'1 0 0'}
flexDirection={'column'}
>
@@ -610,20 +681,25 @@ const Chat = ({
w={'100%'}
overflow={'overlay'}
>
{chatData.history.map((item, index) => (
<Box
key={item._id}
py={[6, 9]}
px={[2, 4]}
backgroundColor={
index % 2 !== 0 ? useColorModeValue('blackAlpha.50', 'gray.700') : ''
}
color={useColorModeValue('blackAlpha.700', 'white')}
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
<Menu autoSelect={false}>
<MenuButton as={Box} mr={[1, 4]} cursor={'pointer'}>
<Box maxW={['auto', '800px', '1000px']} m={'auto'}>
{chatData.history.map((item, index) => (
<Flex key={item._id} alignItems={'flex-start'} py={2} px={[2, 4]}>
{item.obj === 'Human' && <Box flex={1} />}
{/* avatar */}
<Box
{...(item.obj === 'AI'
? {
order: 1,
mr: ['6px', 4],
cursor: 'pointer',
onClick: () => router.push(`/model?modelId=${chatData.modelId}`)
}
: {
order: 3,
ml: ['6px', 4]
})}
>
<Tooltip label={item.obj === 'AI' ? 'AI助手详情' : ''}>
<Image
className="avatar"
src={
@@ -632,74 +708,73 @@ const Chat = ({
: chatData.model.avatar || LOGO_ICON
}
alt="avatar"
w={['20px', '30px']}
maxH={'50px'}
w={['20px', '34px']}
h={['20px', '34px']}
borderRadius={'50%'}
objectFit={'contain'}
/>
</MenuButton>
<MenuList fontSize={'sm'}>
{chatData.model.canUse && (
<MenuItem onClick={() => router.push(`/model?modelId=${chatData.modelId}`)}>
AI助手详情
</MenuItem>
)}
<MenuItem onClick={() => onclickCopy(item.value)}></MenuItem>
<MenuItem onClick={() => delChatRecord(index, item._id)}></MenuItem>
</MenuList>
</Menu>
<Box flex={'1 0 0'} w={0} overflow={'hidden'}>
</Tooltip>
</Box>
{/* message */}
<Flex order={2} maxW={['calc(100% - 50px)', '80%']}>
{item.obj === 'AI' ? (
<>
<Markdown
source={item.value}
isChatting={isChatting && index === chatData.history.length - 1}
/>
{item.systemPrompt && (
<Button
size={'xs'}
mt={2}
fontWeight={'normal'}
colorScheme={'gray'}
variant={'outline'}
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
>
</Button>
<Box w={'100%'}>
{isPc && (
<Box color={'myGray.600'} fontSize={'sm'} mb={1}>
{chatData.model.name}
</Box>
)}
</>
<Card
bg={'white'}
px={4}
py={3}
borderRadius={'0 8px 8px 8px'}
onContextMenu={(e) => onclickContextMenu(e, item)}
>
<Markdown
source={item.value}
isChatting={isChatting && index === chatData.history.length - 1}
/>
{item.systemPrompt && (
<Button
size={'xs'}
mt={2}
fontWeight={'normal'}
colorScheme={'gray'}
variant={'outline'}
w={'90px'}
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
>
</Button>
)}
</Card>
</Box>
) : (
<Box className="markdown" whiteSpace={'pre-wrap'}>
<Box as={'p'}>{item.value}</Box>
<Box>
{isPc && (
<Box color={'myGray.600'} mb={1} fontSize={'sm'} textAlign={'right'}>
Human
</Box>
)}
<Card
className="markdown"
whiteSpace={'pre-wrap'}
px={4}
py={3}
borderRadius={'8px 0 8px 8px'}
bg={'myBlue.300'}
onContextMenu={(e) => onclickContextMenu(e, item)}
>
<Box as={'p'}>{item.value}</Box>
</Card>
</Box>
)}
</Box>
{isPc && (
<Flex h={'100%'} flexDirection={'column'} ml={2} w={'14px'} height={'100%'}>
<Box minH={'40px'} flex={1}>
<MyIcon
name="copy"
w={'14px'}
cursor={'pointer'}
color={'blackAlpha.700'}
onClick={() => onclickCopy(item.value)}
/>
</Box>
<MyIcon
name="delete"
w={'14px'}
cursor={'pointer'}
color={'blackAlpha.700'}
_hover={{
color: 'red.600'
}}
onClick={() => delChatRecord(index, item._id)}
/>
</Flex>
)}
</Flex>
</Flex>
</Box>
))}
{chatData.history.length === 0 && <Empty model={chatData.model} />}
))}
{chatData.history.length === 0 && <Empty model={chatData.model} />}
</Box>
</Box>
{/* 发送区 */}
{chatData.model.canUse ? (
@@ -707,7 +782,7 @@ const Chat = ({
<Box
py={'18px'}
position={'relative'}
boxShadow={`0 0 15px rgba(0,0,0,0.1)`}
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
borderTop={['1px solid', 0]}
borderTopColor={useColorModeValue('gray.200', 'gray.700')}
borderRadius={['none', 'md']}
@@ -743,7 +818,7 @@ const Chat = ({
}}
onKeyDown={(e) => {
// 触发快捷发送
if (isPc && e.keyCode === 13 && !e.shiftKey) {
if (isPcDevice && e.keyCode === 13 && !e.shiftKey) {
sendPrompt();
e.preventDefault();
}
@@ -803,18 +878,35 @@ const Chat = ({
<ModalOverlay />
<ModalContent maxW={'min(90vw, 600px)'} pr={2} maxH={'80vh'} overflowY={'auto'}>
<ModalCloseButton />
<ModalBody pt={5} fontSize={'sm'} whiteSpace={'pre-wrap'} textAlign={'justify'}>
<ModalBody pt={5} whiteSpace={'pre-wrap'} textAlign={'justify'}>
{showSystemPrompt}
</ModalBody>
</ModalContent>
</Modal>
}
{/* context menu */}
{messageContextMenuData && (
<Box
zIndex={10}
position={'fixed'}
top={messageContextMenuData.top}
left={messageContextMenuData.left}
>
<Box ref={ContextMenuRef}></Box>
<Menu isOpen>
<MenuList minW={`100px !important`}>
<MenuItem onClick={() => onclickCopy(messageContextMenuData.message.value)}>
</MenuItem>
<MenuItem onClick={delChatRecord}></MenuItem>
</MenuList>
</Menu>
</Box>
)}
</Flex>
);
};
export default Chat;
Chat.getInitialProps = ({ query, req }: any) => {
return {
modelId: query?.modelId || '',
@@ -822,3 +914,5 @@ Chat.getInitialProps = ({ query, req }: any) => {
isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent)
};
};
export default Chat;

View File

@@ -0,0 +1,5 @@
.home {
* {
position: relative;
}
}

View File

@@ -1,12 +1,19 @@
import React, { useEffect } from 'react';
import { Card, Box, Link } from '@chakra-ui/react';
import { Card, Box, Link, Flex, Image, Button } from '@chakra-ui/react';
import Markdown from '@/components/Markdown';
import { useMarkdown } from '@/hooks/useMarkdown';
import { getFilling } from '@/api/system';
import { useQuery } from '@tanstack/react-query';
import { useScreen } from '@/hooks/useScreen';
import { useRouter } from 'next/router';
import styles from './index.module.scss';
const Home = () => {
const { inviterId } = useRouter().query as { inviterId: string };
const router = useRouter();
const { inviterId } = router.query as { inviterId: string };
const { data } = useMarkdown({ url: '/intro.md' });
const { isPc } = useScreen();
useEffect(() => {
if (inviterId) {
@@ -14,21 +21,171 @@ const Home = () => {
}
}, [inviterId]);
return (
<Box p={[5, 10]}>
<Card p={5} lineHeight={2}>
<Markdown source={data} isChatting={false} />
</Card>
const { data: { beianText = '' } = {} } = useQuery(['init'], getFilling);
<Card p={5} mt={4} textAlign={'center'}>
<Box>
<Link href="https://beian.miit.gov.cn/" target="_blank">
ICP备2023011255号-1
</Link>
</Box>
<Box>Made by FastGpt Team.</Box>
</Card>
</Box>
/* 加载动画 */
useEffect(() => {
setTimeout(() => {
window.particlesJS?.('particles-js', {
particles: {
number: {
value: 40,
density: {
enable: true,
value_area: 500
}
},
color: {
value: '#4e83fd'
},
shape: {
type: 'circle',
stroke: {
width: 0,
color: '#000000'
},
polygon: {
nb_sides: 5
}
},
opacity: {
value: 0.5,
random: false,
anim: {
enable: false,
speed: 0.1,
opacity_min: 0.1,
sync: false
}
},
size: {
value: 3,
random: true,
anim: {
enable: false,
speed: 10,
size_min: 0.1,
sync: false
}
},
line_linked: {
enable: true,
distance: 150,
color: '#adceff',
opacity: 0.4,
width: 1
},
move: {
enable: true,
speed: 2,
direction: 'none',
random: true,
straight: false,
out_mode: 'out',
bounce: false,
attract: {
enable: false,
rotateX: 600,
rotateY: 1200
}
}
},
interactivity: {
detect_on: 'canvas',
events: {
onhover: {
enable: true,
mode: 'grab'
},
onclick: {
enable: true,
mode: 'push'
},
resize: true
},
modes: {
grab: {
distance: 140,
line_linked: {
opacity: 1
}
},
bubble: {
distance: 400,
size: 40,
duration: 2,
opacity: 8,
speed: 3
},
repulse: {
distance: 200,
duration: 0.4
},
push: {
particles_nb: 4
},
remove: {
particles_nb: 2
}
}
},
retina_detect: true
});
}, 1000);
}, [isPc]);
return (
<Flex
className={styles.home}
position={'relative'}
flexDirection={'column'}
alignItems={'center'}
pt={'20vh'}
overflow={'overlay'}
>
<Box id={'particles-js'} position={'absolute'} top={0} left={0} right={0} bottom={0} />
<Image src="/icon/logo.png" w={['70px', '120px']} h={['70px', '120px']} alt={''}></Image>
<Box
fontWeight={'bold'}
fontSize={['40px', '70px']}
letterSpacing={'5px'}
color={'myBlue.600'}
>
FastGpt
</Box>
<Box color={'myBlue.600'} fontSize={['30px', '50px']}>
</Box>
<Box color={'myBlue.600'} fontSize={['30px', '50px']}>
AI
</Box>
<Button
my={5}
fontSize={['xl', '3xl']}
h={'auto'}
py={[2, 3]}
onClick={() => router.push(`/model`)}
>
</Button>
<Box mt={'20vh'} w={'100%'} p={[5, 10]}>
<Card p={5} lineHeight={2}>
<Markdown source={data} isChatting={false} />
</Card>
<Card p={5} mt={4} textAlign={'center'}>
{beianText && (
<Link href="https://beian.miit.gov.cn/" target="_blank">
{beianText}
</Link>
)}
<Box>Made by FastGpt Team.</Box>
</Card>
</Box>
</Flex>
);
};

View File

@@ -95,7 +95,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{!!errors.username && errors.username.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={5} isInvalid={!!errors.username}>
<FormControl mt={8} isInvalid={!!errors.username}>
<Flex>
<Input
flex={1}
@@ -121,7 +121,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{!!errors.code && errors.code.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={5} isInvalid={!!errors.password}>
<FormControl mt={8} isInvalid={!!errors.password}>
<Input
type={'password'}
placeholder="新密码"
@@ -142,7 +142,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{!!errors.password && errors.password.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={5} isInvalid={!!errors.password2}>
<FormControl mt={8} isInvalid={!!errors.password2}>
<Input
type={'password'}
placeholder="确认密码"

View File

@@ -103,7 +103,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{!!errors.username && errors.username.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={5} isInvalid={!!errors.username}>
<FormControl mt={8} isInvalid={!!errors.username}>
<Flex>
<Input
flex={1}
@@ -129,7 +129,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{!!errors.code && errors.code.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={5} isInvalid={!!errors.password}>
<FormControl mt={8} isInvalid={!!errors.password}>
<Input
type={'password'}
placeholder="密码"
@@ -150,7 +150,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
{!!errors.password && errors.password.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={5} isInvalid={!!errors.password2}>
<FormControl mt={8} isInvalid={!!errors.password2}>
<Input
type={'password'}
placeholder="确认密码"

View File

@@ -6,6 +6,7 @@ import { useScreen } from '@/hooks/useScreen';
import type { ResLogin } from '@/api/response/user';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
import { useChatStore } from '@/store/chat';
import LoginForm from './components/LoginForm';
import dynamic from 'next/dynamic';
const RegisterForm = dynamic(() => import('./components/RegisterForm'));
@@ -16,16 +17,33 @@ const Login = ({ isPcDevice }: { isPcDevice: boolean }) => {
const { lastRoute = '' } = router.query as { lastRoute: string };
const { isPc } = useScreen({ defaultIsPc: isPcDevice });
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
const { setUserInfo } = useUserStore();
const { setUserInfo, setLastModelId, loadMyModels } = useUserStore();
const { setLastChatId, setLastChatModelId, loadHistory } = useChatStore();
const loginSuccess = useCallback(
(res: ResLogin) => {
// init store
setLastChatId('');
setLastModelId('');
setLastChatModelId('');
loadMyModels(true);
loadHistory({ pageNum: 1, init: true });
setUserInfo(res.user);
setTimeout(() => {
router.push(lastRoute ? decodeURIComponent(lastRoute) : '/model');
}, 100);
},
[lastRoute, router, setUserInfo]
[
lastRoute,
loadHistory,
loadMyModels,
router,
setLastChatId,
setLastChatModelId,
setLastModelId,
setUserInfo
]
);
function DynamicComponent({ type }: { type: `${PageTypeEnum}` }) {
@@ -56,7 +74,7 @@ const Login = ({ isPcDevice }: { isPcDevice: boolean }) => {
height="100%"
w={'100%'}
maxW={'1240px'}
maxH={['auto', '660px']}
maxH={['auto', 'max(660px,80vh)']}
backgroundColor={'#fff'}
alignItems={'center'}
justifyContent={'center'}

View File

@@ -44,6 +44,13 @@ const InputDataModal = ({
*/
const sureImportData = useCallback(
async (e: FormData) => {
if (e.a.length + e.q.length >= 3000) {
toast({
title: '总长度超长了',
status: 'warning'
});
return;
}
setImporting(true);
try {
@@ -66,7 +73,11 @@ const InputDataModal = ({
q: ''
});
onSuccess();
} catch (err) {
} catch (err: any) {
toast({
title: err?.message || '出现了点意外~',
status: 'error'
});
console.log(err);
}
setImporting(false);
@@ -121,8 +132,8 @@ const InputDataModal = ({
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['230px', '100%']}>
<Box h={'30px'}>{'匹配的知识点'}</Box>
<Textarea
placeholder={'匹配的知识点。这部分内容会被搜索,请把控内容的质量。最多 1500 字。'}
maxLength={1500}
placeholder={'匹配的知识点。这部分内容会被搜索,请把控内容的质量。总和最多 3000 字。'}
maxLength={3000}
resize={'none'}
h={'calc(100% - 30px)'}
{...register(`q`, {
@@ -134,9 +145,9 @@ const InputDataModal = ({
<Box h={'30px'}></Box>
<Textarea
placeholder={
'补充知识。这部分内容不会被搜索,但会作为"匹配的知识点"的内容补充,你可以讲一些细节的内容填写在这里。最多 1500 字。'
'补充知识。这部分内容不会被搜索,但会作为"匹配的知识点"的内容补充,你可以讲一些细节的内容填写在这里。总和最多 3000 字。'
}
maxLength={1500}
maxLength={3000}
resize={'none'}
h={'calc(100% - 30px)'}
{...register('a')}

View File

@@ -88,7 +88,7 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean
onClose: onCloseSelectCsvModal
} = useDisclosure();
const { data: splitDataLen = 0, refetch } = useQuery(
const { data: { splitDataQueue = 0, embeddingQueue = 0 } = {}, refetch } = useQuery(
['getModelSplitDataList'],
() => getModelSplitDataListLen(modelId),
{
@@ -109,7 +109,7 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean
useQuery(['refetchData'], () => refetchData(pageNum), {
refetchInterval: 5000,
enabled: splitDataLen > 0
enabled: splitDataQueue > 0 || embeddingQueue > 0
});
// 获取所有的数据,并导出 json
@@ -186,8 +186,12 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean
)}
</Flex>
<Flex mt={4}>
{isOwner && splitDataLen > 0 && (
<Box fontSize={'xs'}>{splitDataLen}...</Box>
{isOwner && (splitDataQueue > 0 || embeddingQueue > 0) && (
<Box fontSize={'xs'}>
{splitDataQueue > 0 ? `${splitDataQueue}条数据正在拆分,` : ''}
{embeddingQueue > 0 ? `${embeddingQueue}条数据正在生成索引,` : ''}
...
</Box>
)}
<Box flex={1} />
<Input
@@ -275,9 +279,9 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean
</Tbody>
</Table>
</TableContainer>
<Box mt={2} textAlign={'end'}>
<Flex mt={2} justifyContent={'flex-end'}>
<Pagination />
</Box>
</Flex>
</Box>
<Loading loading={isLoading} fixed={false} />

View File

@@ -55,8 +55,8 @@ const ModelEditForm = ({
try {
const base64 = await compressImg({
file,
maxW: 40,
maxH: 60
maxW: 100,
maxH: 100
});
setValue('avatar', base64);
setRefresh((state) => !state);

View File

@@ -61,13 +61,7 @@ const SelectFileModal = ({
const { openConfirm, ConfirmChild } = useConfirm({
content: `确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,未完成的任务会被直接清除。一共 ${
splitRes.chunks.length
} 组。${
splitRes.tokens
? `大约 ${splitRes.tokens} 个tokens, 约 ${formatPrice(
splitRes.tokens * modeMap[mode].price
)}`
: ''
}`
} 组。${splitRes.tokens ? `大约 ${splitRes.tokens} 个tokens。` : ''}`
});
const onSelectFile = useCallback(
@@ -191,8 +185,8 @@ const SelectFileModal = ({
<Radio
ml={3}
list={[
{ label: 'QA拆分', value: 'qa' },
{ label: '直接分段', value: 'subsection' }
{ label: '直接分段', value: 'subsection' },
{ label: 'QA拆分', value: 'qa' }
]}
value={mode}
onChange={(e) => setMode(e as 'subsection' | 'qa')}

View File

@@ -5,8 +5,12 @@ import { useRouter } from 'next/router';
import ModelList from './components/ModelList';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/store/user';
import Loading from '@/components/Loading';
const ModelDetail = dynamic(() => import('./components/detail/index'));
const ModelDetail = dynamic(() => import('./components/detail/index'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const Model = ({ modelId, isPcDevice }: { modelId: string; isPcDevice: boolean }) => {
const router = useRouter();

View File

@@ -101,9 +101,9 @@ const modelList = () => {
<Grid templateColumns={['1fr', '1fr 1fr', '1fr 1fr 1fr']} gridGap={4} mt={4}>
<ShareModelList models={models} onclickCollection={onclickCollection} />
</Grid>
<Box mt={4}>
<Flex mt={4} justifyContent={'flex-end'}>
<Pagination />
</Box>
</Flex>
</Card>
<Loading loading={isLoading} />

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Card, Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
import { Card, Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { BillTypeMap } from '@/constants/user';
import { getUserBills } from '@/api/user';
import type { UserBillType } from '@/types/user';
@@ -48,9 +48,9 @@ const BillTable = () => {
<Loading loading={isLoading} fixed={false} />
</TableContainer>
<Box mt={4} mr={4} textAlign={'end'}>
<Flex mt={4} px={4} justifyContent={'flex-end'}>
<Pagination />
</Box>
</Flex>
</Card>
);
};

View File

@@ -7,22 +7,32 @@ import { useToast } from '@/hooks/useToast';
import { useGlobalStore } from '@/store/global';
import { useUserStore } from '@/store/user';
import { UserType } from '@/types/user';
import { clearToken } from '@/utils/user';
import { clearCookie } from '@/utils/user';
import { useRouter } from 'next/router';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useSelectFile } from '@/hooks/useSelectFile';
import { compressImg } from '@/utils/file';
import Loading from '@/components/Loading';
const PayRecordTable = dynamic(() => import('./components/PayRecordTable'));
const BilTable = dynamic(() => import('./components/BillTable'));
const PayModal = dynamic(() => import('./components/PayModal'));
const PayRecordTable = dynamic(() => import('./components/PayRecordTable'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const BilTable = dynamic(() => import('./components/BillTable'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const PayModal = dynamic(() => import('./components/PayModal'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const NumberSetting = () => {
const router = useRouter();
const { userInfo, updateUserInfo, initUserInfo, setUserInfo } = useUserStore();
const { setLoading } = useGlobalStore();
const { register, handleSubmit } = useForm<UserUpdateParams>({
const { register, handleSubmit, reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const [showPay, setShowPay] = useState(false);
@@ -35,11 +45,11 @@ const NumberSetting = () => {
const onclickSave = useCallback(
async (data: UserUpdateParams) => {
if (data.openaiKey === userInfo?.openaiKey) return;
setLoading(true);
try {
await putUserInfo(data);
updateUserInfo(data);
reset(data);
toast({
title: '更新成功',
status: 'success'
@@ -47,7 +57,7 @@ const NumberSetting = () => {
} catch (error) {}
setLoading(false);
},
[setLoading, toast, updateUserInfo, userInfo?.openaiKey]
[reset, setLoading, toast, updateUserInfo]
);
const onSelectFile = useCallback(
@@ -57,10 +67,11 @@ const NumberSetting = () => {
try {
const base64 = await compressImg({
file,
maxW: 40,
maxH: 60
maxW: 100,
maxH: 100
});
onclickSave({
...userInfo,
avatar: base64
});
} catch (err: any) {
@@ -70,11 +81,11 @@ const NumberSetting = () => {
});
}
},
[onclickSave, toast]
[onclickSave, toast, userInfo]
);
const onclickLogOut = useCallback(() => {
clearToken();
clearCookie();
setUserInfo(null);
router.replace('/login');
}, [router, setUserInfo]);
@@ -99,8 +110,8 @@ const NumberSetting = () => {
src={userInfo?.avatar}
alt={'avatar'}
w={['28px', '36px']}
h={['28px', '36px']}
objectFit={'cover'}
maxH={'40px'}
objectFit={'contain'}
cursor={'pointer'}
title={'点击切换头像'}
onClick={onOpenSelectFile}

View File

@@ -81,7 +81,7 @@ const OpenApi = () => {
mr={4}
variant={'outline'}
onClick={() => {
copyData(`${location.origin}?inviterId=${userInfo?._id}`, '已复制邀请链接');
copyData(`${location.origin}/?inviterId=${userInfo?._id}`, '已复制邀请链接');
}}
>
@@ -139,9 +139,9 @@ const OpenApi = () => {
<Loading loading={isLoading} fixed={false} />
</TableContainer>
<Box mt={4} mr={4} textAlign={'end'}>
<Flex mt={4} px={4} justifyContent={'flex-end'}>
<Pagination />
</Box>
</Flex>
</Card>
<Modal isOpen={isOpenWithdraw} onClose={onCloseWithdraw}>
<ModalOverlay />

View File

@@ -36,7 +36,8 @@ export const proxyError: Record<string, boolean> = {
export enum ERROR_ENUM {
unAuthorization = 'unAuthorization',
insufficientQuota = 'insufficientQuota'
insufficientQuota = 'insufficientQuota',
unAuthModel = 'unAuthModel'
}
export const ERROR_RESPONSE: Record<
any,
@@ -58,5 +59,11 @@ export const ERROR_RESPONSE: Record<
statusText: ERROR_ENUM.insufficientQuota,
message: '账号余额不足',
data: null
},
[ERROR_ENUM.unAuthModel]: {
code: 511,
statusText: ERROR_ENUM.unAuthModel,
message: '无权使用该模型',
data: null
}
};

View File

@@ -128,8 +128,9 @@ export const pushGenerateVectorBill = async ({
try {
const unitPrice = 0.4;
// 计算价格. 至少为1
let price = unitPrice * tokenLen;
price = price > 1 ? price : 1;
const price = 0;
// let price = unitPrice * tokenLen;
// price = price > 1 ? price : 1;
// 插入 Bill 记录
const res = await Bill.create({

View File

@@ -19,7 +19,7 @@ const ModelSchema = new Schema({
},
avatar: {
type: String,
default: ''
default: '/icon/logo.png'
},
status: {
type: String,

View File

@@ -23,10 +23,10 @@ export const searchKb = async ({
similarity?: number;
}): Promise<{
code: 200 | 201;
searchPrompt?: {
obj: `${ChatRoleEnum}`;
searchPrompts: {
obj: ChatRoleEnum;
value: string;
};
}[];
}> => {
async function search(textArr: string[] = []) {
// 获取提示词的向量
@@ -85,17 +85,38 @@ export const searchKb = async ({
};
const filterRate = filterRateMap[systemPrompts.length] || filterRateMap[0];
// count fixed system prompt
const fixedSystemPrompt = `
${model.chat.systemPrompt}
${
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity ? '不回答知识库外的内容.' : ''
}
知识库内容为:`;
// 计算固定提示词的 token 数量
const fixedPrompts = [
...(model.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
: []),
...(model.chat.searchMode === ModelVectorSearchModeEnum.noContext
? [
{
obj: ChatRoleEnum.System,
value: `知识库是关于"${model.name}"的内容,根据知识库内容回答问题.`
}
]
: [
{
obj: ChatRoleEnum.System,
value: `玩一个问答游戏,规则为:
1.你完全忘记你已有的知识
2.你只回答关于"${model.name}"的问题
3.你只从知识库中选择内容进行回答
4.如果问题不在知识库中,你会回答:"我不知道。"
请务必遵守规则`
}
])
];
const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({
messages: [{ obj: 'System', value: fixedSystemPrompt }]
messages: fixedPrompts
});
const maxTokens = modelConstantsData.systemMaxToken - fixedSystemTokens;
const filterSystemPrompt = filterRate
@@ -105,37 +126,45 @@ ${
length: Math.floor(maxTokens * rate)
})
)
.join('\n');
.join('\n')
.trim();
/* 高相似度+不回复 */
if (!filterSystemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity) {
return {
code: 201,
searchPrompt: {
obj: ChatRoleEnum.AI,
value: '对不起,你的问题不在知识库中。'
}
searchPrompts: [
{
obj: ChatRoleEnum.System,
value: '对不起,你的问题不在知识库中。'
}
]
};
}
/* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */
if (!filterSystemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.noContext) {
return {
code: 200,
searchPrompt: model.chat.systemPrompt
? {
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
: undefined
searchPrompts: model.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
: []
};
}
/* 有匹配 */
return {
code: 200,
searchPrompt: {
obj: ChatRoleEnum.System,
value: `${fixedSystemPrompt}'${filterSystemPrompt}'`
}
searchPrompts: [
{
obj: ChatRoleEnum.System,
value: `知识库:${filterSystemPrompt}`
},
...fixedPrompts
]
};
};

View File

@@ -1,5 +1,6 @@
import { NextApiResponse } from 'next';
import { openaiError, openaiError2, proxyError, ERROR_RESPONSE, ERROR_ENUM } from './errorCode';
import { clearCookie } from './utils/tools';
export interface ResponseType<T = any> {
code: number;
@@ -23,7 +24,7 @@ export const jsonRes = <T = any>(
if (ERROR_RESPONSE[errResponseKey]) {
// login is expired
if (errResponseKey === ERROR_ENUM.unAuthorization) {
res.setHeader('Set-Cookie', 'token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT');
clearCookie(res);
}
return res.json(ERROR_RESPONSE[errResponseKey]);

View File

@@ -55,11 +55,11 @@ export const getApiKey = async ({
},
[OpenAiChatEnum.GPT4]: {
userOpenAiKey: user.openaiKey || '',
systemAuthKey: process.env.OPENAIKEY as string
systemAuthKey: process.env.GPT4KEY as string
},
[OpenAiChatEnum.GPT432k]: {
userOpenAiKey: user.openaiKey || '',
systemAuthKey: process.env.OPENAIKEY as string
systemAuthKey: process.env.GPT4KEY as string
},
[ClaudeEnum.Claude]: {
userOpenAiKey: '',
@@ -114,7 +114,7 @@ export const authModel = async ({
2. authUser = false and share, anyone can use
*/
if ((authOwner || (authUser && !model.share.isShare)) && userId !== String(model.userId)) {
return Promise.reject('无权操作该模型');
return Promise.reject(ERROR_ENUM.unAuthModel);
}
// do not share detail info

View File

@@ -25,9 +25,9 @@ export const lafClaudChat = async ({
.filter((item) => item.obj === 'System')
.map((item) => item.value)
.join('\n');
const systemPromptText = systemPrompt ? `\n知识库内容:'${systemPrompt}'\n` : '';
const systemPromptText = systemPrompt ? `你本次知识:${systemPrompt}\n` : '';
const prompt = `${systemPromptText}我的问题:'${messages[messages.length - 1].value}'`;
const prompt = `${systemPromptText}我的问题:'${messages[messages.length - 1].value}'`;
const lafResponse = await axios.post(
'https://hnvacz.laf.run/claude-gpt',

View File

@@ -109,35 +109,22 @@ export const ChatContextFilter = ({
// 根据 tokens 截断内容
const chats: ChatItemSimpleType[] = [];
let systemPrompt: ChatItemSimpleType | null = null;
// System 词保留
if (formatPrompts[0].obj === ChatRoleEnum.System) {
const prompt = formatPrompts.shift();
if (prompt) {
systemPrompt = prompt;
}
}
let messages: ChatItemSimpleType[] = [];
// 从后往前截取对话内容
for (let i = formatPrompts.length - 1; i >= 0; i--) {
chats.unshift(formatPrompts[i]);
messages = systemPrompt ? [systemPrompt, ...chats] : chats;
const tokens = modelToolMap[model].countTokens({
messages
messages: chats
});
/* 整体 tokens 超出范围 */
if (tokens >= maxTokens) {
return systemPrompt ? [systemPrompt, ...chats.slice(1)] : chats.slice(1);
/* 整体 tokens 超出范围, system必须保留 */
if (tokens >= maxTokens && formatPrompts[i].obj !== ChatRoleEnum.System) {
return chats.slice(1);
}
}
return messages;
return chats;
};
/* stream response */

View File

@@ -1,3 +1,4 @@
import type { NextApiResponse } from 'next';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
@@ -19,6 +20,15 @@ export const generateToken = (userId: string) => {
return token;
};
/* set cookie */
export const setCookie = (res: NextApiResponse, userId: string) => {
res.setHeader('Set-Cookie', `token=${generateToken(userId)}; Path=/; HttpOnly; Max-Age=604800`);
};
/* clear cookie */
export const clearCookie = (res: NextApiResponse) => {
res.setHeader('Set-Cookie', 'token=; Path=/; Max-Age=0');
};
/* openai axios config */
export const axiosConfig = () => ({
httpsAgent: global.httpsAgent,

View File

@@ -87,6 +87,13 @@ textarea::placeholder {
}
}
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
body {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
#nprogress .bar {
background: '#85b1ff' !important; //自定义颜色
}

View File

@@ -12,6 +12,7 @@ declare global {
var generatingVector: boolean;
var QRCode: any;
var httpsAgent: Agent;
var particlesJS: any;
interface Window {
['pdfjs-dist/build/pdf']: any;

View File

@@ -1,8 +1,12 @@
import { PRICE_SCALE } from '@/constants/common';
const tokenKey = 'fast-gpt-token';
import { loginOut } from '@/api/user';
export const clearToken = () => {
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
export const clearCookie = () => {
try {
loginOut();
} catch (error) {
error;
}
};
/**