Compare commits

..

46 Commits
v2.8 ... v3.0

Author SHA1 Message Date
archer
5969f5e0c5 docs and login direct 2023-04-28 14:16:30 +08:00
archer
ca8e940c9b perf: image and auth 2023-04-28 14:01:27 +08:00
archer
75073a64fb fix: chat window auth 2023-04-28 13:21:56 +08:00
archer
5b9185159d feat: collection model 2023-04-28 13:10:12 +08:00
archer
08ae4073bd feat: model set avatar 2023-04-28 10:06:14 +08:00
archer
606105d633 perf: select file 2023-04-28 09:32:03 +08:00
archer
3b8e5d2738 feat: model share market 2023-04-27 23:41:42 +08:00
archer
46eb96c72e perf: 去除冗余代码 2023-04-26 22:43:58 +08:00
晓杰
0540c2e46a ci: let image build by ci auto link to fastgpt repo
Signed-off-by: 晓杰 <2561589453@qq.com>
2023-04-26 21:56:36 +08:00
archer
d13b823065 Merge branch 'dev2.9' into main 2023-04-26 21:52:09 +08:00
archer
71f58b791f docs 2023-04-26 21:51:57 +08:00
archer
4b1cc6878c feat: 中转安全凭证;perf: 部署文件 2023-04-26 16:41:58 +08:00
archer
c6a5f16336 perf: stream 2023-04-26 13:50:09 +08:00
archer
7ed3c91ac6 docs: nginx proxy 2023-04-26 10:04:10 +08:00
archer
1f801d1464 fix: abort chat make page error 2023-04-25 23:47:24 +08:00
archer
d0e65431d0 fix: 反向代理 2023-04-25 23:14:46 +08:00
archer
a21b2ccdd0 fix: 反向代理 2023-04-25 22:29:43 +08:00
archer
8767c576be feat: stop chat 2023-04-25 21:06:04 +08:00
archer
fb08f61eb5 feat: openai base url 2023-04-25 20:13:29 +08:00
archer
ce68791c3c perf: not cut text when little text 2023-04-25 09:48:01 +08:00
archer
3294be5e7f perf: auto refresh split data 2023-04-24 23:41:11 +08:00
archer
ec86847280 docs 2023-04-24 19:05:08 +08:00
archer
dd2d93c953 perf: error response 2023-04-24 18:26:36 +08:00
archer
e60c36b423 perf: init chat content.use mongo aggregate 2023-04-24 18:26:36 +08:00
archer
1f112f7715 feat: chat content use tiktoken count 2023-04-24 18:26:35 +08:00
archer
adbaa8b37b feat: use Tiktokenizer to count tokens 2023-04-24 18:26:34 +08:00
archer
29c95d24ae perf: model data code 2023-04-24 18:26:34 +08:00
archer
e0b1a78344 feat: 拆分文本增加滑块,增加直接分段导入方式 2023-04-24 18:26:33 +08:00
archer
2774940851 feat: history chat 2023-04-24 18:26:32 +08:00
archer
c2c73ed23c perf: 生成对话框时机 2023-04-24 18:26:32 +08:00
archer
9682c82713 perf: 凭证校验 2023-04-24 18:26:31 +08:00
archer
e8d4933dc4 docs 2023-04-24 18:26:31 +08:00
archer
0b6020a9cd perf: 索引优化成hash 2023-04-24 18:26:26 +08:00
Archer
894beee266 Merge pull request #18 from xiao-jay/ci-buildimage
ci: build arm and amd image when push main branch
2023-04-24 18:25:39 +08:00
晓杰
405e453ed3 ci: build arm and amd image when push
Signed-off-by: 晓杰 <2561589453@qq.com>
2023-04-23 14:19:49 +08:00
Archer
79d289e25b Merge pull request #17 from xiao-jay/install-mac-docs
docs:how to run fast-gpt in mac
2023-04-23 08:02:23 +08:00
晓杰
51054f5829 docs:how to run fast-gpt in mac
Signed-off-by: 晓杰 <2561589453@qq.com>
2023-04-22 16:10:06 +08:00
archer
317fba1855 fix: init.sql 2023-04-22 10:54:08 +08:00
archer
f61e467d04 fix: 邀请id 2023-04-21 23:47:52 +08:00
archer
27de1cad47 fix: 短信验证码首位不能为0 2023-04-21 23:40:20 +08:00
archer
3ea2cf1dcb perf: chat上下文截断;QA提示词 2023-04-21 23:30:26 +08:00
archer
4397a0ad6b feat: 好友邀请 2023-04-21 22:23:19 +08:00
archer
4f51839026 perf: 参数值 2023-04-21 19:55:56 +08:00
archer
3c0fa30aaf perf: 对话框优化;feat: 模糊搜索 2023-04-20 23:06:30 +08:00
archer
02abe42afe feat: docs 2023-04-20 09:49:35 +08:00
archer
088a90de10 perf: 过滤器 2023-04-19 23:56:16 +08:00
145 changed files with 3508 additions and 3227 deletions

View File

@@ -1,13 +1,27 @@
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT_FAST=7890
AXIOS_PROXY_PORT_NORMAL=7890
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false
MY_MAIL=11111111@qq.com
MAILE_CODE=sdasadasfasfad
aliAccessKeyId=阿里云keyid
aliAccessKeySecret=阿里云keysecret
aliSignName=短信签名
aliTemplateCode=SMS_号
TOKEN_KEY=sssssssss
OPENAIKEY=sk-afadfadfadfsd
REDIS_URL=redis://default:password@0.0.0.0:8100
# proxy
# AXIOS_PROXY_HOST=127.0.0.1
# AXIOS_PROXY_PORT=7890
# OPENAI_BASE_URL=https://api.openai.com/v1
# OPENAI_BASE_URL_AUTH=可选的安全凭证
# 是否开启队列任务。 1-开启0-关闭请求parentUrl去执行任务,单机时直接填1
queueTask=1
parentUrl=https://hostname/api/openapi/startEvents
# email
MY_MAIL=xxx@qq.com
MAILE_CODE=xxx
# ali ems
aliAccessKeyId=xxx
aliAccessKeySecret=xxx
aliSignName=xxx
aliTemplateCode=SMS_xxx
# token
TOKEN_KEY=xxx
# openai
OPENAIKEY=sk-xxx
# db
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
PG_HOST=0.0.0.0
PG_PORT=8100
PG_USER=xxx
PG_PASSWORD=xxx
PG_DB_NAME=xxx

50
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- 'main'
jobs:
release:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Install Dependencies
run: |
sudo apt update && sudo apt install -y nodejs npm
- # Add support for more platforms with QEMU (optional)
# https://github.com/docker/setup-qemu-action
name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
driver-opts: network=host
- name: Login to gitbub
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_PAT }}
- name: build and publish image
env:
# fork friendly ^^
DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/fast-gpt
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--label "org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/FastGPT" \
--label "org.opencontainers.image.description=fast-gpt image" \
--label "org.opencontainers.image.licenses=MIT" \
--push \
-t ${DOCKER_REPO}:latest \
-f Dockerfile \
.

1
.gitignore vendored
View File

@@ -34,7 +34,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
/public/trainData/
/.vscode/
platform.json
testApi/

298
README.md
View File

