Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1226c3efb7 | ||
|
|
39f9080eb2 | ||
|
|
3e4b165ed9 | ||
|
|
451f234f68 | ||
|
|
1ab45651e0 | ||
|
|
16f2ad7615 | ||
|
|
4ba4a99935 | ||
|
|
6f4471d2a0 | ||
|
|
250399a1aa | ||
|
|
1c8ce369b6 | ||
|
|
875f78b42c | ||
|
|
1e262a2198 | ||
|
|
25067a14a6 | ||
|
|
591cc21ff4 | ||
|
|
5a21eb9bc1 | ||
|
|
cdf4b9f324 | ||
|
|
e3c9b8179e | ||
|
|
9b683884cc | ||
|
|
4dc541e0a6 | ||
|
|
ef2de489be | ||
|
|
cb3b9efc6e | ||
|
|
a745993829 | ||
|
|
a837552b56 | ||
|
|
f52f514f5f | ||
|
|
fac53923dd | ||
|
|
b200731d17 | ||
|
|
de6ac0f589 | ||
|
|
18e0212d27 | ||
|
|
f20a5fe9a6 | ||
|
|
d351084688 | ||
|
|
d807f9d097 | ||
|
|
3ef6d3fe63 |
201
LICENSE
Normal 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
@@ -2,282 +2,43 @@
|
||||
|
||||
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,目前集成了 gpt35 和 embedding. 可构建自己的知识库。
|
||||
|
||||
## 知识库原理
|
||||
## 🛸 在线体验
|
||||
|
||||
🎉 [fastgpt.run](https://fastgpt.run/) (国内版)
|
||||
🎉 [ai.fastgpt.run](https://ai.fastgpt.run/) (海外版)
|
||||
|
||||

|
||||
|
||||
#### 知识库原理图
|
||||
|
||||

|
||||
|
||||
## 开发
|
||||
## 👨💻 开发
|
||||
|
||||
**配置环境变量**
|
||||
复制.env.template 文件,生成一个.env.local 环境变量文件夹,修改.env.local 内容,参考下方:
|
||||
项目技术栈: NextJs + TS + ChakraUI + Mongo + Postgres(Vector 插件)
|
||||
这是一个平台项目,非单机项目,除了模型调用外还涉及非常多用户的内容。
|
||||
[本地开发 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
|
||||

|
||||
|
||||
### 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)
|
||||
[](https://star-history.com/#c121914yu/FastGPT&Date)
|
||||
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
255
docs/deploy/docker.md
Normal 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)
|
||||
81
docs/deploy/fastgpt/docker-compose.yml
Normal 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
|
||||
|
||||
2
docs/deploy/fastgpt/docker-compose/init.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
cp ./docker-compose /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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、需要创建一个放置pg和mongo数据的文件夹,这里创建在`~/fastgpt`目录中,里面有`pg` 和`mongo `两个文件夹
|
||||
6、需要创建一个放置 pg 和 mongo 数据的文件夹,这里创建在`~/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 看项目是否跑起来了**
|
||||
BIN
docs/deploy/proxy/imgs/sealos1.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
docs/deploy/proxy/imgs/sealos2.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
docs/deploy/proxy/imgs/sealos3.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
docs/deploy/proxy/imgs/sealos4.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/deploy/proxy/imgs/sealos5.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
@@ -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
@@ -0,0 +1,100 @@
|
||||
# sealos 部署 openai 中转
|
||||
|
||||
## 登录 sealos cloud
|
||||
|
||||
[sealos cloud](https://cloud.sealos.io/)
|
||||
|
||||
## 创建应用
|
||||
|
||||
打开 App Launchpad -> 新建应用
|
||||
|
||||

|
||||

|
||||
|
||||
### 开启外网访问
|
||||
|
||||

|
||||
|
||||
### 添加 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. 点击确认
|
||||
|
||||

|
||||
|
||||
### 部署应用
|
||||
|
||||
填写完毕后,点击右上角的 `部署应用`,即可完成。
|
||||
|
||||
## 修改 FastGpt 环境变量
|
||||
|
||||
1. 进入刚刚部署应用的详情,复制外网地址
|
||||

|
||||
|
||||
2. 修改环境变量:
|
||||
|
||||
```
|
||||
OPENAI_BASE_URL=https://tgohwtdlrmer.cloud.sealos.io/openai/v1
|
||||
OPENAI_BASE_URL_AUTH=auth
|
||||
```
|
||||
|
||||
**Done!**
|
||||
47
docs/dev/README.md
Normal 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
|
After Width: | Height: | Size: 456 KiB |
BIN
docs/imgs/wx300.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
@@ -11,4 +11,6 @@ chatgpt 上下文最长 4096 tokens, 会自动截取上下文,超过 4096 部
|
||||
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/)
|
||||
**其他问题**
|
||||
请 WX 联系: fastgpt123
|
||||

|
||||
| 交流群 | 小助手 |
|
||||
| ----------------------- | -------------------- |
|
||||
|  |  |
|
||||
|
||||
@@ -1,43 +1,34 @@
|
||||
## 欢迎使用 Fast GPT
|
||||
|
||||
[Git 仓库](https://github.com/c121914yu/FastGPT)
|
||||
### 项目开源
|
||||
|
||||
### 交流群/问题反馈
|
||||
FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑。项目地址:[Git 仓库](https://github.com/c121914yu/FastGPT)
|
||||
|
||||
扫码满了,加个小号,定时拉
|
||||
wx 号: fastgpt123
|
||||

|
||||
### 开始使用知识库
|
||||
|
||||
### 快速开始
|
||||
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 消耗会加快。
|
||||
| 交流群 | 小助手 |
|
||||
| ----------------------- | -------------------- |
|
||||
|  |  |
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB |
BIN
public/imgs/wxqun300.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
9
public/js/particles.js
Normal 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 页面内容
|
||||
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
import { GET, POST, PUT } from './request';
|
||||
|
||||
export const getFilling = () => GET<{ beianText: string }>('/system/getFiling');
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
22
src/components/Loading/index.tsx
Normal 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;
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]
|
||||
];
|
||||
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 才计费
|
||||
|
||||
@@ -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, {
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 计算温度
|
||||
|
||||
@@ -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(
|
||||
|
||||
11
src/pages/api/system/getFiling.ts
Normal 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 || ''
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
16
src/pages/api/user/loginout.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
5
src/pages/index.module.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.home {
|
||||
* {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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="确认密码"
|
||||
|
||||
@@ -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="确认密码"
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -19,7 +19,7 @@ const ModelSchema = new Schema({
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '/icon/logo.png'
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
|
||||
@@ -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
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; //自定义颜色
|
||||
}
|
||||
|
||||
1
src/types/index.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||