@@ -1,142 +1,100 @@
# Fast GPT
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,目前集成了 gpt35 和 embedding. 可构建自己的知识库
## 知识库原理
![KBProcess](docs/imgs/KBProcess.jpg?raw=true "KBProcess")
## 开发
复制 .env.template 成 .env.local ,填写核心参数
```
AXIOS_PROXY_HOST=axios代理地址目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
AXIOS_PROXY_PORT_FAST=代理端口1,clash默认为7890
AXIOS_PROXY_PORT_NORMAL=代理端口2
MONGODB_URI=mongo数据库地址
MY_MAIL=发送验证码邮箱
MAILE_CODE=邮箱秘钥代理里设置的是QQ邮箱不知道怎么找这个 code 的,可以百度搜"nodemailer发送邮件"
TOKEN_KEY=随便填一个,用于生成和校验 token
OPENAIKEY=openai的key
REDIS_URL=redis的地址
```
**配置环境变量**
```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_HOST=0.0.0.0
PG_PORT=8102
PG_USER=xxx
PG_PASSWORD=xxx
PG_DB_NAME=xxx
```
**运行**
```
pnpm dev
```
## 部署
### docker 模式
请准备好 docker mongo代理, 和 nginx。 镜像走本机的代理,所以用 network=hostport 改成代理的端口clash 一般都是 7890。
### 代理环境(国外服务器可忽略)
1. [clash 方案](./docs/proxy/clash.md) - 仅需一台服务器(需要有 clash
2. [nginx 方案](./docs/proxy/nginx.md) - 需要一台国外服务器
3. [cloudflare 方案](./docs/proxy/cloudflare.md) - 需要有域名(每日免费 10w 次代理请求)
#### docker 打包
```bash
docker build -t imageName:tag .
docker push imageName:tag
# 或者直接拉镜像,见下方
```
#### 软件教程docker 安装
### docker 部署
#### 1. 安装 docker 和 docker-compose
这个不同系统略有区别,百度安装下。验证安装成功后进行下一步。下面给出一个例子:
```bash
# 安装docker
curl -sSL https://get.daocloud.io/docker | sh
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
```
#### 软件教程: clash 代理
```bash
# 下载包
curl https://glados.rocks/tools/clash-linux.zip -o clash.zip
# 解压
unzip clash.zip
# 下载终端配置⽂件(改成自己配置文件路径)
curl https://update.glados-config.com/clash/98980/8f30944/70870/glados-terminal.yaml > config.yaml
# 赋予运行权限
chmod +x ./clash-linux-amd64-v1.10.0
# 记得配置端口变量:
export ALL_PROXY=socks5://127.0.0.1:7891
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
# 运行脚本: 删除clash - 到 clash 目录 - 删除缓存 - 执行运行. 会生成一个 nohup.out 文件,可以看到 clash 的 logs
OLD_PROCESS=$(pgrep clash)
if [ ! -z "$OLD_PROCESS" ]; then
echo "Killing old process: $OLD_PROCESS"
kill $OLD_PROCESS
fi
sleep 2
cd **/clash
rm -f ./nohup.out || true
rm -f ./cache.db || true
nohup ./clash-linux-amd64-v1.10.0 -d ./ &
echo "Restart clash"
#### 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
```
#### 文件创建
**yml文件**
```yml
version: "3.3"
services:
fast-gpt:
image: c121914yu/fast-gpt:latest
environment:
AXIOS_PROXY_HOST: 127.0.0.1
AXIOS_PROXY_PORT: 7890
MY_MAIL: 11111111@qq.com
MAILE_CODE: sdasadasfasfad
TOKEN_KEY: sssssssss
MONGODB_URI: mongodb://username:password@0.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false
OPENAIKEY: sk-afadfadfadfsd
REDIS_URL: redis://default:password@0.0.0.0:8100
network_mode: host
restart: always
container_name: fast-gpt
mongodb:
image: mongo:6.0.4
container_name: mongo
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=ROOT_1234
- MONGO_DATA_DIR=/data/db
- MONGO_LOG_DIR=/data/logs
volumes:
- /root/fastgpt/mongo/data:/data/db
- /root/fastgpt/mongo/logs:/data/logs
ports:
- 27017:27017
nginx:
image: nginx:alpine3.17
container_name: nginx
restart: always
network_mode: host
ports:
- "80:80"
volumes:
- /root/fastgpt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
redis-stack:
image: redis/redis-stack:6.2.6-v6
container_name: redis-stack
restart: unless-stopped
ports:
- "8100:6379"
- "8101:8001"
environment:
- REDIS_ARGS=--requirepass psw1234
volumes:
- /etc/localtime:/etc/localtime:ro
- /root/fastgpt/redis/redis.conf:/redis.conf
- /root/fastgpt/redis/data:/data
```
**redis.conf**
```
## 开启aop持久化
appendonly yes
#default: 持久化文件
appendfilename "appendonly.aof"
#default: 每秒同步一次
appendfsync everysec
```
**nginx.conf**
**/root/fast-gpt/nginx/nginx.conf**
```conf
user nginx;
worker_processes auto;
@@ -169,9 +127,9 @@ http {
server {
listen 443 ssl;
server_name fastgpt.ahapocket.cn;
ssl_certificate /ssl/fastgpt.pem;
ssl_certificate_key /ssl/fastgpt.key;
server_name docgpt.ahapocket.cn;
ssl_certificate /ssl/docgpt.pem;
ssl_certificate_key /ssl/docgpt.key;
ssl_session_timeout 5m;
location / {
@@ -183,25 +141,88 @@ http {
}
server {
listen 80;
server_name fastgpt.ahapocket.cn;
server_name docgpt.ahapocket.cn;
rewrite ^(.*) https://$server_name$1 permanent;
}
}
```
#### 运行脚本
**redis 初始化**
```bash
# 进入容器
docker exec -it 容器ID bash
redis-cli -p 6379
auth psw1234
# 添加索引
FT.CREATE idx:model:data:hash ON HASH PREFIX 1 model:data: SCHEMA modelId TAG userId TAG status TAG q TEXT text TEXT vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
**/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=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:4.0.1
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 重新拉取镜像完成!"
@@ -220,3 +241,16 @@ do
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)

66
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,66 @@
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:4.0.1
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

49
deploy/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,49 @@
user nginx;
worker_processes auto;
worker_rlimit_nofile 51200;
events {
worker_connections 1024;
}
http {
access_log off;
server_names_hash_bucket_size 512;
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;
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;
}
}

19
deploy/pg/init.sql Normal file
View File

@@ -0,0 +1,19 @@
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

19
deploy/run.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/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

BIN
docs/imgs/KBProcess.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

100
docs/mac.md Normal file
View File

@@ -0,0 +1,100 @@
## 怎么在mac上面部署fastgpt
### 前置条件
1、可以 curl api.openai.com
2、有openai key
3、有邮箱MAILE_CODE
4、有docker
```
docker -v
```
5、有pnpm ,可以使用`brew install pnpm`安装
6、需要创建一个放置pg和mongo数据的文件夹这里创建在`~/fastgpt`目录中,里面有`pg``mongo `两个文件夹
```
➜ fast-gpt pwd
/Users/jie/fast-gpt
➜ fast-gpt ls
mongo pg
```
### docker部署方式
这种方式主要是为了方便调试,可以使用`pnpm dev ` 运行fast-gpt项目
**1、.env.local 文件**
```
# proxy
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT_FAST=7890
AXIOS_PROXY_PORT_NORMAL=7890
queueTask=1
# email
MY_MAIL= {Your Mail}
MAILE_CODE={Yoir Mail code}
# ali ems
aliAccessKeyId=xxx
aliAccessKeySecret=xxx
aliSignName=xxx
aliTemplateCode=SMS_xxx
# token
TOKEN_KEY=sswada
# openai
OPENAIKEY={Your openapi key}
# db
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
PG_HOST=0.0.0.0
PG_PORT=8100
PG_USER=xxx
PG_PASSWORD=xxx
PG_DB_NAME=xxx
```
**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**
```
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容器运行
```
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 (status);
CREATE INDEX modelData_modelId_index ON modelData (modelId);
CREATE INDEX modelData_userId_index ON modelData (userId);
EOSQL
```
4、**最后在FASTGPT项目里面运行pnpm dev 运行项目然后进入localhost:3000 看项目是否跑起来了**

68
docs/proxy/clash.md Normal file
View File

@@ -0,0 +1,68 @@
# 安装 clash
clash 会在本机启动代理。对应的,你需要配置项目的两个环境变量:
```
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT=7890
```
需要注的是,在你的 config.yaml 文件中,最好仅指定 api.openai.com 走代理,其他请求都直连。
**安装clash**
```bash
# 下载包
curl https://glados.rocks/tools/clash-linux.zip -o clash.zip
# 解压
unzip clash.zip
# 下载终端配置⽂件(改成自己配置文件路径)
curl https://update.glados-config.com/clash/98980/8f30944/70870/glados-terminal.yaml > config.yaml
# 赋予运行权限
chmod +x ./clash-linux-amd64-v1.10.0
```
**runClash.sh**
```sh
# 记得配置端口变量:
export ALL_PROXY=socks5://127.0.0.1:7891
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
# 运行脚本: 删除clash - 到 clash 目录 - 删除缓存 - 执行运行. 会生成一个 nohup.out 文件,可以看到 clash 的 logs
OLD_PROCESS=$(pgrep clash)
if [ ! -z "$OLD_PROCESS" ]; then
echo "Killing old process: $OLD_PROCESS"
kill $OLD_PROCESS
fi
sleep 2
cd **/clash
rm -f ./nohup.out || true
rm -f ./cache.db || true
nohup ./clash-linux-amd64-v1.10.0 -d ./ &
echo "Restart clash"
```
**config.yaml配置例子**
```yaml
mixed-port: 7890
allow-lan: false
bind-address: '*'
mode: rule
log-level: warning
dns:
enable: true
ipv6: false
nameserver:
- 8.8.8.8
- 8.8.4.4
cache-size: 400
proxies:
-
proxy-groups:
- { name: '♻️ 自动选择', type: url-test, proxies: [香港V01×1.5], url: 'https://api.openai.com', interval: 3600}
rules:
- 'DOMAIN-SUFFIX,api.openai.com,♻️ 自动选择'
- 'MATCH,DIRECT'
```

46
docs/proxy/cloudflare.md Normal file
View File

@@ -0,0 +1,46 @@
# cloudflare 代理配置
[来自 "不做了睡觉" 教程](https://gravel-twister-d32.notion.site/FastGPT-API-ba7bb261d5fd4fd9bbb2f0607dacdc9e)
**workers 配置文件**
```js
const TELEGRAPH_URL = 'https://api.openai.com';
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
// 安全校验
if (request.headers.get('auth') !== 'auth_code') {
return new Response('UnAuthorization', { status: 403 });
}
const url = new URL(request.url);
url.host = TELEGRAPH_URL.replace(/^https?:\/\//, '');
const modifiedRequest = new Request(url.toString(), {
headers: request.headers,
method: request.method,
body: request.body,
redirect: 'follow'
});
const response = await fetch(modifiedRequest);
const modifiedResponse = new Response(response.body, response);
// 添加允许跨域访问的响应头
modifiedResponse.headers.set('Access-Control-Allow-Origin', '*');
return modifiedResponse;
}
```
**对应的环境变量**
务必别忘了填 v1
```
OPENAI_BASE_URL=https://xxxxxx/v1
OPENAI_BASE_URL_AUTH=auth_code
```

72
docs/proxy/nginx.md Normal file
View File

@@ -0,0 +1,72 @@
# nginx 反向代理 openai 接口
如果你有国外的服务器,可以通过配置 nginx 反向代理,转发 openai 相关的请求,从而让国内的服务器可以通过访问该 nginx 去访问 openai 接口。
```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 32k;
large_client_header_buffers 4 32k;
client_max_body_size 50M;
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 your_host;
ssl_certificate /ssl/your_host.pem;
ssl_certificate_key /ssl/your_host.key;
ssl_session_timeout 5m;
location ~ /openai/(.*) {
# auth check
if ($http_authkey != "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;
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;
}
}
server {
listen 80;
server_name ai.fastgpt.run;
rewrite ^(.*) https://$server_name$1 permanent;
}
}
```

View File

@@ -1,13 +1,15 @@
/** @type {import('next').NextConfig} */
const path = require('path');
const isDev = process.env.NODE_ENV === 'development';
const nextConfig = {
output: 'standalone',
reactStrictMode: false,
compress: true,
webpack(config) {
config.experiments = {
asyncWebAssembly: true,
layers: true
};
config.module.rules = config.module.rules.concat([
{
test: /\.svg$/i,

View File

@@ -16,6 +16,8 @@
"@alicloud/tea-util": "^1.4.5",
"@chakra-ui/icons": "^2.0.17",
"@chakra-ui/react": "^2.5.1",
"@chakra-ui/system": "^2.5.5",
"@dqbd/tiktoken": "^1.0.6",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@next/font": "13.1.6",
@@ -27,7 +29,7 @@
"eventsource-parser": "^0.1.0",
"formidable": "^2.1.1",
"framer-motion": "^9.0.6",
"gpt-token-utils": "^1.2.0",
"graphemer": "^1.4.0",
"hyperdown": "^2.4.29",
"immer": "^9.0.19",
"jsonwebtoken": "^9.0.0",

137
pnpm-lock.yaml generated
View File

@@ -6,6 +6,8 @@ specifiers:
'@alicloud/tea-util': ^1.4.5
'@chakra-ui/icons': ^2.0.17
'@chakra-ui/react': ^2.5.1
'@chakra-ui/system': ^2.5.5
'@dqbd/tiktoken': ^1.0.6
'@emotion/react': ^11.10.6
'@emotion/styled': ^11.10.6
'@next/font': 13.1.6
@@ -31,7 +33,7 @@ specifiers:
eventsource-parser: ^0.1.0
formidable: ^2.1.1
framer-motion: ^9.0.6
gpt-token-utils: ^1.2.0
graphemer: ^1.4.0
husky: ^8.0.3
hyperdown: ^2.4.29
immer: ^9.0.19
@@ -68,8 +70,10 @@ dependencies:
'@alicloud/dysmsapi20170525': registry.npmmirror.com/@alicloud/dysmsapi20170525/2.0.23
'@alicloud/openapi-client': registry.npmmirror.com/@alicloud/openapi-client/0.4.5
'@alicloud/tea-util': registry.npmmirror.com/@alicloud/tea-util/1.4.5
'@chakra-ui/icons': registry.npmmirror.com/@chakra-ui/icons/2.0.17_react@18.2.0
'@chakra-ui/icons': registry.npmmirror.com/@chakra-ui/icons/2.0.17_lze4h7kxffpjhokvtqbtrlfkmq
'@chakra-ui/react': registry.npmmirror.com/@chakra-ui/react/2.5.1_e6pzu3hsaqmql4fl7jx73ckiym
'@chakra-ui/system': registry.npmmirror.com/@chakra-ui/system/2.5.5_xqp3pgpqjlfxxa3zxu4zoc4fba
'@dqbd/tiktoken': registry.npmmirror.com/@dqbd/tiktoken/1.0.6
'@emotion/react': registry.npmmirror.com/@emotion/react/11.10.6_pmekkgnqduwlme35zpnqhenc34
'@emotion/styled': registry.npmmirror.com/@emotion/styled/11.10.6_oouaibmszuch5k64ms7uxp2aia
'@next/font': registry.npmmirror.com/@next/font/13.1.6
@@ -81,7 +85,7 @@ dependencies:
eventsource-parser: registry.npmmirror.com/eventsource-parser/0.1.0
formidable: registry.npmmirror.com/formidable/2.1.1
framer-motion: registry.npmmirror.com/framer-motion/9.0.6_biqbaboplfbrettd7655fr4n2y
gpt-token-utils: registry.npmmirror.com/gpt-token-utils/1.2.0
graphemer: registry.npmmirror.com/graphemer/1.4.0
hyperdown: registry.npmmirror.com/hyperdown/2.4.29
immer: registry.npmmirror.com/immer/9.0.19
jsonwebtoken: registry.npmmirror.com/jsonwebtoken/9.0.0
@@ -3085,6 +3089,20 @@ packages:
react: registry.npmmirror.com/react/18.2.0
dev: false
registry.npmmirror.com/@chakra-ui/icon/3.0.16_lze4h7kxffpjhokvtqbtrlfkmq:
resolution: {integrity: sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icon/-/icon-3.0.16.tgz}
id: registry.npmmirror.com/@chakra-ui/icon/3.0.16
name: '@chakra-ui/icon'
version: 3.0.16
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
react: '>=18'
dependencies:
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
'@chakra-ui/system': registry.npmmirror.com/@chakra-ui/system/2.5.5_xqp3pgpqjlfxxa3zxu4zoc4fba
react: registry.npmmirror.com/react/18.2.0
dev: false
registry.npmmirror.com/@chakra-ui/icon/3.0.16_n3dxrjldmk5gnycgnw7noyo5tu:
resolution: {integrity: sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icon/-/icon-3.0.16.tgz}
id: registry.npmmirror.com/@chakra-ui/icon/3.0.16
@@ -3099,20 +3117,7 @@ packages:
react: registry.npmmirror.com/react/18.2.0
dev: false
registry.npmmirror.com/@chakra-ui/icon/3.0.16_react@18.2.0:
resolution: {integrity: sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icon/-/icon-3.0.16.tgz}
id: registry.npmmirror.com/@chakra-ui/icon/3.0.16
name: '@chakra-ui/icon'
version: 3.0.16
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
react: '>=18'
dependencies:
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
react: registry.npmmirror.com/react/18.2.0
dev: false
registry.npmmirror.com/@chakra-ui/icons/2.0.17_react@18.2.0:
registry.npmmirror.com/@chakra-ui/icons/2.0.17_lze4h7kxffpjhokvtqbtrlfkmq:
resolution: {integrity: sha512-HMJP0WrJgAmFR9+Xh/CBH0nVnGMsJ4ZC8MK6tMgxPKd9/muvn0I4hsicHqdPlLpmB0TlxlhkBAKaVMtOdz6F0w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icons/-/icons-2.0.17.tgz}
id: registry.npmmirror.com/@chakra-ui/icons/2.0.17
name: '@chakra-ui/icons'
@@ -3121,7 +3126,8 @@ packages:
'@chakra-ui/system': '>=2.0.0'
react: '>=18'
dependencies:
'@chakra-ui/icon': registry.npmmirror.com/@chakra-ui/icon/3.0.16_react@18.2.0
'@chakra-ui/icon': registry.npmmirror.com/@chakra-ui/icon/3.0.16_lze4h7kxffpjhokvtqbtrlfkmq
'@chakra-ui/system': registry.npmmirror.com/@chakra-ui/system/2.5.5_xqp3pgpqjlfxxa3zxu4zoc4fba
react: registry.npmmirror.com/react/18.2.0
dev: false
@@ -3868,6 +3874,16 @@ packages:
lodash.mergewith: registry.npmmirror.com/lodash.mergewith/4.6.2
dev: false
registry.npmmirror.com/@chakra-ui/styled-system/2.8.0:
resolution: {integrity: sha512-bmRv/8ACJGGKGx84U1npiUddwdNifJ+/ETklGwooS5APM0ymwUtBYZpFxjYNJrqvVYpg3mVY6HhMyBVptLS7iA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/styled-system/-/styled-system-2.8.0.tgz}
name: '@chakra-ui/styled-system'
version: 2.8.0
dependencies:
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
csstype: registry.npmmirror.com/csstype/3.1.1
lodash.mergewith: registry.npmmirror.com/lodash.mergewith/4.6.2
dev: false
registry.npmmirror.com/@chakra-ui/switch/2.0.22_6k64q2ggygf5zznlgufl3vff54:
resolution: {integrity: sha512-+/Yy6y7VFD91uSPruF8ZvePi3tl5D8UNVATtWEQ+QBI92DLSM+PtgJ2F0Y9GMZ9NzMxpZ80DqwY7/kqcPCfLvw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/switch/-/switch-2.0.22.tgz}
id: registry.npmmirror.com/@chakra-ui/switch/2.0.22
@@ -3907,6 +3923,28 @@ packages:
react-fast-compare: registry.npmmirror.com/react-fast-compare/3.2.0
dev: false
registry.npmmirror.com/@chakra-ui/system/2.5.5_xqp3pgpqjlfxxa3zxu4zoc4fba:
resolution: {integrity: sha512-52BIp/Zyvefgxn5RTByfkTeG4J+y81LWEjWm8jCaRFsLVm8IFgqIrngtcq4I7gD5n/UKbneHlb4eLHo4uc5yDQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/system/-/system-2.5.5.tgz}
id: registry.npmmirror.com/@chakra-ui/system/2.5.5
name: '@chakra-ui/system'
version: 2.5.5
peerDependencies:
'@emotion/react': ^11.0.0
'@emotion/styled': ^11.0.0
react: '>=18'
dependencies:
'@chakra-ui/color-mode': registry.npmmirror.com/@chakra-ui/color-mode/2.1.12_react@18.2.0
'@chakra-ui/object-utils': registry.npmmirror.com/@chakra-ui/object-utils/2.0.8
'@chakra-ui/react-utils': registry.npmmirror.com/@chakra-ui/react-utils/2.0.12_react@18.2.0
'@chakra-ui/styled-system': registry.npmmirror.com/@chakra-ui/styled-system/2.8.0
'@chakra-ui/theme-utils': registry.npmmirror.com/@chakra-ui/theme-utils/2.0.15
'@chakra-ui/utils': registry.npmmirror.com/@chakra-ui/utils/2.0.15
'@emotion/react': registry.npmmirror.com/@emotion/react/11.10.6_pmekkgnqduwlme35zpnqhenc34
'@emotion/styled': registry.npmmirror.com/@emotion/styled/11.10.6_oouaibmszuch5k64ms7uxp2aia
react: registry.npmmirror.com/react/18.2.0
react-fast-compare: registry.npmmirror.com/react-fast-compare/3.2.1
dev: false
registry.npmmirror.com/@chakra-ui/table/2.0.16_n3dxrjldmk5gnycgnw7noyo5tu:
resolution: {integrity: sha512-vWDXZ6Ad3Aj66curp1tZBHvCfQHX2FJ4ijLiqGgQszWFIchfhJ5vMgEBJaFMZ+BN1draAjuRTZqaQefOApzvRg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/table/-/table-2.0.16.tgz}
id: registry.npmmirror.com/@chakra-ui/table/2.0.16
@@ -3988,6 +4026,20 @@ packages:
color2k: registry.npmmirror.com/color2k/2.0.2
dev: false
registry.npmmirror.com/@chakra-ui/theme-tools/2.0.17_wv7sq5bj4kx5i3evdevscgumbi:
resolution: {integrity: sha512-Auu38hnihlJZQcPok6itRDBbwof3TpXGYtDPnOvrq4Xp7jnab36HLt7KEXSDPXbtOk3ZqU99pvI1en5LbDrdjg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme-tools/-/theme-tools-2.0.17.tgz}
id: registry.npmmirror.com/@chakra-ui/theme-tools/2.0.17
name: '@chakra-ui/theme-tools'
version: 2.0.17
peerDependencies:
'@chakra-ui/styled-system': '>=2.0.0'
dependencies:
'@chakra-ui/anatomy': registry.npmmirror.com/@chakra-ui/anatomy/2.1.2
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
'@chakra-ui/styled-system': registry.npmmirror.com/@chakra-ui/styled-system/2.8.0
color2k: registry.npmmirror.com/color2k/2.0.2
dev: false
registry.npmmirror.com/@chakra-ui/theme-utils/2.0.11:
resolution: {integrity: sha512-lBAay6Sq3/fl7exd3mFxWAbzgdQowytor0fnlHrpNStn1HgFjXukwsf6356XQOie2Vd8qaMM7qZtMh4AiC0dcg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme-utils/-/theme-utils-2.0.11.tgz}
name: '@chakra-ui/theme-utils'
@@ -3999,6 +4051,17 @@ packages:
lodash.mergewith: registry.npmmirror.com/lodash.mergewith/4.6.2
dev: false
registry.npmmirror.com/@chakra-ui/theme-utils/2.0.15:
resolution: {integrity: sha512-UuxtEgE7gwMTGDXtUpTOI7F5X0iHB9ekEOG5PWPn2wWBL7rlk2JtPI7UP5Um5Yg6vvBfXYGK1ySahxqsgf+87g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme-utils/-/theme-utils-2.0.15.tgz}
name: '@chakra-ui/theme-utils'
version: 2.0.15
dependencies:
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
'@chakra-ui/styled-system': registry.npmmirror.com/@chakra-ui/styled-system/2.8.0
'@chakra-ui/theme': registry.npmmirror.com/@chakra-ui/theme/3.0.1_wv7sq5bj4kx5i3evdevscgumbi
lodash.mergewith: registry.npmmirror.com/lodash.mergewith/4.6.2
dev: false
registry.npmmirror.com/@chakra-ui/theme/2.2.5_es2flcfvdj7o2v4vs237ptvmhy:
resolution: {integrity: sha512-hYASZMwu0NqEv6PPydu+F3I+kMNd44yR4TwjR/lXBz/LEh64L6UPY6kQjebCfgdVtsGdl3HKg+eLlfa7SvfRgw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme/-/theme-2.2.5.tgz}
id: registry.npmmirror.com/@chakra-ui/theme/2.2.5
@@ -4013,6 +4076,20 @@ packages:
'@chakra-ui/theme-tools': registry.npmmirror.com/@chakra-ui/theme-tools/2.0.17_es2flcfvdj7o2v4vs237ptvmhy
dev: false
registry.npmmirror.com/@chakra-ui/theme/3.0.1_wv7sq5bj4kx5i3evdevscgumbi:
resolution: {integrity: sha512-92kDm/Ux/51uJqhRKevQo/O/rdwucDYcpHg2QuwzdAxISCeYvgtl2TtgOOl5EnqEP0j3IEAvZHZUlv8TTbawaw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme/-/theme-3.0.1.tgz}
id: registry.npmmirror.com/@chakra-ui/theme/3.0.1
name: '@chakra-ui/theme'
version: 3.0.1
peerDependencies:
'@chakra-ui/styled-system': '>=2.0.0'
dependencies:
'@chakra-ui/anatomy': registry.npmmirror.com/@chakra-ui/anatomy/2.1.2
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
'@chakra-ui/styled-system': registry.npmmirror.com/@chakra-ui/styled-system/2.8.0
'@chakra-ui/theme-tools': registry.npmmirror.com/@chakra-ui/theme-tools/2.0.17_wv7sq5bj4kx5i3evdevscgumbi
dev: false
registry.npmmirror.com/@chakra-ui/toast/6.0.1_jgj3ekl54faqnu3nlobnfmds2q:
resolution: {integrity: sha512-ej2kJXvu/d2h6qnXU5D8XTyw0qpsfmbiU7hUffo/sPxkz89AUOQ08RUuUmB1ssW/FZcQvNMJ5WgzCTKHGBxtxw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/toast/-/toast-6.0.1.tgz}
id: registry.npmmirror.com/@chakra-ui/toast/6.0.1
@@ -4101,6 +4178,12 @@ packages:
react: registry.npmmirror.com/react/18.2.0
dev: false
registry.npmmirror.com/@dqbd/tiktoken/1.0.6:
resolution: {integrity: sha512-umSdeZTy/SbPPKVuZKV/XKyFPmXSN145CcM3iHjBbmhlohBJg7vaDp4cPCW+xNlWL6L2U1sp7T2BD+di2sUKdA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@dqbd/tiktoken/-/tiktoken-1.0.6.tgz}
name: '@dqbd/tiktoken'
version: 1.0.6
dev: false
registry.npmmirror.com/@emotion/babel-plugin/11.10.6:
resolution: {integrity: sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz}
name: '@emotion/babel-plugin'
@@ -7583,12 +7666,6 @@ packages:
get-intrinsic: registry.npmmirror.com/get-intrinsic/1.2.0
dev: true
registry.npmmirror.com/gpt-token-utils/1.2.0:
resolution: {integrity: sha512-s8twaU38UE2Vp65JhQEjz8qvWhWY8KZYvmvYHapxlPT03Ok35Clq+gm9eE27wQILdFisseMVRSiC5lJR9GBklA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/gpt-token-utils/-/gpt-token-utils-1.2.0.tgz}
name: gpt-token-utils
version: 1.2.0
dev: false
registry.npmmirror.com/graceful-fs/4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz}
name: graceful-fs
@@ -7600,6 +7677,12 @@ packages:
version: 1.0.4
dev: true
registry.npmmirror.com/graphemer/1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz}
name: graphemer
version: 1.4.0
dev: false
registry.npmmirror.com/has-bigints/1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/has-bigints/-/has-bigints-1.0.2.tgz}
name: has-bigints
@@ -10182,6 +10265,12 @@ packages:
version: 3.2.0
dev: false
registry.npmmirror.com/react-fast-compare/3.2.1:
resolution: {integrity: sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz}
name: react-fast-compare
version: 3.2.1
dev: false
registry.npmmirror.com/react-focus-lock/2.9.4_pmekkgnqduwlme35zpnqhenc34:
resolution: {integrity: sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-focus-lock/-/react-focus-lock-2.9.4.tgz}
id: registry.npmmirror.com/react-focus-lock/2.9.4

View File

@@ -1,9 +1,10 @@
## 常见问题
### 常见问题
**请求次数太多了**
一般是因为自己的 openai 账号异常。请先检查自己的账号是否正常使用。
**内容长度**
chatgpt 上下文最长 4096 tokens, 上下文超长时会报错。
**删除和复制**
点击对话头像,可以选择复制删除该条内容
电脑端:聊天内容右侧有复制删除的图标
移动端:点击对话头像,可以选择复制或删除该条内容。
**代理出错**
服务器代理不稳定,可以过一会儿再尝试。
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/)

View File

@@ -15,6 +15,16 @@ wx号: fastgpt123
4. 进入模型页,创建一个模型,建议直接用 ChatGPT。
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
### 价格表
如果使用了自己的 Api Key不会计费。可以在账号页看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| chatgpt - 对话 | 0.03 |
| 知识库 - 对话 | 0.03 |
| 知识库 - 索引 | 0.004 |
| 文件拆分 | 0.03 |
### 定制 prompt
1. 进入模型编辑页
@@ -29,12 +39,3 @@ wx号: fastgpt123
4. 使用该模型对话。
注意使用知识库模型对话时tokens 消耗会加快。
### 价格表
如果使用了自己的 Api Key不会计费。可以在账号页看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| chatgpt - 对话 | 0.03 |
| 知识库 - 对话 | 0.03 |
| 知识库 - 索引 | 0.004 |
| 文件拆分 | 0.03 |

View File

@@ -1,3 +1,6 @@
## Fast GPT V2.7
* FastGpt Api 允许你将 Fast Gpt 的部分功能通过 api 的形式,将知识库接入到自己的应用中,例如:飞书、企业微信、客服助手.
* 通过 csv 文件导入和导出你的问答对。你可以将你的 csv 文件放置在飞书文档上,以便团队共享。
### Fast GPT V3.0
- 新增 - 模型共享市场,可以使用其他用户分享的模型。
- 新增 - 邀请好友注册功能。
- 优化 - 选择文件,采用链式处理,避免卡死。
- 修复 - 导入时分段问题。

BIN
public/imgs/modelAvatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
public/imgs/wx300-2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -406,12 +406,12 @@ function getVerbosityLevel() {
}
function info(msg) {
if (verbosity >= VerbosityLevel.INFOS) {
console.log(`Info: ${msg}`);
// console.log(`Info: ${msg}`);
}
}
function warn(msg) {
if (verbosity >= VerbosityLevel.WARNINGS) {
console.log(`Warning: ${msg}`);
// console.log(`Warning: ${msg}`);
}
}
function unreachable(msg) {
@@ -4206,7 +4206,7 @@ function loadScript(src, removeScriptElement = false) {
});
}
function deprecated(details) {
console.log("Deprecated API usage: " + details);
// console.log("Deprecated API usage: " + details);
}
let pdfDateStringRegex;
class PDFDateString {

View File

@@ -1008,12 +1008,12 @@ function getVerbosityLevel() {
}
function info(msg) {
if (verbosity >= VerbosityLevel.INFOS) {
console.log(`Info: ${msg}`);
// console.log(`Info: ${msg}`);
}
}
function warn(msg) {
if (verbosity >= VerbosityLevel.WARNINGS) {
console.log(`Warning: ${msg}`);
// console.log(`Warning: ${msg}`);
}
}
function unreachable(msg) {

View File

@@ -1,41 +1,32 @@
import { GET, POST, DELETE } from './request';
import type { ChatItemType, ChatSiteItemType } from '@/types/chat';
import type { ChatItemType } from '@/types/chat';
import type { InitChatResponse } from './response/chat';
/**
* 获取一个聊天框的ID
*/
export const getChatSiteId = (modelId: string) => GET<string>(`/chat/generate?modelId=${modelId}`);
/**
* 获取初始化聊天内容
*/
export const getInitChatSiteInfo = (chatId: string) =>
GET<InitChatResponse>(`/chat/init?chatId=${chatId}`);
export const getInitChatSiteInfo = (modelId: string, chatId: '' | string) =>
GET<InitChatResponse>(`/chat/init?modelId=${modelId}&chatId=${chatId}`);
/**
* 发送 GPT3 prompt
* 获取历史记录
*/
export const postGPT3SendPrompt = ({
chatId,
prompt
}: {
prompt: ChatSiteItemType[];
chatId: string;
}) =>
POST<string>(`/chat/gpt3`, {
chatId,
prompt: prompt.map((item) => ({
obj: item.obj,
value: item.value
}))
});
export const getChatHistory = () =>
GET<{ _id: string; title: string; modelId: string }[]>('/chat/getHistory');
/**
* 删除一条历史记录
*/
export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${id}`);
/**
* 存储一轮对话
*/
export const postSaveChat = (data: { chatId: string; prompts: ChatItemType[] }) =>
POST('/chat/saveChat', data);
export const postSaveChat = (data: {
modelId: string;
chatId: '' | string;
prompts: ChatItemType[];
}) => POST<string>('/chat/saveChat', data);
/**
* 删除一句对话

View File

@@ -1,25 +0,0 @@
import { GET, POST, DELETE, PUT } from './request';
import { RequestPaging } from '../types/index';
import { Obj2Query } from '@/utils/tools';
import type { DataListItem } from '@/types/data';
import type { PagingData } from '../types/index';
import type { DataItemSchema } from '@/types/mongoSchema';
import type { CreateDataProps } from '@/pages/data/components/CreateDataModal';
export const getDataList = () => GET<DataListItem[]>(`/data/getDataList`);
export const postData = (data: CreateDataProps) => POST<string>(`/data/postData`, data);
export const postSplitData = (dataId: string, text: string) =>
POST(`/data/splitData`, { dataId, text });
export const updateDataName = (dataId: string, name: string) =>
PUT(`/data/putDataName?dataId=${dataId}&name=${name}`);
export const delData = (dataId: string) => DELETE(`/data/delData?dataId=${dataId}`);
type GetDataItemsProps = RequestPaging & {
dataId: string;
};
export const getDataItems = (data: GetDataItemsProps) =>
GET<PagingData<DataItemSchema>>(`/data/getDataItems?${Obj2Query(data)}`);

View File

@@ -6,7 +6,7 @@ interface StreamFetchProps {
abortSignal: AbortController;
}
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise(async (resolve, reject) => {
new Promise<string>(async (resolve, reject) => {
try {
const res = await fetch(url, {
method: 'POST',
@@ -23,26 +23,30 @@ export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchPr
let responseText = '';
const read = async () => {
const { done, value } = await reader?.read();
if (done) {
if (res.status === 200) {
resolve(responseText);
} else {
try {
try {
const { done, value } = await reader?.read();
if (done) {
if (res.status === 200) {
resolve(responseText);
} else {
const parseError = JSON.parse(responseText);
reject(parseError?.message || '请求异常');
} catch (err) {
reject('请求异常');
}
}
return;
return;
}
const text = decoder.decode(value).replace(/<br\/>/g, '\n');
res.status === 200 && onMessage(text);
responseText += text;
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve(responseText);
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
const text = decoder.decode(value).replace(/<br\/>/g, '\n');
res.status === 200 && onMessage(text);
responseText += text;
read();
};
read();
} catch (err: any) {
console.log(err, '====');

View File

@@ -1,7 +1,6 @@
import { GET, POST, DELETE, PUT } from './request';
import type { ModelSchema, ModelDataSchema, ModelSplitDataSchema } from '@/types/mongoSchema';
import { ModelUpdateParams } from '@/types/model';
import { TrainingItemType } from '../types/training';
import type { ModelSchema, ModelDataSchema } from '@/types/mongoSchema';
import { ModelUpdateParams, ShareModelItem } from '@/types/model';
import { RequestPaging } from '../types/index';
import { Obj2Query } from '@/utils/tools';
@@ -32,21 +31,7 @@ export const getModelById = (id: string) => GET<ModelSchema>(`/model/detail?mode
export const putModelById = (id: string, data: ModelUpdateParams) =>
PUT(`/model/update?modelId=${id}`, data);
export const postTrainModel = (id: string, form: FormData) =>
POST(`/model/train/train?modelId=${id}`, form, {
headers: {
'content-type': 'multipart/form-data'
}
});
export const putModelTrainingStatus = (id: string) =>
PUT(`/model/train/putTrainStatus?modelId=${id}`);
export const getModelTrainings = (id: string) =>
GET<TrainingItemType[]>(`/model/train/getTrainings?modelId=${id}`);
/* 模型 data */
type GetModelDataListProps = RequestPaging & {
modelId: string;
searchText: string;
@@ -85,8 +70,12 @@ export const postModelDataInput = (data: {
/**
* 拆分数据
*/
export const postModelDataSplitData = (data: { modelId: string; text: string; prompt: string }) =>
POST(`/model/data/splitData`, data);
export const postModelDataSplitData = (data: {
modelId: string;
chunks: string[];
prompt: string;
mode: 'qa' | 'subsection';
}) => POST(`/model/data/splitData`, data);
/**
* json导入数据
@@ -104,3 +93,19 @@ export const putModelDataById = (data: { dataId: string; a: string; q?: string }
*/
export const delOneModelData = (dataId: string) =>
DELETE(`/model/data/delModelDataById?dataId=${dataId}`);
/* 共享市场 */
/**
* 获取共享市场模型
*/
export const getShareModelList = (data: { searchText?: string } & RequestPaging) =>
POST(`/model/share/getModels`, data);
/**
* 获取收藏的模型
*/
export const getCollectionModels = () => GET<ShareModelItem[]>(`/model/share/getCollection`);
/**
* 收藏/取消收藏模型
*/
export const triggerModelCollection = (modelId: string) =>
POST<number>(`/model/share/collection?modelId=${modelId}`);

View File

@@ -1,6 +1,6 @@
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { getToken, clearToken } from '@/utils/user';
import { TOKEN_ERROR_CODE } from '@/constants/responseCode';
import { TOKEN_ERROR_CODE } from '@/service/errorCode';
interface ConfigType {
headers?: { [key: string]: string };

View File

@@ -1,5 +1,13 @@
import type { UserType } from '@/types/user';
import type { PromotionRecordSchema } from '@/types/mongoSchema';
export interface ResLogin {
token: string;
user: UserType;
}
export interface PromotionRecordType {
_id: PromotionRecordSchema['_id'];
type: PromotionRecordSchema['type'];
createTime: PromotionRecordSchema['createTime'];
amount: PromotionRecordSchema['amount'];
}

View File

@@ -1,6 +1,6 @@
import { GET, POST, PUT } from './request';
import { createHashPassword, Obj2Query } from '@/utils/tools';
import { ResLogin } from './response/user';
import { ResLogin, PromotionRecordType } from './response/user';
import { UserAuthTypeEnum } from '@/constants/common';
import { UserType, UserUpdateParams } from '@/types/user';
import type { PagingData, RequestPaging } from '@/types';
@@ -17,6 +17,14 @@ export const sendAuthCode = ({
export const getTokenLogin = () => GET<UserType>('/user/tokenLogin');
/* get promotion init data */
export const getPromotionInitData = () =>
GET<{
invitedAmount: number;
historyAmount: number;
residueAmount: number;
}>('/user/promotion/getPromotionData');
export const postRegister = ({
username,
password,
@@ -73,3 +81,7 @@ export const getPayCode = (amount: number) =>
}>(`/user/getPayCode?amount=${amount}`);
export const checkPayResult = (payId: string) => GET<number>(`/user/checkPayResult?payId=${payId}`);
/* promotion records */
export const getPromotionRecords = (data: RequestPaging) =>
GET<PromotionRecordType>(`/user/promotion/getPromotions?${Obj2Query(data)}`);

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="204px" height="204px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="84" cy="50" r="10" fill="#e15b64">
<animate attributeName="r" repeatCount="indefinite" dur="0.5681818181818182s" calcMode="spline" keyTimes="0;1" values="15;0" keySplines="0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="fill" repeatCount="indefinite" dur="2.272727272727273s" calcMode="discrete" keyTimes="0;0.25;0.5;0.75;1" values="#e15b64;#abbd81;#f8b26a;#f47e60;#e15b64" begin="0s"></animate>
</circle><circle cx="16" cy="50" r="10" fill="#e15b64">
<animate attributeName="r" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;15;15;15" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
</circle><circle cx="50" cy="50" r="10" fill="#f47e60">
<animate attributeName="r" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;15;15;15" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.5681818181818182s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.5681818181818182s"></animate>
</circle><circle cx="84" cy="50" r="10" fill="#f8b26a">
<animate attributeName="r" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;15;15;15" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.1363636363636365s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.1363636363636365s"></animate>
</circle><circle cx="16" cy="50" r="10" fill="#abbd81">
<animate attributeName="r" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;15;15;15" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7045454545454546s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7045454545454546s"></animate>
</circle>
<!-- [ldio] generated by https://loading.io/ --></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682602070818" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2479" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M509.606998 143.114488c9.082866 0 17.327644 4.840238 20.996197 12.331863l97.262184 197.441814c5.613858 11.403724 16.663518 19.358907 29.438473 21.216207l223.738737 32.552393c8.420787 1.215688 15.604396 6.851035 18.23327 14.254655 2.520403 7.18361 0.595564 15.062044-5.084808 20.586874L730.253304 601.611947c-8.949836 8.751315-12.994965 21.171182-10.916631 33.370015l38.011732 222.060515c1.325182 7.737218-2.165316 15.426341-8.905834 19.978007-4.088108 2.741437-8.861832 4.155646-13.812587 4.155646-4.022617 0-7.999185-0.972141-11.425214-2.740414L528.149307 775.671215c-5.768377-3.006474-12.155854-4.552689-18.542308-4.552689-6.364965 0-12.727882 1.547239-18.518772 4.552689L296.254819 878.348736c-3.559059 1.855254-7.602142 2.828418-11.668761 2.828418-4.861728 0-9.723455-1.459235-13.546527-4.022617-6.961552-4.684696-10.475586-12.419867-9.127891-20.155039l38.011732-222.016513c2.078335-12.198833-1.988284-24.619724-10.939143-33.370015L125.02397 441.443038c-5.635347-5.492084-7.55814-13.348006-5.061272-20.453844 2.63092-7.481392 9.812483-13.116739 18.298761-14.332427l223.674269-32.552393c12.839423-1.857301 23.867594-9.813506 29.481452-21.216207l97.194646-197.396789C492.325403 147.965983 500.590648 143.114488 509.606998 143.114488M509.606998 104.904235c-24.043602 0-45.922912 13.226233-56.177464 33.95637L356.189863 336.302419l-223.674269 32.54216c-22.983457 3.304256-42.100864 18.718317-49.481971 39.659255-7.381108 21.048385-1.812275 44.23241 14.431687 60.033281l163.916257 160.125931-38.011732 222.016513c-3.868097 22.408359 6.03239 44.819788 25.458835 57.94676 10.69662 7.116071 23.204491 10.784624 35.757388 10.784624 10.298554 0 20.663622-2.475378 30.055526-7.337105l194.987926-102.7205L704.662463 912.072815c9.369392 4.861728 19.712971 7.337105 29.990035 7.337105 12.57541 0 25.082258-3.668553 35.778878-10.784624 19.426445-13.126972 29.305443-35.538401 25.460882-57.94676l-38.012755-222.016513 163.937746-160.125931c16.22145-15.812127 21.810748-38.984896 14.408151-60.033281-7.402597-20.940938-26.51898-36.353976-49.503461-39.659255L663.04767 336.302419l-97.240695-197.441814C555.619962 118.131491 533.695626 104.904235 509.606998 104.904235L509.606998 104.904235z" p-id="2480"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682602068431" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2339" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M335.008 916.629333c-35.914667 22.314667-82.88 10.773333-104.693333-25.557333a77.333333 77.333333 0 0 1-8.96-57.429333l46.485333-198.24a13.141333 13.141333 0 0 0-4.021333-12.864l-152.16-132.586667c-31.605333-27.52-35.253333-75.648-8.234667-107.733333a75.68 75.68 0 0 1 51.733333-26.752L354.848 339.2c4.352-0.362667 8.245333-3.232 10.026667-7.594667l76.938666-188.170666c16.032-39.2 60.618667-57.92 99.52-41.461334a76.309333 76.309333 0 0 1 40.832 41.461334l76.938667 188.16c1.781333 4.373333 5.674667 7.253333 10.026667 7.605333l199.712 16.277333c41.877333 3.413333 72.885333 40.458667 69.568 82.517334a76.938667 76.938667 0 0 1-26.08 51.978666l-152.16 132.586667c-3.541333 3.082667-5.141333 8.074667-4.021334 12.853333l46.485334 198.24c9.621333 41.013333-15.36 82.336-56.138667 92.224a75.285333 75.285333 0 0 1-57.525333-9.237333l-170.976-106.24a11.296 11.296 0 0 0-12.010667 0l-170.986667 106.24z" p-id="2340"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682232349111" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7070" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="28"><path d="M512 102.6c110.7 0 215 12.3 293.9 34.7 35.8 10.2 65 22.1 84.5 34.7 18.6 12 21.3 19.7 21.6 20.6-0.2 0.9-3 8.6-21.6 20.6-19.5 12.5-48.7 24.5-84.5 34.7-78.9 22.3-183.2 34.7-293.9 34.7s-215-12.3-293.9-34.7c-35.8-10.2-65-22.1-84.5-34.7-18.6-12-21.3-19.7-21.6-20.6 0.2-0.9 3-8.6 21.6-20.6 19.5-12.5 48.7-24.5 84.5-34.7 78.9-22.4 183.2-34.7 293.9-34.7m0-40c-243 0-440 58.2-440 130s197 130 440 130 440-58.2 440-130-197-130-440-130zM112 190.4H72v641h40v-641z m840-0.3h-40v641h40v-641zM912 831v0.5c-0.2 0.9-3 8.6-21.6 20.6-19.5 12.5-48.7 24.5-84.5 34.7-78.9 22.3-183.2 34.6-293.9 34.6s-215-12.3-293.9-34.7c-35.8-10.2-65-22.1-84.5-34.7-18.6-12-21.3-19.7-21.6-20.6v-0.3l-40 0.3v0.1c0 71.8 197 130 440 130s440-58.2 440-130v-0.4l-40-0.1z m0-210.5v0.5c-0.2 0.9-3 8.6-21.6 20.6-19.5 12.5-48.7 24.5-84.5 34.7C727 698.6 622.7 711 512 711s-215-12.3-293.9-34.7c-35.8-10.2-65-22.1-84.5-34.7-18.6-12-21.3-19.7-21.6-20.6v-0.3l-40 0.3v0.1c0 71.8 197 130 440 130s440-58.2 440-130v-0.4l-40-0.2z m0-221.5v0.5c-0.2 0.9-3 8.6-21.6 20.6-19.5 12.5-48.7 24.5-84.5 34.7-78.9 22.3-183.2 34.7-293.9 34.7s-215-12.3-293.9-34.7c-35.8-10.2-65-22.1-84.5-34.7-18.6-12-21.3-19.7-21.6-20.6v-0.3l-40 0.3v0.1c0 71.8 197 130 440 130s440-58.2 440-130v-0.4l-40-0.2z" fill="" p-id="7071"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1681997838051" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4520" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M898 178.7H665.3c4.3-9.8 6.7-20.6 6.7-32 0-44-36-80-80-80H432c-44 0-80 36-80 80 0 11.4 2.4 22.2 6.7 32H126c-13.2 0-24 10.8-24 24s10.8 24 24 24h772c13.2 0 24-10.8 24-24s-10.8-24-24-24z m-466 0c-8.5 0-16.5-3.4-22.6-9.4-6.1-6.1-9.4-14.1-9.4-22.6s3.4-16.5 9.4-22.6c6.1-6.1 14.1-9.4 22.6-9.4h160c8.5 0 16.5 3.4 22.6 9.4 6.1 6.1 9.4 14.1 9.4 22.6 0 8.5-3.4 16.5-9.4 22.6-6.1 6.1-14.1 9.4-22.6 9.4H432zM513 774.7c18.1 0 33-14.8 33-33v-334c0-18.1-14.9-33-33-33h-2c-18.1 0-33 14.8-33 33v334c0 18.2 14.8 33 33 33h2zM363 774.7c18.1 0 33-14.8 33-33v-334c0-18.1-14.9-33-33-33h-2c-18.1 0-33 14.8-33 33v334c0 18.2 14.8 33 33 33h2zM663 774.7c18.1 0 33-14.8 33-33v-334c0-18.1-14.9-33-33-33h-2c-18.1 0-33 14.8-33 33v334c0 18.2 14.8 33 33 33h2z" p-id="4521"></path><path d="M812 280.7c-13.3 0-24 10.7-24 24v530c0 41.9-34.1 76-76 76H312c-41.9 0-76-34.1-76-76v-530c0-13.3-10.7-24-24-24s-24 10.7-24 24v530c0 68.4 55.6 124 124 124h400c68.4 0 124-55.6 124-124v-530c0-13.2-10.7-24-24-24z" p-id="4522"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682232686576" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8959" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="28"><path d="M762.805186 140.938939c-14.335497-9.66922-33.725102-5.887081-43.373857 8.398274-9.648754 14.295588-5.897314 33.714869 8.398274 43.373857 106.369609 71.852468 169.864736 191.267185 169.864736 319.445496 0 212.414831-172.802648 385.217479-385.217479 385.217479S127.259382 724.571397 127.259382 512.156566c0-128.178311 63.494103-247.593028 169.864736-319.445496 14.295588-9.658987 18.047028-29.078269 8.398274-43.373857-9.658987-14.285355-29.088502-18.067494-43.373857-8.398274C138.575102 224.432539 64.791655 363.206162 64.791655 512.156566c0 246.851131 200.834074 447.685205 447.685205 447.685205S960.162066 759.007697 960.162066 512.156566C960.162066 363.206162 886.377596 224.432539 762.805186 140.938939z" p-id="8960"></path><path d="M401.003 64.47136c-17.253966 0-31.234375 13.980409-31.234375 31.233352l0 30.470989c0 17.253966 13.980409 31.234375 31.234375 31.234375s31.234375-13.980409 31.234375-31.234375L432.237375 95.704712C432.236352 78.450746 418.256966 64.47136 401.003 64.47136z" p-id="8961"></path><path d="M623.950721 64.47136c-17.253966 0-31.233352 13.980409-31.233352 31.233352l0 30.470989c0 17.253966 13.980409 31.234375 31.233352 31.234375s31.234375-13.980409 31.234375-31.234375L655.185097 95.704712C655.184073 78.450746 641.204687 64.47136 623.950721 64.47136z" p-id="8962"></path><path d="M426.012603 227.493248c11.214413 18.047028 41.970904 48.589648 86.157265 48.589648 43.963281 0 75.105558-30.318516 86.574774-48.223305 9.222035-14.396895 5.03262-33.358759-9.242502-42.763966-14.304797-9.405207-33.593096-5.398964-43.159986 8.764618-0.132006 0.193405-13.614066 19.754926-34.172287 19.754926-19.989263 0-32.423457-18.098193-33.267685-19.36914-9.160637-14.427594-28.264741-18.799158-42.834574-9.770528C421.416935 193.584973 416.912341 212.841549 426.012603 227.493248z" p-id="8963"></path><path d="M510.781242 335.164502c-17.253966 0-31.233352 13.980409-31.233352 31.233352l0 208.225415c0 0.63445 0.149403 1.227967 0.187265 1.853208 0.067538 1.115404 0.148379 2.217505 0.333598 3.314489 0.168846 1.00898 0.416486 1.978051 0.679475 2.951215 0.258896 0.954745 0.529049 1.895163 0.87595 2.821255 0.36839 0.981351 0.801249 1.916653 1.26276 2.847861 0.431835 0.876973 0.880043 1.734504 1.393743 2.569522 0.532119 0.860601 1.115404 1.670036 1.727341 2.472308 0.610914 0.805342 1.235131 1.588171 1.926886 2.336208 0.688685 0.74292 1.424442 1.420349 2.181689 2.093684 0.741897 0.659009 1.484817 1.303692 2.298346 1.89721 0.899486 0.657986 1.850138 1.222851 2.819209 1.783623 0.544399 0.314155 1.00898 0.714268 1.577938 0.998747l208.225415 104.113219c4.484128 2.236947 9.252735 3.304256 13.94971 3.304256 11.44875 0 22.479991-6.334265 27.959795-17.274432 7.706519-15.433504 1.454118-34.192753-13.970176-41.909505l-190.961216-95.480608L542.015617 366.397854C542.015617 349.143888 528.035208 335.164502 510.781242 335.164502z" p-id="8964"></path></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682078370900" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3577" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M941.312 888.704H628.032a32 32 0 0 1 0-64h313.28a32 32 0 0 1 0 64zM519.808 576.768c-158.976 0-288.384-129.344-288.384-288.384S360.832 0 519.808 0s288.384 129.344 288.384 288.384-129.408 288.384-288.384 288.384z m0-512.768C396.096 64 295.424 164.672 295.424 288.384s100.672 224.384 224.384 224.384c123.776 0 224.384-100.672 224.384-224.384S643.584 64 519.808 64z" p-id="3578"></path><path d="M763.264 606.528a31.552 31.552 0 0 1-16.96-4.864 427.2 427.2 0 0 0-100.544-45.952 32 32 0 0 1-21.184-40 31.744 31.744 0 0 1 39.936-21.184 492.16 492.16 0 0 1 115.712 52.864 32 32 0 0 1-16.96 59.136zM59.776 996.928a32 32 0 0 1-32-32 489.6 489.6 0 0 1 347.328-470.464 32 32 0 1 1 18.816 61.184 425.856 425.856 0 0 0-302.144 409.28 32 32 0 0 1-32 32zM964.224 879.68a32.128 32.128 0 0 1-24.32-11.2l-108.224-126.336a32 32 0 1 1 48.64-41.6l108.224 126.336a32 32 0 0 1-24.32 52.8z" p-id="3579"></path><path d="M856 1024a32 32 0 0 1-25.664-51.2l108.224-144.32a32.064 32.064 0 0 1 51.264 38.336L881.6 1011.2a32 32 0 0 1-25.6 12.8z" p-id="3580"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682599933100" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5707" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M750.592 668.7232a159.6416 159.6416 0 0 0-128.4608 64.9216l-269.568-138.24a159.3344 159.3344 0 0 0 17.0496-128.6656l261.12-136.704a159.4368 159.4368 0 1 0-31.1296-53.0432L341.1456 412.3648a159.7952 159.7952 0 1 0-32.256 229.7856l286.72 146.9952a159.7952 159.7952 0 1 0 154.88-120.4224z m0-542.72a98.3552 98.3552 0 1 1-98.3552 98.3552 98.4576 98.4576 0 0 1 98.3552-98.304z m-534.2208 484.352A98.3552 98.3552 0 1 1 314.7264 512a98.4576 98.4576 0 0 1-98.3552 98.3552zM750.592 926.72a98.3552 98.3552 0 1 1 98.3552-98.3552A98.4576 98.4576 0 0 1 750.592 926.72z" p-id="5708"></path></svg>

After

Width:  |  Height:  |  Size: 915 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682424901088" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3662" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M885.333333 85.333333H138.666667a53.393333 53.393333 0 0 0-53.333334 53.333334v746.666666a53.393333 53.393333 0 0 0 53.333334 53.333334h746.666666a53.393333 53.393333 0 0 0 53.333334-53.333334V138.666667a53.393333 53.393333 0 0 0-53.333334-53.333334z m-160 602.666667a37.373333 37.373333 0 0 1-37.333333 37.333333H336a37.373333 37.373333 0 0 1-37.333333-37.333333V336a37.373333 37.373333 0 0 1 37.333333-37.333333h352a37.373333 37.373333 0 0 1 37.333333 37.333333z" p-id="3663"></path></svg>

After

Width:  |  Height:  |  Size: 823 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682079057126" class="icon" viewBox="0 0 1322 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2677" xmlns:xlink="http://www.w3.org/1999/xlink" width="36.1484375" height="28"><path d="M952.04654459 837.88839531H336.95615443A113.52706888 113.52706888 0 0 1 223.61160468 724.54384556v-419.79462838h728.43493991a113.52706888 113.52706888 0 0 1 113.34454973 113.34454975V724.54384556a113.52706888 113.52706888 0 0 1-113.34454973 113.34454975zM278.36742569 359.13999928v365.03880736a58.77124787 58.77124787 0 0 0 58.58872873 58.58872874h615.09039016a58.77124787 58.77124787 0 0 0 58.58872874-58.58872874V417.72872802a58.77124787 58.77124787 0 0 0-58.58872874-58.58872874z" p-id="2678"></path><path d="M278.36742569 350.37906772H223.61160468V297.44844068A111.51935577 111.51935577 0 0 1 334.94844068 186.11160469h334.01050924a111.51935577 111.51935577 0 0 1 111.33683598 111.33683599v49.09771996h-54.75582101V297.44844068A56.76353475 56.76353475 0 0 0 668.95894991 240.8674257H334.94844068A56.76353475 56.76353475 0 0 0 278.36742569 297.44844068zM1038.19570329 704.83175018H825.92563707A131.59649008 131.59649008 0 0 1 825.92563707 441.63877003h208.43715913v54.75582103H825.92563707a76.84066906 76.84066906 0 0 0 0 153.86385725h212.27006621z" p-id="2679"></path><path d="M889.80742792 600.43065117h-65.34194654a27.37791082 27.37791082 0 0 1 0-54.75582103h65.34194654a27.37791082 27.37791082 0 0 1-1e-8 54.75582103z" p-id="2680"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -13,14 +13,30 @@ const map = {
board: require('./icons/board.svg').default,
develop: require('./icons/develop.svg').default,
user: require('./icons/user.svg').default,
chatting: require('./icons/chatting.svg').default
promotion: require('./icons/promotion.svg').default,
delete: require('./icons/delete.svg').default,
withdraw: require('./icons/withdraw.svg').default,
dbModel: require('./icons/dbModel.svg').default,
history: require('./icons/history.svg').default,
stop: require('./icons/stop.svg').default,
shareMarket: require('./icons/shareMarket.svg').default,
collectionLight: require('./icons/collectionLight.svg').default,
collectionSolid: require('./icons/collectionSolid.svg').default
};
export type IconName = keyof typeof map;
const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps) => {
return map[name] ? (
<Icon as={map[name]} w={w} h={h} boxSizing={'content-box'} verticalAlign={'top'} {...props} />
<Icon
as={map[name]}
w={w}
h={h}
boxSizing={'content-box'}
verticalAlign={'top'}
fill={'currentcolor'}
{...props}
/>
) : null;
};

View File

@@ -7,7 +7,8 @@ import { useQuery } from '@tanstack/react-query';
const unAuthPage: { [key: string]: boolean } = {
'/': true,
'/login': true
'/login': true,
'/model/share': true
};
const Auth = ({ children }: { children: JSX.Element }) => {
@@ -33,7 +34,9 @@ const Auth = ({ children }: { children: JSX.Element }) => {
{
onError(error) {
console.log('error->', error);
router.push('/login');
router.replace(
`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`
);
toast();
},
onSettled() {

View File

@@ -20,18 +20,31 @@ const navbarList = [
link: '/',
activeLink: ['/']
},
{
label: '共享',
icon: 'shareMarket',
link: '/model/share',
activeLink: ['/model/share']
},
{
label: '模型',
icon: 'model',
link: '/model/list',
activeLink: ['/model/list', '/model/detail']
},
{
label: '账号',
icon: 'user',
link: '/number/setting',
activeLink: ['/number/setting']
},
{
label: '邀请',
icon: 'promotion',
link: '/promotion',
activeLink: ['/promotion']
},
{
label: '开发',
icon: 'develop',

View File

@@ -1,4 +1,4 @@
import React, { memo, useMemo } from 'react';
import React, { memo } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
@@ -13,7 +13,6 @@ import styles from './index.module.scss';
import { codeLight } from './codeLight';
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
const formatSource = useMemo(() => source, [source]);
const { copyData } = useCopyData();
return (
@@ -63,7 +62,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
}}
linkTarget="_blank"
>
{formatSource}
{source}
</ReactMarkdown>
);
};

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { Stack, Box, Flex, useTheme } from '@chakra-ui/react';
import type { StackProps } from '@chakra-ui/react';
// @ts-ignore
interface Props extends StackProps {
list: { label: string; value: string | number }[];
value: string | number;
onChange: (e: string | number) => void;
}
const Radio = ({ list, value, onChange, ...props }: Props) => {
return (
<Stack {...props} spacing={5} direction={'row'}>
{list.map((item) => (
<Flex
key={item.value}
alignItems={'center'}
cursor={'pointer'}
userSelect={'none'}
_before={{
content: '""',
w: '16px',
h: '16px',
mr: 1,
borderRadius: '16px',
transition: '0.2s',
...(value === item.value
? {
border: '5px solid',
borderColor: 'blue.500'
}
: {
border: '2px solid',
borderColor: 'gray.200'
})
}}
_hover={{
_before: {
borderColor: 'blue.400'
}
}}
onClick={() => onChange(item.value)}
>
{item.label}
</Flex>
))}
</Stack>
);
};
export default Radio;

View File

@@ -1,6 +0,0 @@
import type { DataType } from '@/types/data';
export const DataTypeTextMap: Record<DataType, string> = {
QA: '问答拆分',
abstract: '摘要总结'
};

View File

@@ -1,28 +1,27 @@
import type { ServiceName, ModelDataType, ModelSchema } from '@/types/mongoSchema';
import type { ModelSchema } from '@/types/mongoSchema';
export enum ModelDataStatusEnum {
ready = 'ready',
waiting = 'waiting'
export const embeddingModel = 'text-embedding-ada-002';
export enum ChatModelEnum {
'GPT35' = 'gpt-3.5-turbo',
'GPT4' = 'gpt-4',
'GPT432k' = 'gpt-4-32k'
}
export enum ChatModelNameEnum {
export enum ModelNameEnum {
GPT35 = 'gpt-3.5-turbo',
VECTOR_GPT = 'VECTOR_GPT',
VECTOR = 'text-embedding-ada-002'
VECTOR_GPT = 'VECTOR_GPT'
}
export const ChatModelNameMap = {
[ChatModelNameEnum.GPT35]: 'gpt-3.5-turbo',
[ChatModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo',
[ChatModelNameEnum.VECTOR]: 'text-embedding-ada-002'
export const Model2ChatModelMap: Record<`${ModelNameEnum}`, `${ChatModelEnum}`> = {
[ModelNameEnum.GPT35]: 'gpt-3.5-turbo',
[ModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo'
};
export type ModelConstantsData = {
serviceCompany: `${ServiceName}`;
icon: 'model' | 'dbModel';
name: string;
model: `${ChatModelNameEnum}`;
model: `${ModelNameEnum}`;
trainName: string; // 空字符串代表不能训练
maxToken: number;
contextMaxToken: number;
maxTemperature: number;
price: number; // 多少钱 / 1token单位: 0.00001元
@@ -30,34 +29,25 @@ export type ModelConstantsData = {
export const modelList: ModelConstantsData[] = [
{
serviceCompany: 'openai',
icon: 'model',
name: 'chatGPT',
model: ChatModelNameEnum.GPT35,
model: ModelNameEnum.GPT35,
trainName: '',
maxToken: 4000,
contextMaxToken: 7500,
contextMaxToken: 4096,
maxTemperature: 1.5,
price: 3
},
{
serviceCompany: 'openai',
icon: 'dbModel',
name: '知识库',
model: ChatModelNameEnum.VECTOR_GPT,
model: ModelNameEnum.VECTOR_GPT,
trainName: 'vector',
maxToken: 4000,
contextMaxToken: 7000,
contextMaxToken: 4096,
maxTemperature: 1,
price: 3
}
];
export enum TrainingStatusEnum {
pending = 'pending',
succeed = 'succeed',
errored = 'errored',
canceled = 'canceled'
}
export enum ModelStatusEnum {
running = 'running',
training = 'training',
@@ -84,6 +74,11 @@ export const formatModelStatus = {
}
};
export enum ModelDataStatusEnum {
ready = 'ready',
waiting = 'waiting'
}
export const ModelDataStatusMap: Record<`${ModelDataStatusEnum}`, string> = {
ready: '训练完成',
waiting: '训练中'
@@ -118,24 +113,26 @@ export const ModelVectorSearchModeMap: Record<
};
export const defaultModel: ModelSchema = {
_id: '',
userId: '',
name: '',
avatar: '',
_id: 'modelId',
userId: 'userId',
name: 'modelName',
avatar: '/icon/logo.png',
status: ModelStatusEnum.pending,
updateTime: Date.now(),
trainingTimes: 0,
systemPrompt: '',
intro: '',
temperature: 5,
search: {
mode: ModelVectorSearchModeEnum.hightSimilarity
},
share: {
isShare: false,
isShareDetail: false,
intro: '',
collection: 0
},
service: {
company: 'openai',
trainId: '',
chatModel: ChatModelNameEnum.GPT35,
modelName: ChatModelNameEnum.GPT35
chatModel: ModelNameEnum.GPT35,
modelName: ModelNameEnum.GPT35
},
security: {
domain: ['*'],

View File

@@ -1,6 +0,0 @@
export const VecModelDataPrefix = 'model:data';
export const VecModelDataIdx = `idx:${VecModelDataPrefix}:hash`;
export enum ModelDataStatusEnum {
ready = 'ready',
waiting = 'waiting'
}

View File

@@ -1,20 +0,0 @@
export const ERROR_CODE: { [key: number]: string } = {
400: '请求失败',
401: '无权访问',
403: '紧张访问',
404: '请求不存在',
405: '请求方法错误',
406: '请求的格式错误',
410: '资源已删除',
422: '验证错误',
500: '服务器发生错误',
502: '网关错误',
503: '服务器暂时过载或维护',
504: '网关超时'
};
export const TOKEN_ERROR_CODE: { [key: number]: string } = {
506: '请先登录',
507: '请重新登录',
508: '登录已过期'
};

View File

@@ -20,3 +20,15 @@ export const BillTypeMap: Record<`${BillTypeEnum}`, string> = {
[BillTypeEnum.vector]: '索引生成',
[BillTypeEnum.return]: '退款'
};
export enum PromotionEnum {
invite = 'invite',
shareModel = 'shareModel',
withdraw = 'withdraw'
}
export const PromotionTypeMap = {
[PromotionEnum.invite]: '好友充值',
[PromotionEnum.shareModel]: '模型分享',
[PromotionEnum.withdraw]: '提现'
};

View File

@@ -18,7 +18,7 @@ export const usePagination = <T = any,>({
const [pageNum, setPageNum] = useState(1);
const [total, setTotal] = useState(0);
const [data, setData] = useState<T[]>([]);
const maxPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);
const maxPage = useMemo(() => Math.ceil(total / pageSize) || 1, [pageSize, total]);
const { mutate, isLoading } = useMutation({
mutationFn: async (num: number = pageNum) => {
@@ -41,10 +41,6 @@ export const usePagination = <T = any,>({
}
});
useEffect(() => {
mutate(1);
}, []);
const Pagination = useCallback(() => {
return (
<Flex alignItems={'center'} justifyContent={'end'}>
@@ -93,6 +89,10 @@ export const usePagination = <T = any,>({
);
}, [maxPage, mutate, pageNum]);
useEffect(() => {
mutate(1);
}, []);
return {
pageNum,
pageSize,

15
src/pages/_error.tsx Normal file
View File

@@ -0,0 +1,15 @@
function Error({ statusCode }: { statusCode: number }) {
return (
<p>
{statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'}
</p>
);
}
Error.getInitialProps = ({ res, err }: { res: any; err: any }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
console.log(err);
return { statusCode };
};
export default Error;

View File

@@ -1,11 +1,9 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { httpsAgent, openaiChatFilter } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { getOpenAIApi, authChat } from '@/service/utils/auth';
import { axiosConfig, openaiChatFilter } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import type { ModelSchema } from '@/types/mongoSchema';
import { PassThrough } from 'stream';
import { modelList } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
@@ -28,29 +26,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
try {
const { chatId, prompt } = req.body as {
const { chatId, prompt, modelId } = req.body as {
prompt: ChatItemType;
chatId: string;
modelId: string;
chatId: '' | string;
};
const { authorization } = req.headers;
if (!chatId || !prompt) {
if (!modelId || !prompt) {
throw new Error('缺少参数');
}
await connectToDatabase();
let startTime = Date.now();
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
const { model, content, userApiKey, systemKey, userId } = await authChat({
modelId,
chatId,
authorization
});
const model: ModelSchema = chat.modelId;
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
// 读取对话内容
const prompts = [...chat.content, prompt];
const prompts = [...content, prompt];
// 如果有系统提示词,自动插入
if (model.systemPrompt) {
@@ -61,31 +63,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
// 控制在 tokens 数量,防止超出
// const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// 格式化文本内容成 chatgpt 格式
const map = {
Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
};
const formatPrompts: ChatCompletionRequestMessage[] = prompts.map((item: ChatItemType) => ({
role: map[item.obj],
content: item.value
}));
// console.log(formatPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
// console.log(filterPrompts);
// 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey || systemKey);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: temperature,
// max_tokens: modelConstantsData.maxToken,
messages: formatPrompts,
temperature,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: true,
@@ -94,7 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{
timeout: 40000,
responseType: 'stream',
httpsAgent: httpsAgent(!userApiKey)
...axiosConfig
}
);
@@ -107,7 +101,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
stream,
chatResponse
});
const promptsContent = formatPrompts.map((item) => item.content).join('');
// 只有使用平台的 key 才计费
pushChatBill({
@@ -115,7 +108,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
modelName: model.service.modelName,
userId,
chatId,
text: promptsContent + responseContent
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});
} catch (err: any) {
if (step === 1) {

View File

@@ -1,54 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { ModelSchema } from '@/types/mongoSchema';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId } = req.query as {
modelId: string;
};
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权生成对话');
}
if (!modelId) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 校验是否为用户的模型
const model = await Model.findOne<ModelSchema>({
_id: modelId,
userId
});
if (!model) {
throw new Error('无权使用该模型');
}
// 创建 chat 数据
const response = await Chat.create({
userId,
modelId,
content: []
});
jsonRes(res, {
data: response._id // 即聊天框的 ID
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,31 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
/* 获取历史记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const userId = await authToken(req.headers.authorization);
await connectToDatabase();
const data = await Chat.find(
{
userId
},
'_id title modelId'
)
.sort({ updateTime: -1 })
.limit(20);
jsonRes(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,9 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import type { ChatPopulate } from '@/types/mongoSchema';
import type { InitChatResponse } from '@/api/response/chat';
import { authToken } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { authModel } from '@/service/utils/auth';
import mongoose from 'mongoose';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -11,43 +13,55 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { authorization } = req.headers;
const userId = await authToken(authorization);
const { chatId } = req.query as { chatId: string };
const { modelId, chatId } = req.query as { modelId: string; chatId: '' | string };
if (!chatId) {
if (!modelId) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 获取 chat 数据
const chat = await Chat.findOne<ChatPopulate>({
_id: chatId,
userId
}).populate({
path: 'modelId',
options: {
strictPopulate: false
}
});
// 获取 model 数据
const { model } = await authModel({ modelId, userId, authUser: false, authOwner: false });
if (!chat) {
throw new Error('聊天框不存在');
// 历史记录
let history: ChatItemType[] = [];
if (chatId) {
// 获取 chat.content 数据
history = await Chat.aggregate([
{
$match: {
_id: new mongoose.Types.ObjectId(chatId),
userId: new mongoose.Types.ObjectId(userId)
}
},
{ $unwind: '$content' },
{ $match: { 'content.deleted': false } },
{ $sort: { 'content._id': -1 } },
{ $limit: 50 },
{
$project: {
id: '$content._id',
obj: '$content.obj',
value: '$content.value'
}
}
]);
history.reverse();
}
// filter 掉被 deleted 的内容
chat.content = chat.content.filter((item) => item.deleted !== true);
const model = chat.modelId;
jsonRes<InitChatResponse>(res, {
data: {
chatId: chat._id,
modelId: model._id,
chatId: chatId || '',
modelId: modelId,
name: model.name,
avatar: model.avatar,
intro: model.intro,
intro: model.share.intro,
modelName: model.service.modelName,
chatModel: model.service.chatModel,
history: chat.content
history
}
});
} catch (err) {

View File

@@ -0,0 +1,27 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { ChatItemType } from '@/types/chat';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
/* 获取历史记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { id } = req.query;
const userId = await authToken(req.headers.authorization);
await connectToDatabase();
await Chat.findOneAndRemove({
_id: id,
userId
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -2,34 +2,55 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { ChatItemType } from '@/types/chat';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authModel } from '@/service/utils/auth';
import { authToken } from '@/service/utils/tools';
/* 聊天内容存存储 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, prompts } = req.body as {
chatId: string;
const { chatId, modelId, prompts } = req.body as {
chatId: '' | string;
modelId: string;
prompts: ChatItemType[];
};
if (!chatId || !prompts) {
if (!prompts) {
throw new Error('缺少参数');
}
const userId = await authToken(req.headers.authorization);
await connectToDatabase();
// 存入库
await Chat.findByIdAndUpdate(chatId, {
$push: {
content: {
$each: prompts.map((item) => ({
obj: item.obj,
value: item.value
}))
}
},
updateTime: new Date()
});
const content = prompts.map((item) => ({
obj: item.obj,
value: item.value
}));
await authModel({ modelId, userId, authOwner: false });
// 没有 chatId, 创建一个对话
if (!chatId) {
const { _id } = await Chat.create({
userId,
modelId,
content,
title: content[0].value.slice(0, 20)
});
return jsonRes(res, {
data: _id
});
} else {
// 已经有记录,追加入库
await Chat.findByIdAndUpdate(chatId, {
$push: {
content: {
$each: content
}
},
updateTime: new Date()
});
}
jsonRes(res);
} catch (err) {
jsonRes(res, {

View File

@@ -1,13 +1,16 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authChat } from '@/service/utils/chat';
import { httpsAgent, systemPromptFilter } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { authChat } from '@/service/utils/auth';
import { axiosConfig, systemPromptFilter, openaiChatFilter } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import type { ModelSchema } from '@/types/mongoSchema';
import { PassThrough } from 'stream';
import { modelList, ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
import {
modelList,
ModelVectorSearchModeMap,
ModelVectorSearchModeEnum,
ModelDataStatusEnum
} from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
import dayjs from 'dayjs';
@@ -30,29 +33,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
try {
const { chatId, prompt } = req.body as {
const { modelId, chatId, prompt } = req.body as {
modelId: string;
chatId: '' | string;
prompt: ChatItemType;
chatId: string;
};
const { authorization } = req.headers;
if (!chatId || !prompt) {
if (!modelId || !prompt) {
throw new Error('缺少参数');
}
await connectToDatabase();
let startTime = Date.now();
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
const { model, content, userApiKey, systemKey, userId } = await authChat({
modelId,
chatId,
authorization
});
const model: ModelSchema = chat.modelId;
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
// 读取对话内容
const prompts = [...chat.content, prompt];
const prompts = [...content, prompt];
// 获取提示词的向量
const { vector: promptVector, chatAPI } = await openaiCreateEmbedding({
@@ -66,9 +73,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const similarity = ModelVectorSearchModeMap[model.search.mode]?.similarity || 0.22;
const vectorSearch = await PgClient.select<{ id: string; q: string; a: string }>('modelData', {
fields: ['id', 'q', 'a'],
where: [['model_id', model._id], 'AND', `vector <=> '[${promptVector}]' < ${similarity}`],
where: [
['status', ModelDataStatusEnum.ready],
'AND',
['model_id', model._id],
'AND',
`vector <=> '[${promptVector}]' < ${similarity}`
],
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
limit: 30
limit: 20
});
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
@@ -90,32 +103,36 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
value: model.systemPrompt
});
} else {
// 有匹配情况下,添加知识库内容。
// 系统提示词过滤,最多 2000 tokens
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2000);
// 有匹配情况下,system 添加知识库内容。
// 系统提示词过滤,最多 2500 tokens
const systemPrompt = systemPromptFilter({
model: model.service.chatModel,
prompts: formatRedisPrompt,
maxTokens: 2500
});
prompts.unshift({
obj: 'SYSTEM',
value: `${model.systemPrompt} 知识库是最新的,下面是知识库内容:当前时间为${dayjs().format(
'YYYY/MM/DD HH:mm:ss'
)}\n${systemPrompt}`
value: `
${model.systemPrompt}
${
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
? `你只能从知识库选择内容回答.不在知识库内容拒绝回复`
: ''
}
知识库内容为: 当前时间为${dayjs().format('YYYY/MM/DD HH:mm:ss')}\n${systemPrompt}'
`
});
}
// 控制在 tokens 数量,防止超出
// const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// 格式化文本内容成 chatgpt 格式
const map = {
Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
};
const formatPrompts: ChatCompletionRequestMessage[] = prompts.map((item: ChatItemType) => ({
role: map[item.obj],
content: item.value
}));
// console.log(formatPrompts);
// console.log(filterPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
@@ -123,17 +140,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: temperature,
// max_tokens: modelConstantsData.maxToken,
messages: formatPrompts,
temperature,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: true
stream: true,
stop: ['.!?。']
},
{
timeout: 40000,
responseType: 'stream',
httpsAgent: httpsAgent(!userApiKey)
...axiosConfig
}
);
@@ -147,14 +164,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
chatResponse
});
const promptsContent = formatPrompts.map((item) => item.content).join('');
// 只有使用平台的 key 才计费
pushChatBill({
isPay: !userApiKey,
modelName: model.service.modelName,
userId,
chatId,
text: promptsContent + responseContent
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});
// jsonRes(res);
} catch (err: any) {

View File

@@ -1,47 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Data, DataItem } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { DataListItem } from '@/types/data';
import type { PagingData } from '@/types';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少登录凭证');
}
await authToken(authorization);
const { dataId } = req.query as { dataId: string };
if (!dataId) {
throw new Error('缺少参数');
}
await connectToDatabase();
await Data.findByIdAndUpdate(dataId, {
isDeleted: true
});
// 改变 dataItem 状态为 0
await DataItem.updateMany(
{
dataId
},
{
status: 0
}
);
jsonRes<PagingData<DataListItem>>(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,71 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Data, DataItem } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { DataListItem } from '@/types/data';
import mongoose from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少登录凭证');
}
const userId = await authToken(authorization);
await connectToDatabase();
const datalist = await Data.aggregate<DataListItem>([
{
$match: {
userId: new mongoose.Types.ObjectId(userId),
isDeleted: false
}
},
{
$sort: { createTime: -1 } // 按照创建时间倒序排列
},
{
$lookup: {
from: 'dataitems',
localField: '_id',
foreignField: 'dataId',
as: 'items'
}
},
{
$addFields: {
totalData: {
$size: '$items' // 统计dataItem的总数
},
trainingData: {
$size: {
$filter: {
input: '$items',
as: 'item',
cond: { $ne: ['$$item.status', 0] } // 统计 status 不为0的数量
}
}
}
}
},
{
$project: {
items: 0 // 不返回 items 字段
}
}
]);
jsonRes(res, {
data: datalist
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,35 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Data } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { DataType } from '@/types/data';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let { name, type } = req.body as { name: string; type: DataType };
if (!name || !type) {
throw new Error('参数错误');
}
await connectToDatabase();
const { authorization } = req.headers;
const userId = await authToken(authorization);
// 生成 data 集合
const data = await Data.create({
userId,
name,
type
});
jsonRes(res, {
data: data._id
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,37 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Data } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { DataListItem } from '@/types/data';
import type { PagingData } from '@/types';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少登录凭证');
}
await authToken(authorization);
const { dataId, name } = req.query as { dataId: string; name: string };
if (!dataId || !name) {
throw new Error('缺少参数');
}
await connectToDatabase();
await Data.findByIdAndUpdate(dataId, {
name
});
jsonRes<PagingData<DataListItem>>(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,69 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, DataItem, Data } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { generateQA } from '@/service/events/generateQA';
import { generateAbstract } from '@/service/events/generateAbstract';
import { encode } from 'gpt-token-utils';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { text, dataId } = req.body as { text: string; dataId: string };
if (!text || !dataId) {
throw new Error('参数错误');
}
await connectToDatabase();
const { authorization } = req.headers;
const userId = await authToken(authorization);
const DataRecord = await Data.findById(dataId);
if (!DataRecord) {
throw new Error('找不到数据集');
}
const replaceText = text.replace(/[\\n]+/g, ' ');
// 文本拆分成 chunk
let chunks = replaceText.match(/[^!?.。]+[!?.。]/g) || [];
const dataItems: any[] = [];
let splitText = '';
chunks.forEach((chunk) => {
splitText += chunk;
const tokens = encode(splitText).length;
if (tokens >= 780) {
dataItems.push({
userId,
dataId,
type: DataRecord.type,
text: splitText,
status: 1
});
splitText = '';
}
});
// 批量插入数据
await DataItem.insertMany(dataItems);
try {
generateQA();
generateAbstract();
} catch (error) {
error;
}
jsonRes(res, {
data: { chunks, replaceText }
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -3,14 +3,14 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { ModelStatusEnum, modelList, ChatModelNameEnum, ChatModelNameMap } from '@/constants/model';
import { ModelStatusEnum, modelList, ModelNameEnum, Model2ChatModelMap } from '@/constants/model';
import { Model } from '@/service/models/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, serviceModelName } = req.body as {
name: string;
serviceModelName: `${ChatModelNameEnum}`;
serviceModelName: `${ModelNameEnum}`;
};
const { authorization } = req.headers;
@@ -47,9 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
userId,
status: ModelStatusEnum.running,
service: {
company: modelItem.serviceCompany,
trainId: '',
chatModel: ChatModelNameMap[modelItem.model], // 聊天时用的模型
chatModel: Model2ChatModelMap[modelItem.model], // 聊天时用的模型
modelName: modelItem.model // 最底层的模型,不会变,用于计费等核心操作
}
});

View File

@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import axios from 'axios';
import { httpsAgent } from '@/service/utils/tools';
import { axiosConfig } from '@/service/utils/tools';
/**
* 读取网站的内容
@@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const data = await axios
.get(url, {
httpsAgent: httpsAgent(false)
httpsAgent: axiosConfig.httpsAgent
})
.then((res) => res.data as string);

View File

@@ -4,6 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
import type { PgModelDataItemType } from '@/types/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -36,9 +37,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const { model } = await authModel({
userId,
modelId,
authOwner: false
});
const where: any = [
...(model.share.isShareDetail ? [] : [['user_id', userId], 'AND']),
['model_id', modelId],
...(searchText ? ['AND', `(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%')`] : [])
];
const searchRes = await PgClient.select<PgModelDataItemType>('modelData', {
fields: ['id', 'q', 'a', 'status'],
where: [['user_id', userId], 'AND', ['model_id', modelId]],
where,
order: [{ field: 'id', mode: 'DESC' }],
limit: pageSize,
offset: pageSize * (pageNum - 1)
@@ -50,7 +63,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
pageSize,
data: searchRes.rows,
total: await PgClient.count('modelData', {
where: [['user_id', userId], 'AND', ['model_id', modelId]]
where
})
}
});

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { generateVector } from '@/service/events/generateVector';
import { ModelDataStatusEnum } from '@/constants/model';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -28,18 +29,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
await authModel({
userId,
modelId
});
if (!model) {
throw new Error('无权操作该模型');
}
// 去重
const searchRes = await Promise.allSettled(
data.map(async ([q, a]) => {
if (!q || !a) {
return Promise.reject('q/a为空');
}
try {
q = q.replace(/\\n/g, '\n');
a = a.replace(/\\n/g, '\n');

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { ModelDataSchema } from '@/types/mongoSchema';
import { generateVector } from '@/service/events/generateVector';
import { connectPg, PgClient } from '@/service/pg';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -26,18 +27,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const userId = await authToken(authorization);
await connectToDatabase();
const pg = await connectPg();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
await authModel({
userId,
modelId
});
if (!model) {
throw new Error('无权操作该模型');
}
// 插入记录
await PgClient.insert('modelData', {
values: data.map((item) => [

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authToken } from '@/service/utils/tools';
import { ModelDataStatusEnum } from '@/constants/redis';
import { ModelDataStatusEnum } from '@/constants/model';
import { generateVector } from '@/service/events/generateVector';
import { PgClient } from '@/service/pg';

View File

@@ -2,14 +2,20 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { generateVector } from '@/service/events/generateVector';
import { generateQA } from '@/service/events/generateQA';
import { encode } from 'gpt-token-utils';
import { PgClient } from '@/service/pg';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { text, modelId, prompt } = req.body as { text: string; modelId: string; prompt: string };
if (!text || !modelId || !prompt) {
const { chunks, modelId, prompt, mode } = req.body as {
modelId: string;
chunks: string[];
prompt: string;
mode: 'qa' | 'subsection';
};
if (!chunks || !modelId || !prompt) {
throw new Error('参数错误');
}
await connectToDatabase();
@@ -28,46 +34,31 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('无权操作该模型');
}
const replaceText = text.replace(/\\n/g, '\n');
if (mode === 'qa') {
// 批量QA拆分插入数据
await SplitData.create({
userId,
modelId,
textList: chunks,
prompt
});
// 文本拆分成 chunk
const chunks = replaceText.split('\n').filter((item) => item.trim());
generateQA();
} else if (mode === 'subsection') {
// 插入记录
await PgClient.insert('modelData', {
values: chunks.map((item) => [
{ key: 'user_id', value: userId },
{ key: 'model_id', value: modelId },
{ key: 'q', value: item },
{ key: 'a', value: '' },
{ key: 'status', value: 'waiting' }
])
});
const textList: string[] = [];
let splitText = '';
/* 取 3k ~ 4K tokens 内容 */
chunks.forEach((chunk) => {
const tokens = encode(splitText + chunk).length;
if (tokens >= 4000) {
// 超过 4000不要这块内容
textList.push(splitText);
splitText = chunk;
} else if (tokens >= 3000) {
// 超过 3000取内容
textList.push(splitText + chunk);
splitText = '';
} else {
//没超过 3000继续添加
splitText += chunk;
}
});
if (splitText) {
textList.push(splitText);
generateVector();
}
// 批量插入数据
await SplitData.create({
userId,
modelId,
rawText: text,
textList,
prompt
});
generateQA();
jsonRes(res);
} catch (err) {
jsonRes(res, {

View File

@@ -1,12 +1,9 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Chat, Model, Training, connectToDatabase } from '@/service/mongo';
import { Chat, Model, connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { getUserApiOpenai } from '@/service/utils/openai';
import { TrainingStatusEnum } from '@/constants/model';
import { TrainingItemType } from '@/types/training';
import { httpsAgent } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -25,18 +22,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
await authModel({
modelId,
userId
});
if (!model) {
throw new Error('无权操作该模型');
}
await connectToDatabase();
// 删除 pg 中所有该模型的数据
await PgClient.delete('modelData', {
where: [['user_id', userId], 'AND', ['model_id', modelId]]
@@ -47,31 +40,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
modelId
});
// 查看是否正在训练
const training: TrainingItemType | null = await Training.findOne({
modelId,
status: TrainingStatusEnum.pending
});
// 如果正在训练需要删除openai上的相关信息
if (training) {
const { openai } = await getUserApiOpenai(userId);
// 获取训练记录
const tuneRecord = await openai.retrieveFineTune(training.tuneId, {
httpsAgent: httpsAgent(false)
});
// 删除训练文件
openai.deleteFile(tuneRecord.data.training_files[0].id, { httpsAgent: httpsAgent(false) });
// 取消训练
openai.cancelFineTune(training.tuneId, { httpsAgent: httpsAgent(false) });
}
// 删除对应训练记录
await Training.deleteMany({
modelId
});
// 删除模型
await Model.deleteOne({
_id: modelId,

View File

@@ -2,8 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -14,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('无权操作');
}
const { modelId } = req.query;
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('参数错误');
@@ -25,16 +24,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 根据 userId 获取模型信息
const model = await Model.findOne<ModelSchema>({
const { model } = await authModel({
modelId,
userId,
_id: modelId
authOwner: false
});
if (!model) {
throw new Error('模型不存在');
}
jsonRes(res, {
data: model
});

View File

@@ -4,7 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
/* 获取我的模型 */
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { authorization } = req.headers;

View File

@@ -0,0 +1,44 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
/* 模型收藏切换 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(req.headers.authorization);
await connectToDatabase();
const collectionRecord = await Collection.findOne({
userId,
modelId
});
if (collectionRecord) {
await Collection.findByIdAndRemove(collectionRecord._id);
} else {
await Collection.create({
userId,
modelId
});
}
await Model.findByIdAndUpdate(modelId, {
'share.collection': await Collection.countDocuments({ modelId })
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,37 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { ShareModelItem } from '@/types/model';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
// 凭证校验
const userId = await authToken(req.headers.authorization);
await connectToDatabase();
// get my collections
const collections = await Collection.find({
userId
}).populate('modelId', '_id avatar name userId share');
jsonRes<ShareModelItem[]>(res, {
data: collections
.map((item: any) => ({
_id: item.modelId?._id,
avatar: item.modelId?.avatar || '/icon/logo.png',
name: item.modelId?.name || '',
userId: item.modelId?.userId || '',
share: item.modelId?.share || {},
isCollection: true
}))
.filter((item) => item.share.isShare)
});
} catch (err) {
jsonRes(res, {
data: []
});
}
}

View File

@@ -0,0 +1,60 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { PagingData } from '@/types';
import type { ShareModelItem } from '@/types/model';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
searchText = '',
pageNum = 1,
pageSize = 20
} = req.body as { searchText: string; pageNum: number; pageSize: number };
await connectToDatabase();
const regex = new RegExp(searchText, 'i');
const where = {
$and: [
{ 'share.isShare': true },
{ $or: [{ name: { $regex: regex } }, { 'share.intro': { $regex: regex } }] }
]
};
// 获取被分享的模型
const [models, total] = await Promise.all([
Model.find(where, '_id avatar name userId share')
.sort({
'share.collection': -1
})
.limit(pageSize)
.skip((pageNum - 1) * pageSize),
Model.countDocuments(where)
]);
jsonRes<PagingData<ShareModelItem>>(res, {
data: {
pageNum,
pageSize,
data: models.map((item) => ({
_id: item._id,
avatar: item.avatar || '/icon/logo.png',
name: item.name,
userId: item.userId,
share: item.share,
isCollection: false
})),
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,52 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Training } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式
export const config = {
api: {
bodyParser: false
}
};
/* 获取模型训练记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
const { modelId } = req.query;
if (!modelId) {
throw new Error('参数错误');
}
await authToken(authorization);
await connectToDatabase();
/* 获取 modelId 下的 training 记录 */
const records = await Training.find({
modelId
});
jsonRes(res, {
data: records
});
} catch (err: any) {
/* 清除上传的文件,关闭训练记录 */
// @ts-ignore
if (openai) {
// @ts-ignore
uploadFileId && openai.deleteFile(uploadFileId);
// @ts-ignore
trainId && openai.cancelFineTune(trainId);
}
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,106 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { getUserApiOpenai } from '@/service/utils/openai';
import type { ModelSchema } from '@/types/mongoSchema';
import { TrainingItemType } from '@/types/training';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
import { httpsAgent } from '@/service/utils/tools';
/* 更新训练状态 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('参数错误');
}
const userId = await authToken(authorization);
await connectToDatabase();
// 获取模型
const model = await Model.findById<ModelSchema>(modelId);
if (!model || model.status !== 'training') {
throw new Error('模型不在训练中');
}
// 查询正在训练中的训练记录
const training: TrainingItemType | null = await Training.findOne({
modelId,
status: 'pending'
});
if (!training) {
throw new Error('找不到训练记录');
}
// 用户的 openai 实例
const { openai } = await getUserApiOpenai(userId);
// 获取 openai 的训练情况
const { data } = await openai.retrieveFineTune(training.tuneId, {
httpsAgent: httpsAgent(false)
});
// console.log(data);
if (data.status === OpenAiTuneStatusEnum.succeeded) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, { httpsAgent: httpsAgent(false) });
// 更新模型状态和模型内容
await Model.findByIdAndUpdate(modelId, {
status: ModelStatusEnum.running,
updateTime: new Date(),
service: {
...model.service,
trainId: data.fine_tuned_model, // 训练完后,再次训练和对话使用的 model 是一样的
chatModel: data.fine_tuned_model
}
});
// 更新训练数据
await Training.findByIdAndUpdate(training._id, {
status: TrainingStatusEnum.succeed
});
return jsonRes(res, {
data: '模型微调完成'
});
}
/* 取消微调 */
if (data.status === OpenAiTuneStatusEnum.cancelled) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, { httpsAgent: httpsAgent(false) });
// 更新模型
await Model.findByIdAndUpdate(modelId, {
status: ModelStatusEnum.running,
updateTime: new Date()
});
// 更新训练数据
await Training.findByIdAndUpdate(training._id, {
status: TrainingStatusEnum.canceled
});
return jsonRes(res, {
data: '模型微调已取消'
});
}
jsonRes(res, {
data: '模型还在训练中'
});
} catch (err: any) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,130 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo';
import formidable from 'formidable';
import { authToken } from '@/service/utils/tools';
import { getUserApiOpenai } from '@/service/utils/openai';
import { join } from 'path';
import fs from 'fs';
import type { ModelSchema } from '@/types/mongoSchema';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { httpsAgent } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式
export const config = {
api: {
bodyParser: false
}
};
/* 上传文件,开始微调 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
let openai: OpenAIApi, trainId: string, uploadFileId: string;
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
const { modelId } = req.query;
if (!modelId) {
throw new Error('参数错误');
}
const userId = await authToken(authorization);
await connectToDatabase();
// 获取模型的状态
const model = await Model.findById<ModelSchema>(modelId);
if (!model || model.status !== 'running') {
throw new Error('模型正忙');
}
// const trainingType = model.service.modelType
const trainingType = model.service.trainId; // 目前都默认是 openai text-davinci-03
// 获取用户的 API Key 实例化后的对象
const user = await getUserApiOpenai(userId);
openai = user.openai;
// 接收文件并保存
const form = formidable({
uploadDir: join(process.cwd(), 'public/trainData'),
keepExtensions: true
});
const { files } = await new Promise<{
fields: formidable.Fields;
files: formidable.Files;
}>((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) return reject(err);
resolve({ fields, files });
});
});
const file = files.file;
// 上传文件到 openai
// @ts-ignore
const uploadRes = await openai.createFile(
// @ts-ignore
fs.createReadStream(file.filepath),
'fine-tune',
{ httpsAgent: httpsAgent(false) }
);
uploadFileId = uploadRes.data.id; // 记录上传文件的 ID
// 开始训练
const trainRes = await openai.createFineTune(
{
training_file: uploadFileId,
model: trainingType,
suffix: model.name,
n_epochs: 4
},
{ httpsAgent: httpsAgent(false) }
);
trainId = trainRes.data.id; // 记录训练 ID
// 创建训练记录
await Training.create({
serviceName: 'openai',
tuneId: trainId,
status: TrainingStatusEnum.pending,
modelId
});
// 修改模型状态
await Model.findByIdAndUpdate(modelId, {
$inc: {
trainingTimes: +1
},
updateTime: new Date(),
status: ModelStatusEnum.training
});
jsonRes(res, {
data: 'start training'
});
} catch (err: any) {
/* 清除上传的文件,关闭训练记录 */
// @ts-ignore
if (openai) {
// @ts-ignore
uploadFileId && openai.deleteFile(uploadFileId, { httpsAgent: httpsAgent(false) });
// @ts-ignore
trainId && openai.cancelFineTune(trainId, { httpsAgent: httpsAgent(false) });
}
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,11 +4,12 @@ import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import type { ModelUpdateParams } from '@/types/model';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, search, service, security, systemPrompt, intro, temperature } =
const { name, avatar, search, share, service, security, systemPrompt, temperature } =
req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string };
const { authorization } = req.headers;
@@ -26,6 +27,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
await authModel({
modelId,
userId
});
// 更新模型
await Model.updateOne(
{
@@ -34,11 +40,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
},
{
name,
avatar,
systemPrompt,
intro,
temperature,
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail,
'share.intro': share.intro,
search,
// service,
security
}
);

View File

@@ -1,8 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase, Model } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat';
import { httpsAgent, openaiChatFilter, authOpenApiKey } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { getOpenAIApi } from '@/service/utils/auth';
import { axiosConfig, openaiChatFilter, authOpenApiKey } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
@@ -74,17 +73,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
// 格式化文本内容成 chatgpt 格式
const map = {
Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
};
const formatPrompts: ChatCompletionRequestMessage[] = prompts.map((item: ChatItemType) => ({
role: map[item.obj],
content: item.value
}));
// console.log(formatPrompts);
// 控制在 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// console.log(filterPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
@@ -94,9 +90,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: temperature,
// max_tokens: modelConstantsData.maxToken,
messages: formatPrompts,
temperature,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: isStream,
@@ -105,16 +100,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{
timeout: 40000,
responseType: isStream ? 'stream' : 'json',
httpsAgent: httpsAgent(true)
...axiosConfig
}
);
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
step = 1;
let responseContent = '';
if (isStream) {
step = 1;
const streamResponse = await gpt35StreamResponse({
res,
stream,
@@ -128,14 +123,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
const promptsContent = formatPrompts.map((item) => item.content).join('');
// 只有使用平台的 key 才计费
pushChatBill({
isPay: true,
modelName: model.service.modelName,
userId,
text: promptsContent + responseContent
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});
} catch (err: any) {
if (step === 1) {

View File

@@ -1,17 +1,16 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase, Model } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat';
import { getOpenAIApi } from '@/service/utils/auth';
import { authOpenApiKey } from '@/service/utils/tools';
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { axiosConfig, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
import {
ChatModelNameEnum,
ModelNameEnum,
modelList,
ChatModelNameMap,
ModelVectorSearchModeMap
ModelVectorSearchModeMap,
ChatModelEnum
} from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
@@ -60,9 +59,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('找不到模型');
}
const modelConstantsData = modelList.find(
(item) => item.model === ChatModelNameEnum.VECTOR_GPT
);
const modelConstantsData = modelList.find((item) => item.model === ModelNameEnum.VECTOR_GPT);
if (!modelConstantsData) {
throw new Error('模型已下架');
}
@@ -74,7 +71,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 请求一次 chatgpt 拆解需求
const promptResponse = await chatAPI.createChatCompletion(
{
model: ChatModelNameMap[ChatModelNameEnum.GPT35],
model: ChatModelEnum.GPT35,
temperature: 0,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
@@ -122,8 +119,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
]
},
{
timeout: 120000,
httpsAgent: httpsAgent(true)
timeout: 180000,
...axiosConfig
}
);
@@ -163,30 +160,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
// textArr 筛选,最多 2500 tokens
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2500);
// system 筛选,最多 2500 tokens
const systemPrompt = systemPromptFilter({
model: model.service.chatModel,
prompts: formatRedisPrompt,
maxTokens: 2500
});
prompts.unshift({
obj: 'SYSTEM',
value: `${model.systemPrompt} 知识库是最新的,下面是知识库内容:${systemPrompt}`
});
// 控制 tokens 数量,防止超出
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
// 控制上下文 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// 格式化文本内容成 chatgpt 格式
const map = {
Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
};
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
(item: ChatItemType) => ({
role: map[item.obj],
content: item.value
})
);
// console.log(formatPrompts);
// console.log(filterPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
@@ -195,24 +188,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{
model: model.service.chatModel,
temperature,
messages: formatPrompts,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: isStream
},
{
timeout: 120000,
timeout: 180000,
responseType: isStream ? 'stream' : 'json',
httpsAgent: httpsAgent(true)
...axiosConfig
}
);
console.log('code response. time:', `${(Date.now() - startTime) / 1000}s`);
step = 1;
let responseContent = '';
if (isStream) {
step = 1;
const streamResponse = await gpt35StreamResponse({
res,
stream,
@@ -228,13 +221,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
console.log('laf gpt done. time:', `${(Date.now() - startTime) / 1000}s`);
const promptsContent = formatPrompts.map((item) => item.content).join('');
pushChatBill({
isPay: true,
modelName: model.service.modelName,
userId,
text: promptsContent + responseContent
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});
} catch (err: any) {
if (step === 1) {

View File

@@ -1,11 +1,20 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase, Model } from '@/service/mongo';
import { httpsAgent, systemPromptFilter, authOpenApiKey } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import {
axiosConfig,
systemPromptFilter,
authOpenApiKey,
openaiChatFilter
} from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
import { modelList, ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
import {
modelList,
ModelVectorSearchModeMap,
ModelVectorSearchModeEnum,
ModelDataStatusEnum
} from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
import dayjs from 'dayjs';
@@ -80,9 +89,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const similarity = ModelVectorSearchModeMap[model.search.mode]?.similarity || 0.22;
const vectorSearch = await PgClient.select<{ id: string; q: string; a: string }>('modelData', {
fields: ['id', 'q', 'a'],
where: [['model_id', model._id], 'AND', `vector <=> '[${promptVector}]' < ${similarity}`],
where: [
['status', ModelDataStatusEnum.ready],
'AND',
['model_id', model._id],
'AND',
`vector <=> '[${promptVector}]' < ${similarity}`
],
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
limit: 30
limit: 20
});
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
@@ -110,28 +125,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
} else {
// 有匹配或者低匹配度模式情况下,添加知识库内容。
// 系统提示词过滤,最多 2000 tokens
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2000);
// 系统提示词过滤,最多 2500 tokens
const systemPrompt = systemPromptFilter({
model: model.service.chatModel,
prompts: formatRedisPrompt,
maxTokens: 2500
});
prompts.unshift({
obj: 'SYSTEM',
value: `${model.systemPrompt} 知识库是最新的,下面是知识库内容:当前时间为${dayjs().format(
'YYYY/MM/DD HH:mm:ss'
)}\n${systemPrompt}`
value: `
${model.systemPrompt}
${
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
? `你只能从知识库选择内容回答.不在知识库内容拒绝回复`
: ''
}
知识库内容为: 当前时间为${dayjs().format('YYYY/MM/DD HH:mm:ss')}\n${systemPrompt}'
`
});
}
// 格式化文本内容成 chatgpt 格式
const map = {
Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
};
const formatPrompts: ChatCompletionRequestMessage[] = prompts.map((item: ChatItemType) => ({
role: map[item.obj],
content: item.value
}));
// console.log(formatPrompts);
// 控制在 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// console.log(filterPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
@@ -139,25 +161,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: temperature,
messages: formatPrompts,
temperature,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: isStream
stream: isStream,
stop: ['.!?。']
},
{
timeout: 120000,
timeout: 180000,
responseType: isStream ? 'stream' : 'json',
httpsAgent: httpsAgent(true)
...axiosConfig
}
);
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
step = 1;
let responseContent = '';
if (isStream) {
step = 1;
const streamResponse = await gpt35StreamResponse({
res,
stream,
@@ -171,12 +194,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
const promptsContent = formatPrompts.map((item) => item.content).join('');
pushChatBill({
isPay: true,
modelName: model.service.modelName,
userId,
text: promptsContent + responseContent
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});
// jsonRes(res);
} catch (err: any) {

View File

@@ -2,9 +2,11 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, User, Pay } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { PaySchema } from '@/types/mongoSchema';
import { PaySchema, UserModelSchema } from '@/types/mongoSchema';
import dayjs from 'dayjs';
import { getPayResult } from '@/service/utils/wxpay';
import { pushPromotionRecord } from '@/service/utils/promotion';
import { PRICE_SCALE } from '@/constants/common';
/* 校验支付结果 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -26,6 +28,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('订单已结算');
}
// 获取当前用户
const user = await User.findById(userId);
if (!user) {
throw new Error('找不到用户');
}
// 获取邀请者
let inviter: UserModelSchema | null = null;
if (user.inviterId) {
inviter = await User.findById(user.inviterId);
}
const payRes = await getPayResult(payOrder.orderId);
// 校验下是否超过一天
@@ -50,6 +63,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await User.findByIdAndUpdate(userId, {
$inc: { balance: payOrder.price }
});
// 推广佣金发放
if (inviter) {
pushPromotionRecord({
userId: inviter._id,
objUId: userId,
type: 'invite',
// amount 单位为元,需要除以缩放比例,最后乘比例
amount: (payOrder.price / PRICE_SCALE) * inviter.promotion.rate * 0.01
});
}
jsonRes(res, {
data: '支付成功'
});

View File

@@ -15,7 +15,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
const records = await Pay.find({
userId
userId,
status: { $ne: 'CLOSED' }
}).sort({ createTime: -1 });
jsonRes(res, {

View File

@@ -0,0 +1,70 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, User, promotionRecord } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import mongoose from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少登录凭证');
}
const userId = await authToken(authorization);
await connectToDatabase();
const invitedAmount = await User.countDocuments({
inviterId: userId
});
// 计算累计合
const countHistory: { totalAmount: number }[] = await promotionRecord.aggregate([
{ $match: { userId: new mongoose.Types.ObjectId(userId), amount: { $gt: 0 } } },
{
$group: {
_id: null, // 分组条件,这里使用 null 表示不分组
totalAmount: { $sum: '$amount' } // 计算 amount 字段的总和
}
},
{
$project: {
_id: false, // 排除 _id 字段
totalAmount: true // 只返回 totalAmount 字段
}
}
]);
// 计算剩余金额
const countResidue: { totalAmount: number }[] = await promotionRecord.aggregate([
{ $match: { userId: new mongoose.Types.ObjectId(userId) } },
{
$group: {
_id: null, // 分组条件,这里使用 null 表示不分组
totalAmount: { $sum: '$amount' } // 计算 amount 字段的总和
}
},
{
$project: {
_id: false, // 排除 _id 字段
totalAmount: true // 只返回 totalAmount 字段
}
}
]);
jsonRes(res, {
data: {
invitedAmount,
historyAmount: countHistory[0]?.totalAmount || 0,
residueAmount: countResidue[0]?.totalAmount || 0
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,31 +1,31 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, DataItem } from '@/service/mongo';
import { connectToDatabase, promotionRecord } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let {
dataId,
pageNum = 1,
pageSize = 10
} = req.query as { dataId: string; pageNum: string; pageSize: string };
const { authorization } = req.headers;
let { pageNum = 1, pageSize = 10 } = req.query as { pageNum: string; pageSize: string };
pageNum = +pageNum;
pageSize = +pageSize;
if (!dataId) {
throw new Error('参数错误');
if (!authorization) {
throw new Error('缺少登录凭证');
}
const userId = await authToken(authorization);
await connectToDatabase();
const { authorization } = req.headers;
await authToken(authorization);
const dataItems = await DataItem.find({
dataId
})
.sort({ _id: -1 }) // 按照创建时间倒序排列
const data = await promotionRecord
.find(
{
userId
},
'_id createTime type amount'
)
.sort({ _id: -1 })
.skip((pageNum - 1) * pageSize)
.limit(pageSize);
@@ -33,9 +33,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
data: {
pageNum,
pageSize,
data: dataItems,
total: await DataItem.countDocuments({
dataId
data,
total: await promotionRecord.countDocuments({
userId
})
}
});

View File

@@ -5,6 +5,8 @@ import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
import { sendPhoneCode, sendEmailCode } from '@/service/utils/sendNote';
import { UserAuthTypeEnum } from '@/constants/common';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('123456789', 6);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -16,10 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
let code = '';
for (let i = 0; i < 6; i++) {
code += Math.floor(Math.random() * 10);
}
const code = nanoid();
// 判断 1 分钟内是否有重复数据
const authCode = await AuthCode.findOne({

View File

@@ -3,12 +3,7 @@ import { Card, Box } from '@chakra-ui/react';
import { useMarkdown } from '@/hooks/useMarkdown';
import Markdown from '@/components/Markdown';
const Empty = ({ intro }: { intro: string }) => {
const Header = ({ children }: { children: string }) => (
<Box fontSize={'lg'} fontWeight={'bold'} textAlign={'center'} pb={2}>
{children}
</Box>
);
const Empty = ({ modelName, intro }: { modelName: string; intro: string }) => {
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
@@ -24,7 +19,9 @@ const Empty = ({ intro }: { intro: string }) => {
>
{!!intro && (
<Card p={4} mb={10}>
<Header></Header>
<Box fontSize={'xl'} fontWeight={'600'} textAlign={'center'} pb={2}>
{modelName}
</Box>
<Box whiteSpace={'pre-line'}>{intro}</Box>
</Card>
)}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useRef, useEffect, useMemo } from 'react';
import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
import {
Box,
@@ -11,28 +11,19 @@ import {
Flex,
Divider,
IconButton,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure,
useColorMode,
useColorModeValue
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useChatStore } from '@/store/chat';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { getToken } from '@/utils/user';
import MyIcon from '@/components/Icon';
import { useCopyData } from '@/utils/tools';
import Markdown from '@/components/Markdown';
import { getChatSiteId } from '@/api/chat';
import WxConcat from '@/components/WxConcat';
import { useMarkdown } from '@/hooks/useMarkdown';
import { getChatHistory, delChatHistoryById } from '@/api/chat';
import { getCollectionModels } from '@/api/model';
import { modelList } from '@/constants/model';
const SlideBar = ({
chatId,
@@ -42,32 +33,62 @@ const SlideBar = ({
}: {
chatId: string;
modelId: string;
resetChat: () => void;
resetChat: (modelId?: string, chatId?: string) => void;
onClose: () => void;
}) => {
const router = useRouter();
const { colorMode, toggleColorMode } = useColorMode();
const { copyData } = useCopyData();
const { myModels, getMyModels } = useUserStore();
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
const [hasReady, setHasReady] = useState(false);
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
const { data: shareHint } = useMarkdown({ url: '/chatProblem.md' });
const preChatId = useRef('chatId'); // 用于校验上一次chatId的情况,判断是否需要刷新历史记录
const { isSuccess } = useQuery(['init'], getMyModels, {
const { isSuccess } = useQuery(['getMyModels'], getMyModels, {
cacheTime: 5 * 60 * 1000
});
const { data: collectionModels = [] } = useQuery([getCollectionModels], getCollectionModels);
const models = useMemo(() => {
const myModelList = myModels.map((item) => ({
id: item._id,
name: item.name,
icon: modelList.find((model) => model.model === item?.service?.modelName)?.icon || 'model'
}));
const collectionList = collectionModels
.map((item) => ({
id: item._id,
name: item.name,
icon: 'collectionSolid' as any
}))
.filter((model) => !myModelList.find((item) => item.id === model.id));
return myModelList.concat(collectionList);
}, [collectionModels, myModels]);
const { data: chatHistory = [], mutate: loadChatHistory } = useMutation({
mutationFn: getChatHistory
});
// update history
useEffect(() => {
setHasReady(true);
}, []);
if (chatId && preChatId.current === '') {
loadChatHistory();
}
preChatId.current = chatId;
}, [chatId, loadChatHistory]);
// init history
useEffect(() => {
setTimeout(() => {
loadChatHistory();
}, 1000);
}, [loadChatHistory]);
const RenderHistory = () => (
<>
{chatHistory.map((item) => (
<Flex
key={item.chatId}
key={item._id}
alignItems={'center'}
p={3}
borderRadius={'md'}
@@ -78,15 +99,16 @@ const SlideBar = ({
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item.chatId === chatId
{...(item._id === chatId
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={() => {
if (item.chatId === chatId) return;
router.replace(`/chat?chatId=${item.chatId}`);
if (item._id === chatId) return;
preChatId.current = 'chatId';
resetChat(item.modelId, item._id);
onClose();
}}
>
@@ -100,12 +122,14 @@ const SlideBar = ({
variant={'unstyled'}
aria-label={'edit'}
size={'xs'}
onClick={(e) => {
removeChatHistoryByWindowId(item.chatId);
if (item.chatId === chatId) {
onClick={async (e) => {
e.stopPropagation();
await delChatHistoryById(item._id);
loadChatHistory();
if (item._id === chatId) {
resetChat();
}
e.stopPropagation();
}}
/>
</Box>
@@ -155,58 +179,51 @@ const SlideBar = ({
mb={4}
mx={'auto'}
leftIcon={<AddIcon />}
onClick={resetChat}
onClick={() => resetChat()}
>
</Button>
)}
{/* 我的模型 & 历史记录 折叠框*/}
<Box flex={'1 0 0'} px={3} h={0} overflowY={'auto'}>
<Accordion defaultIndex={[0]} allowMultiple>
{isSuccess && (
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} px={0}>
{myModels.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={2}
cursor={'pointer'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.1)'
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item._id === modelId
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={async () => {
if (item._id === modelId) return;
router.replace(`/chat?chatId=${await getChatSiteId(item._id)}`);
onClose();
}}
>
<MyIcon name="model" mr={2} fill={'white'} w={'16px'} h={'16px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>
</Flex>
))}
</AccordionPanel>
</AccordionItem>
)}
{isSuccess && (
<>
<Box>
{models.map((item) => (
<Flex
key={item.id}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={2}
cursor={'pointer'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.1)'
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item.id === modelId
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={async () => {
if (item.id === modelId) return;
resetChat(item.id);
onClose();
}}
>
<MyIcon name={item.icon} mr={2} color={'white'} w={'16px'} h={'16px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>
</Flex>
))}
</Box>
</>
)}
<Accordion allowToggle>
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
@@ -215,7 +232,7 @@ const SlideBar = ({
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={0} px={0}>
{hasReady && <RenderHistory />}
<RenderHistory />
</AccordionPanel>
</AccordionItem>
</Accordion>
@@ -230,12 +247,6 @@ const SlideBar = ({
</>
</RenderButton>
{/* <RenderButton onClick={onOpenShare}>
<>
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
分享
</>
</RenderButton> */}
<RenderButton onClick={() => router.push('/number/setting')}>
<>
<MyIcon name="pay" fill={'white'} w={'16px'} h={'16px'} mr={4} />
@@ -260,49 +271,6 @@ const SlideBar = ({
/>
</Flex>
{/* 分享提示modal */}
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
<ModalOverlay />
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<Markdown source={shareHint} />
</ModalBody>
<ModalFooter>
<Button colorScheme="gray" variant={'outline'} mr={3} onClick={onCloseShare}>
</Button>
{getToken() && (
<Button
variant="outline"
mr={3}
onClick={async () => {
copyData(
`${location.origin}/chat?chatId=${await getChatSiteId(modelId)}`,
'已复制分享链接'
);
onCloseShare();
onClose();
}}
>
</Button>
)}
<Button
onClick={() => {
copyData(`${location.origin}/chat?chatId=${chatId}`, '已复制分享链接');
onCloseShare();
onClose();
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* wx 联系 */}
{isOpenWx && <WxConcat onClose={onCloseWx} />}
</Flex>

View File

@@ -0,0 +1,11 @@
.stopIcon {
animation: zoomStopIcon 0.4s infinite alternate;
}
@keyframes zoomStopIcon {
0% {
transform: scale(0.8);
}
100% {
transform: scale(1.2);
}
}

View File

@@ -1,9 +1,9 @@
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
import { useRouter } from 'next/router';
import Image from 'next/image';
import { getInitChatSiteInfo, getChatSiteId, delChatRecordByIndex, postSaveChat } from '@/api/chat';
import { getInitChatSiteInfo, delChatRecordByIndex, postSaveChat } from '@/api/chat';
import type { InitChatResponse } from '@/api/response/chat';
import { ChatSiteItemType } from '@/types/chat';
import type { ChatItemType } from '@/types/chat';
import {
Textarea,
Box,
@@ -21,44 +21,55 @@ import {
import { useToast } from '@/hooks/useToast';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import { ChatModelNameEnum } from '@/constants/model';
import { ModelNameEnum } from '@/constants/model';
import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global';
import { useChatStore } from '@/store/chat';
import { useCopyData } from '@/utils/tools';
import { streamFetch } from '@/api/fetch';
import Icon from '@/components/Icon';
import { modelList } from '@/constants/model';
import MyIcon from '@/components/Icon';
import { throttle } from 'lodash';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 5);
const SlideBar = dynamic(() => import('./components/SlideBar'));
const Empty = dynamic(() => import('./components/Empty'));
const Markdown = dynamic(() => import('@/components/Markdown'));
import styles from './index.module.scss';
const textareaMinH = '22px';
export type ChatSiteItemType = {
id: string;
status: 'loading' | 'finish';
} & ChatItemType;
interface ChatType extends InitChatResponse {
history: ChatSiteItemType[];
}
const Chat = ({ chatId }: { chatId: string }) => {
const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const router = useRouter();
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
const { toast } = useToast();
const router = useRouter();
// 中断请求
const controller = useRef(new AbortController());
const isResetPage = useRef(false);
const [chatData, setChatData] = useState<ChatType>({
chatId: '',
modelId: '',
chatId,
modelId,
name: '',
avatar: '',
avatar: '/icon/logo.png',
intro: '',
chatModel: '',
modelName: '',
history: []
}); // 聊天框整体数据
const [inputVal, setInputVal] = useState(''); // 输入的内容
const isChatting = useMemo(
@@ -67,22 +78,34 @@ const Chat = ({ chatId }: { chatId: string }) => {
);
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const { toast } = useToast();
const { copyData } = useCopyData();
const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore();
const { pushChatHistory } = useChatStore();
// 滚动到底部
const scrollToBottom = useCallback(() => {
setTimeout(() => {
ChatBox.current &&
ChatBox.current.scrollTo({
top: ChatBox.current.scrollHeight,
behavior: 'smooth'
});
}, 100);
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
if (!ChatBox.current) return;
ChatBox.current.scrollTo({
top: ChatBox.current.scrollHeight,
behavior
});
}, []);
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
// eslint-disable-next-line react-hooks/exhaustive-deps
const generatingMessage = useCallback(
throttle(() => {
if (!ChatBox.current) return;
const isBottom =
ChatBox.current.scrollTop + ChatBox.current.clientHeight + 150 >=
ChatBox.current.scrollHeight;
isBottom && scrollToBottom('auto');
}, 100),
[]
);
// 重置输入内容
const resetInputVal = useCallback((val: string) => {
setInputVal(val);
@@ -95,40 +118,105 @@ const Chat = ({ chatId }: { chatId: string }) => {
}, 100);
}, []);
// 重载对话
const resetChat = useCallback(async () => {
if (!chatData) return;
try {
router.replace(`/chat?chatId=${await getChatSiteId(chatData.modelId)}`);
} catch (error: any) {
toast({
title: error?.message || '生成新对话失败',
status: 'warning'
});
}
onCloseSlider();
}, [chatData, onCloseSlider, router, toast]);
// 获取对话信息
const loadChatInfo = useCallback(
async ({
modelId,
chatId,
isLoading = false,
isScroll = false
}: {
modelId: string;
chatId: string;
isLoading?: boolean;
isScroll?: boolean;
}) => {
isLoading && setLoading(true);
try {
const res = await getInitChatSiteInfo(modelId, chatId);
setChatData({
...res,
history: res.history.map((item: any, i) => ({
obj: item.obj,
value: item.value,
id: item.id || `${nanoid()}-${i}`,
status: 'finish'
}))
});
if (isScroll && res.history.length > 0) {
setTimeout(() => {
scrollToBottom('auto');
}, 1200);
}
} catch (e: any) {
toast({
title: e?.message || '获取对话信息异常,请检查地址',
status: 'error',
isClosable: true,
duration: 5000
});
router.back();
}
setLoading(false);
return null;
},
[router, scrollToBottom, setLoading, toast]
);
// 重载新的对话
const resetChat = useCallback(
async (modelId = chatData.modelId, chatId = '') => {
// 强制中断流
isResetPage.current = true;
controller.current?.abort();
try {
router.replace(`/chat?modelId=${modelId}&chatId=${chatId}`);
loadChatInfo({
modelId,
chatId,
isLoading: true,
isScroll: true
});
} catch (error: any) {
toast({
title: error?.message || '生成新对话失败',
status: 'warning'
});
}
onCloseSlider();
},
[chatData.modelId, loadChatInfo, onCloseSlider, router, toast]
);
// gpt 对话
const gptChatPrompt = useCallback(
async (prompts: ChatSiteItemType) => {
const urlMap: Record<string, string> = {
[ChatModelNameEnum.GPT35]: '/api/chat/chatGpt',
[ChatModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt'
[ModelNameEnum.GPT35]: '/api/chat/chatGpt',
[ModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt'
};
if (!urlMap[chatData.modelName]) return Promise.reject('找不到模型');
// create abort obj
const abortSignal = new AbortController();
controller.current = abortSignal;
isResetPage.current = false;
const prompt = {
obj: prompts.obj,
value: prompts.value
};
// 流请求,获取数据
const res = await streamFetch({
const responseText = await streamFetch({
url: urlMap[chatData.modelName],
data: {
prompt,
chatId
chatId,
modelId
},
onMessage: (text: string) => {
setChatData((state) => ({
@@ -141,22 +229,33 @@ const Chat = ({ chatId }: { chatId: string }) => {
};
})
}));
generatingMessage();
},
abortSignal: controller.current
abortSignal
});
// 重置了页面,说明退出了当前聊天, 不缓存任何内容
if (isResetPage.current) {
return;
}
let newChatId = '';
// 保存对话信息
try {
await postSaveChat({
newChatId = await postSaveChat({
modelId,
chatId,
prompts: [
prompt,
{
obj: 'AI',
value: res as string
value: responseText
}
]
});
if (newChatId) {
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
}
} catch (err) {
toast({
title: '对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
@@ -166,9 +265,10 @@ const Chat = ({ chatId }: { chatId: string }) => {
});
}
// 设置完成状态
// 设置聊天内容为完成状态
setChatData((state) => ({
...state,
chatId: newChatId || state.chatId, // 如果有 Id说明是新创建的对话
history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return {
@@ -178,7 +278,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
})
}));
},
[chatData.modelName, chatId, toast]
[chatData.modelName, chatId, generatingMessage, modelId, router, toast]
);
/**
@@ -196,7 +296,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
// 去除空行
const val = inputVal.trim().replace(/\n\s*/g, '\n');
if (!chatData?.modelId || !val) {
if (!val) {
toast({
title: '内容为空',
status: 'warning'
@@ -207,11 +307,13 @@ const Chat = ({ chatId }: { chatId: string }) => {
const newChatList: ChatSiteItemType[] = [
...chatData.history,
{
id: nanoid(),
obj: 'Human',
value: val,
status: 'finish'
},
{
id: nanoid(),
obj: 'AI',
value: '',
status: 'loading'
@@ -226,19 +328,12 @@ const Chat = ({ chatId }: { chatId: string }) => {
// 清空输入内容
resetInputVal('');
scrollToBottom();
setTimeout(() => {
scrollToBottom();
}, 100);
try {
await gptChatPrompt(newChatList[newChatList.length - 2]);
// 如果是 Human 第一次发送,插入历史记录
const humanChat = newChatList.filter((item) => item.obj === 'Human');
if (humanChat.length === 1) {
pushChatHistory({
chatId,
title: humanChat[0].value
});
}
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
@@ -254,17 +349,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
history: newChatList.slice(0, newChatList.length - 2)
}));
}
}, [
inputVal,
chatData,
isChatting,
resetInputVal,
scrollToBottom,
toast,
gptChatPrompt,
pushChatHistory,
chatId
]);
}, [isChatting, inputVal, chatData.history, resetInputVal, toast, scrollToBottom, gptChatPrompt]);
// 删除一句话
const delChatRecord = useCallback(
@@ -296,50 +381,23 @@ const Chat = ({ chatId }: { chatId: string }) => {
);
// 初始化聊天框
useQuery(
['init', chatId],
() => {
setLoading(true);
return getInitChatSiteInfo(chatId);
},
{
onSuccess(res) {
setChatData({
...res,
history: res.history.map((item) => ({
...item,
status: 'finish'
}))
});
if (res.history.length > 0) {
setTimeout(() => {
scrollToBottom();
}, 500);
}
},
onError(e: any) {
toast({
title: e?.message || '初始化异常,请检查地址',
status: 'error',
isClosable: true,
duration: 5000
});
router.push('/model/list');
},
onSettled() {
setLoading(false);
}
}
useQuery(['init'], () =>
loadChatInfo({
modelId,
chatId,
isLoading: true,
isScroll: true
})
);
// 更新流中断对象
useEffect(() => {
controller.current = new AbortController();
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
controller.current?.abort();
};
}, [chatId]);
}, []);
return (
<Flex
h={'100%'}
@@ -351,7 +409,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
<SlideBar
resetChat={resetChat}
chatId={chatId}
modelId={chatData.modelId}
modelId={modelId}
onClose={onCloseSlider}
/>
</Box>
@@ -382,7 +440,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
<SlideBar
resetChat={resetChat}
chatId={chatId}
modelId={chatData.modelId}
modelId={modelId}
onClose={onCloseSlider}
/>
</DrawerContent>
@@ -399,7 +457,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
<Box ref={ChatBox} pb={[4, 0]} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
{chatData.history.map((item, index) => (
<Box
key={index}
key={item.id}
py={media(9, 6)}
px={media(4, 2)}
backgroundColor={
@@ -409,11 +467,15 @@ const Chat = ({ chatId }: { chatId: string }) => {
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
<Menu>
<Menu autoSelect={false}>
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
<Image
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
alt="/icon/logo.png"
src={
item.obj === 'Human'
? '/icon/human.png'
: chatData.avatar || '/icon/logo.png'
}
alt="avatar"
width={media(30, 20)}
height={media(30, 20)}
/>
@@ -433,10 +495,35 @@ const Chat = ({ chatId }: { chatId: string }) => {
<Box whiteSpace={'pre-wrap'}>{item.value}</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={'alphaBlack.400'}
onClick={() => onclickCopy(item.value)}
/>
</Box>
<MyIcon
name="delete"
w={'14px'}
cursor={'pointer'}
color={'alphaBlack.400'}
_hover={{
color: 'red.600'
}}
onClick={() => delChatRecord(index)}
/>
</Flex>
)}
</Flex>
</Box>
))}
{chatData.history.length === 0 && <Empty intro={chatData.intro} />}
{chatData.history.length === 0 && (
<Empty modelName={chatData.name} intro={chatData.intro} />
)}
</Box>
{/* 发送区 */}
<Box m={media('20px auto', '0 auto')} w={'100%'} maxW={media('min(750px, 100%)', 'auto')}>
@@ -491,19 +578,23 @@ const Chat = ({ chatId }: { chatId: string }) => {
<Flex
alignItems={'center'}
justifyContent={'center'}
h={'30px'}
w={'30px'}
h={'25px'}
w={'25px'}
position={'absolute'}
right={['12px', '20px']}
bottom={'15px'}
onClick={sendPrompt}
>
{isChatting ? (
<Icon
style={{ transform: 'translateY(4px)' }}
h={'30px'}
w={'30px'}
name={'chatting'}
className={styles.stopIcon}
width={['22px', '25px']}
height={['22px', '25px']}
cursor={'pointer'}
name={'stop'}
color={useColorModeValue('gray.500', 'white')}
onClick={() => {
controller.current?.abort();
}}
/>
) : (
<Icon
@@ -511,8 +602,9 @@ const Chat = ({ chatId }: { chatId: string }) => {
width={['18px', '20px']}
height={['18px', '20px']}
cursor={'pointer'}
fill={useColorModeValue('#718096', 'white')}
></Icon>
color={useColorModeValue('gray.500', 'white')}
onClick={sendPrompt}
/>
)}
</Flex>
</Box>
@@ -525,9 +617,10 @@ const Chat = ({ chatId }: { chatId: string }) => {
export default Chat;
export async function getServerSideProps(context: any) {
const chatId = context?.query?.chatId || 'noid';
const modelId = context?.query?.modelId || '';
const chatId = context?.query?.chatId || '';
return {
props: { chatId }
props: { modelId, chatId }
};
}

View File

@@ -1,97 +0,0 @@
import React, { useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Button,
Input,
Select,
FormControl,
FormErrorMessage
} from '@chakra-ui/react';
import { postData } from '@/api/data';
import { useMutation } from '@tanstack/react-query';
import { useForm, SubmitHandler } from 'react-hook-form';
import { DataType } from '@/types/data';
import { DataTypeTextMap } from '@/constants/data';
export interface CreateDataProps {
name: string;
type: DataType;
}
const CreateDataModal = ({
onClose,
onSuccess
}: {
onClose: () => void;
onSuccess: () => void;
}) => {
const [inputVal, setInputVal] = useState('');
const {
getValues,
register,
handleSubmit,
formState: { errors }
} = useForm<CreateDataProps>({
defaultValues: {
name: '',
type: 'abstract'
}
});
const { isLoading, mutate } = useMutation({
mutationFn: (e: CreateDataProps) => postData(e),
onSuccess() {
onSuccess();
onClose();
}
});
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<FormControl mb={8} isInvalid={!!errors.name}>
<Input
placeholder="数据集名称"
{...register('name', {
required: '数据集名称不能为空'
})}
/>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.name && errors.name.message}
</FormErrorMessage>
</FormControl>
<FormControl>
<Select placeholder="数据集类型" {...register('type', {})}>
{Object.entries(DataTypeTextMap).map(([key, value]) => (
<option key={key} value={key}>
{value}
</option>
))}
</Select>
</FormControl>
</ModalBody>
<ModalFooter>
<Button colorScheme={'gray'} onClick={onClose}>
</Button>
<Button ml={3} isLoading={isLoading} onClick={handleSubmit(mutate as any)}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default CreateDataModal;

View File

@@ -1,229 +0,0 @@
import React, { useState, useCallback } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Button,
Box,
Flex,
Textarea
} from '@chakra-ui/react';
import { useTabs } from '@/hooks/useTabs';
import { useConfirm } from '@/hooks/useConfirm';
import { useSelectFile } from '@/hooks/useSelectFile';
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/file';
import { postSplitData } from '@/api/data';
import { useMutation } from '@tanstack/react-query';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { formatPrice } from '@/utils/user';
import { modelList, ChatModelNameEnum } from '@/constants/model';
import { encode } from 'gpt-token-utils';
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
const ImportDataModal = ({
dataId,
onClose,
onSuccess
}: {
dataId: string;
onClose: () => void;
onSuccess: () => void;
}) => {
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认提交生成任务?该任务无法终止!'
});
const { toast } = useToast();
const { setIsLoading, Loading } = useLoading();
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
const { tabs, activeTab, setActiveTab } = useTabs({
tabs: [
{ id: 'text', label: '文本' },
{ id: 'doc', label: '文件' }
// { id: 'url', label: '链接' }
]
});
const [textInput, setTextInput] = useState('');
const [fileText, setFileText] = useState('');
const { mutate: handleClickSubmit, isLoading } = useMutation({
mutationFn: async () => {
let text = '';
if (activeTab === 'text') {
text = textInput;
} else if (activeTab === 'doc') {
text = fileText;
} else if (activeTab === 'url') {
}
if (!text) return;
return postSplitData(dataId, text);
},
onSuccess() {
toast({
title: '任务提交成功',
status: 'success'
});
onClose();
onSuccess();
},
onError(err: any) {
toast({
title: err?.message || '提交任务异常',
status: 'error'
});
}
});
const onSelectFile = useCallback(
async (e: File[]) => {
setIsLoading(true);
try {
const fileTexts = (
await Promise.all(
e.map((file) => {
// @ts-ignore
const extension = file?.name?.split('.').pop().toLowerCase();
switch (extension) {
case 'txt':
case 'md':
return readTxtContent(file);
case 'pdf':
return readPdfContent(file);
case 'doc':
case 'docx':
return readDocContent(file);
default:
return '';
}
})
)
)
.join('\n')
.replace(/\n+/g, '\n');
setFileText(fileTexts);
console.log(encode(fileTexts));
} catch (error: any) {
console.log(error);
toast({
title: typeof error === 'string' ? error : '解析文件失败',
status: 'error'
});
}
setIsLoading(false);
},
[setIsLoading, toast]
);
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent position={'relative'} maxW={['90vw', '800px']}>
<ModalHeader>
QA
<Box ml={2} as={'span'} fontSize={'sm'} color={'blackAlpha.600'}>
{formatPrice(
modelList.find((item) => item.model === ChatModelNameEnum.GPT35)?.price || 0,
1000
)}
/1K tokens
</Box>
</ModalHeader>
<ModalCloseButton />
<ModalBody display={'flex'}>
<Box>
{tabs.map((item) => (
<Button
key={item.id}
display={'block'}
variant={activeTab === item.id ? 'solid' : 'outline'}
_notLast={{
mb: 3
}}
onClick={() => setActiveTab(item.id)}
>
{item.label}
</Button>
))}
</Box>
<Box flex={'1 0 0'} w={0} ml={3} minH={'200px'}>
{activeTab === 'text' && (
<>
<Textarea
h={'100%'}
maxLength={-1}
value={textInput}
placeholder={'请粘贴或输入需要处理的文本'}
onChange={(e) => setTextInput(e.target.value)}
/>
<Box mt={2}>
{textInput.length} {encode(textInput).length} tokens
</Box>
</>
)}
{activeTab === 'doc' && (
<Flex
flexDirection={'column'}
p={2}
h={'100%'}
alignItems={'center'}
justifyContent={'center'}
border={'1px solid '}
borderColor={'blackAlpha.200'}
borderRadius={'md'}
fontSize={'sm'}
>
<Button onClick={onOpen}></Button>
<Box mt={2}> {fileExtension} </Box>
{fileText && (
<>
<Box mt={2}>
{fileText.length} {encode(fileText).length} tokens
</Box>
<Box
maxH={'300px'}
w={'100%'}
overflow={'auto'}
p={2}
backgroundColor={'blackAlpha.50'}
whiteSpace={'pre'}
fontSize={'xs'}
>
{fileText}
</Box>
</>
)}
</Flex>
)}
</Box>
</ModalBody>
<ModalFooter>
<Button colorScheme={'gray'} onClick={onClose}>
</Button>
<Button
ml={3}
isLoading={isLoading}
isDisabled={!textInput && !fileText}
onClick={openConfirm(handleClickSubmit)}
>
</Button>
</ModalFooter>
<Loading />
</ModalContent>
<ConfirmChild />
<File onSelect={onSelectFile} />
</Modal>
);
};
export default ImportDataModal;

View File

@@ -1,67 +0,0 @@
import React from 'react';
import { Box, Card } from '@chakra-ui/react';
import ScrollData from '@/components/ScrollData';
import { getDataItems } from '@/api/data';
import { usePaging } from '@/hooks/usePaging';
import type { DataItemSchema } from '@/types/mongoSchema';
const DataDetail = ({ dataName, dataId }: { dataName: string; dataId: string }) => {
const {
nextPage,
isLoadAll,
requesting,
data: dataItems
} = usePaging<DataItemSchema>({
api: getDataItems,
pageSize: 10,
params: {
dataId
}
});
return (
<Card py={4} h={'100%'} display={'flex'} flexDirection={'column'}>
<Box px={6} fontSize={'xl'} fontWeight={'bold'}>
{dataName}
</Box>
<ScrollData
flex={'1 0 0'}
h={0}
px={6}
mt={3}
isLoadAll={isLoadAll}
requesting={requesting}
nextPage={nextPage}
fontSize={'xs'}
whiteSpace={'pre-wrap'}
>
{dataItems.map((item) => (
<Box key={item._id}>
{item.result.map((result, i) => (
<Box key={i} mb={3}>
{item.type === 'QA' && (
<>
<Box fontWeight={'bold'}>Q: {result.q}</Box>
<Box>A: {result.a}</Box>
</>
)}
{item.type === 'abstract' && <Box fontSize={'sm'}>{result.abstract}</Box>}
</Box>
))}
</Box>
))}
</ScrollData>
</Card>
);
};
export default DataDetail;
export async function getServerSideProps(context: any) {
return {
props: {
dataName: context.query?.dataName || '',
dataId: context.query?.dataId || ''
}
};
}

View File

@@ -1,235 +0,0 @@
import React, { useState, useCallback } from 'react';
import {
Card,
Box,
Flex,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
useDisclosure,
Input,
Menu,
MenuButton,
MenuList,
MenuItem
} from '@chakra-ui/react';
import { getDataList, updateDataName, delData, getDataItems } from '@/api/data';
import type { DataListItem } from '@/types/data';
import dayjs from 'dayjs';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import { useConfirm } from '@/hooks/useConfirm';
import { useRequest } from '@/hooks/useRequest';
import { DataItemSchema } from '@/types/mongoSchema';
import { DataTypeTextMap } from '@/constants/data';
import { customAlphabet } from 'nanoid';
import { useQuery } from '@tanstack/react-query';
const nanoid = customAlphabet('.,', 1);
const CreateDataModal = dynamic(() => import('./components/CreateDataModal'));
const ImportDataModal = dynamic(() => import('./components/ImportDataModal'));
export type ExportDataType = 'jsonl' | 'txt';
const DataList = () => {
const router = useRouter();
const [ImportDataId, setImportDataId] = useState<string>();
const { openConfirm, ConfirmChild } = useConfirm({
content: '删除数据集,将删除里面的所有内容,请确认!'
});
const {
isOpen: isOpenCreateDataModal,
onOpen: onOpenCreateDataModal,
onClose: onCloseCreateDataModal
} = useDisclosure();
const { data: dataList = [], refetch } = useQuery(['getDataList'], getDataList, {
refetchInterval: 10000
});
const { mutate: handleDelData, isLoading: isDeleting } = useRequest({
mutationFn: (dataId: string) => delData(dataId),
successToast: '删除数据集成功',
errorToast: '删除数据集异常',
onSuccess() {
refetch();
}
});
const { mutate: handleExportData, isLoading: isExporting } = useRequest({
mutationFn: async ({ data, type }: { data: DataListItem; type: ExportDataType }) => ({
type,
data: await getDataItems({ dataId: data._id, pageNum: 1, pageSize: data.totalData }).then(
(res) => res.data
)
}),
successToast: '导出数据集成功',
errorToast: '导出数据集异常',
onSuccess(res: { type: ExportDataType; data: DataItemSchema[] }) {
// 合并数据
const data = res.data.map((item) => item.result).flat();
let text = '';
// 生成 jsonl
data.forEach((item) => {
if (res.type === 'jsonl' && item.q && item.a) {
const result = JSON.stringify({
prompt: `${item.q.toLocaleLowerCase()}${nanoid()}</s>`,
completion: ` ${item.a}###`
});
text += `${result}\n`;
} else if (res.type === 'txt' && item.abstract) {
text += `${item.abstract}\n`;
}
});
// 去掉最后一个 \n
text = text.substring(0, text.length - 1);
// 导出为文件
const blob = new Blob([text], { type: 'application/json;charset=utf-8' });
// 创建下载链接
const downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(blob);
downloadLink.download = `data.${res.type}`;
// 添加链接到页面并触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
});
return (
<Box display={['block', 'flex']} flexDirection={'column'} h={'100%'}>
<Card px={6} py={4}>
<Flex>
<Box flex={1} mr={1}>
<Box fontSize={'xl'} fontWeight={'bold'}>
</Box>
<Box fontSize={'xs'} color={'blackAlpha.600'}>
QA
</Box>
</Box>
<Button variant={'outline'} onClick={onOpenCreateDataModal}>
</Button>
</Flex>
</Card>
{/* 数据表 */}
<TableContainer
mt={3}
flex={'1 0 0'}
h={['auto', '0']}
overflowY={'auto'}
px={6}
py={4}
backgroundColor={'white'}
borderRadius={'md'}
boxShadow={'base'}
>
<Table>
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
<Th> / </Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{dataList.map((item, i) => (
<Tr key={item._id}>
<Td>
<Input
minW={'150px'}
placeholder="请输入数据集名称"
defaultValue={item.name}
size={'sm'}
onBlur={(e) => {
if (!e.target.value || e.target.value === item.name) return;
updateDataName(item._id, e.target.value);
}}
/>
</Td>
<Td>{DataTypeTextMap[item.type || 'QA']}</Td>
<Td>{dayjs(item.createTime).format('YYYY/MM/DD HH:mm')}</Td>
<Td>
{item.trainingData} / {item.totalData}
</Td>
<Td>
<Button
size={'sm'}
variant={'outline'}
colorScheme={'gray'}
mr={2}
onClick={() =>
router.push(`/data/detail?dataId=${item._id}&dataName=${item.name}`)
}
>
</Button>
<Button
size={'sm'}
variant={'outline'}
mr={2}
onClick={() => setImportDataId(item._id)}
>
</Button>
{/* <Menu>
<MenuButton as={Button} mr={2} size={'sm'} isLoading={isExporting}>
导出
</MenuButton>
<MenuList>
{item.type === 'QA' && (
<MenuItem onClick={() => handleExportData({ data: item, type: 'jsonl' })}>
jsonl
</MenuItem>
)}
{item.type === 'abstract' && (
<MenuItem onClick={() => handleExportData({ data: item, type: 'txt' })}>
txt
</MenuItem>
)}
</MenuList>
</Menu> */}
<Button
size={'sm'}
colorScheme={'red'}
isLoading={isDeleting}
onClick={openConfirm(() => handleDelData(item._id))}
>
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{ImportDataId && (
<ImportDataModal
dataId={ImportDataId}
onClose={() => setImportDataId(undefined)}
onSuccess={refetch}
/>
)}
{isOpenCreateDataModal && (
<CreateDataModal onClose={onCloseCreateDataModal} onSuccess={refetch} />
)}
<ConfirmChild />
</Box>
);
};
export default DataList;

View File

@@ -1,15 +1,34 @@
import React from 'react';
import { Card } from '@chakra-ui/react';
import React, { useEffect } from 'react';
import { Card, Box, Link } from '@chakra-ui/react';
import Markdown from '@/components/Markdown';
import { useMarkdown } from '@/hooks/useMarkdown';
import { useRouter } from 'next/router';
const Home = () => {
const { inviterId } = useRouter().query as { inviterId: string };
const { data } = useMarkdown({ url: '/intro.md' });
useEffect(() => {
if (inviterId) {
localStorage.setItem('inviterId', inviterId);
}
}, [inviterId]);
return (
<Card p={5} lineHeight={2}>
<Markdown source={data} isChatting={false} />
</Card>
<>
<Card p={5} lineHeight={2}>
<Markdown source={data} isChatting={false} />
</Card>
<Card p={5} mt={4} textAlign={'center'}>
<Box>
{/* <Link href="https://beian.miit.gov.cn/" target="_blank">
浙B2-20080101
</Link> */}
</Box>
<Box>Made by FastGpt Team.</Box>
</Card>
</>
);
};

View File

@@ -57,7 +57,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
username,
code,
password,
inviterId
inviterId: inviterId || localStorage.getItem('inviterId') || ''
})
);
toast({

View File

@@ -13,6 +13,7 @@ const ForgetPasswordForm = dynamic(() => import('./components/ForgetPasswordForm
const Login = () => {
const router = useRouter();
const { lastRoute = '' } = router.query as { lastRoute: string };
const { isPc } = useScreen();
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
const { setUserInfo } = useUserStore();
@@ -20,9 +21,11 @@ const Login = () => {
const loginSuccess = useCallback(
(res: ResLogin) => {
setUserInfo(res.user, res.token);
router.push('/model/list');
setTimeout(() => {
router.push(lastRoute ? decodeURIComponent(lastRoute) : '/model/list');
}, 100);
},
[router, setUserInfo]
[lastRoute, router, setUserInfo]
);
function DynamicComponent({ type }: { type: `${PageTypeEnum}` }) {

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