Compare commits

..

27 Commits
v2.8.5 ... v3.1

Author SHA1 Message Date
archer
c971adaabd docs 2023-04-29 16:01:42 +08:00
archer
ea100d84bf perf: auth token 2023-04-29 15:59:53 +08:00
archer
78762498eb perf: model framwork 2023-04-29 15:55:47 +08:00
archer
cd9acab938 fix: request config 2023-04-28 16:47:55 +08:00
archer
56b3ddc147 fix: mode data 2023-04-28 15:07:38 +08:00
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
126 changed files with 2147 additions and 2397 deletions

View File

@@ -1,7 +1,9 @@
# proxy
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT_FAST=7890
AXIOS_PROXY_PORT_NORMAL=7890
# 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

View File

@@ -5,7 +5,7 @@ on:
push:
branches:
- "main"
- 'main'
jobs:
release:
@@ -14,7 +14,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
fetch-depth: 1
- name: Install Dependencies
run: |
@@ -41,6 +41,8 @@ jobs:
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 \

1
.gitignore vendored
View File

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

106
README.md
View File

@@ -6,30 +6,33 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
![KBProcess](docs/imgs/KBProcess.jpg?raw=true "KBProcess")
## 开发
复制 .env.template 成 .env.local ,填写核心参数
**配置环境变量**
```bash
# proxy不需要代理可忽略
# proxy可选
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT_FAST=7890
AXIOS_PROXY_PORT_NORMAL=7890
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
# email参考 nodeMail 获取参数
# 发送邮箱验证码配置。参考 nodeMail 获取参数,自行百度。
MY_MAIL=xxx@qq.com
MAILE_CODE=xxx
# 阿里短信服务
# 阿里短信服务(邮箱和短信至少二选一)
aliAccessKeyId=xxx
aliAccessKeySecret=xxx
aliSignName=xxx
aliTemplateCode=SMS_xxx
# token随便填登录凭证
# token随便填作为登录凭证)
TOKEN_KEY=xxx
# openai key
OPENAIKEY=sk-xxx
# mongo连接地址
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
MONGODB_NAME=xxx # mongo数据库名称
# mongo数据库名称
MONGODB_NAME=xxx
# pg 数据库相关内容,和 docker-compose 对上
PG_HOST=0.0.0.0
PG_PORT=8102
@@ -37,13 +40,20 @@ PG_USER=xxx
PG_PASSWORD=xxx
PG_DB_NAME=xxx
```
```bash
**运行**
```
pnpm dev
```
## 部署
### 安装 docker 和 docker-compose
### 代理环境(国外服务器可忽略)
1. [clash 方案](./docs/proxy/clash.md) - 仅需一台服务器(需要有 clash
2. [nginx 方案](./docs/proxy/nginx.md) - 需要一台国外服务器
3. [cloudflare 方案](./docs/proxy/cloudflare.md) - 需要有域名(每日免费 10w 次代理请求)
### docker 部署
#### 1. 安装 docker 和 docker-compose
这个不同系统略有区别,百度安装下。验证安装成功后进行下一步。下面给出一个例子:
```bash
# 安装docker
@@ -57,48 +67,12 @@ 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 里内容复制过去
### 本地 docker 打包
```bash
docker build -t imageName:tag .
docker push imageName:tag
# 或者直接拉镜像,见下方
```
### 准备初始化文件
**/root/fast-gpt/pg/init.sql**
```sql
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
@@ -119,6 +93,7 @@ CREATE INDEX modelData_userId_index ON modelData USING HASH (user_id);
CREATE INDEX modelData_modelId_index ON modelData USING HASH (model_id);
EOSQL
```
**/root/fast-gpt/nginx/nginx.conf**
```conf
user nginx;
@@ -171,6 +146,7 @@ http {
}
}
```
**/root/fast-gpt/docker-compose.yml**
```yml
version: "3.3"
@@ -181,24 +157,19 @@ services:
restart: always
container_name: fast-gpt
environment:
# 代理(不需要代理,可去掉下面三个参数)
- AXIOS_PROXY_HOST=127.0.0.1
- AXIOS_PROXY_PORT_FAST=7890
- AXIOS_PROXY_PORT_NORMAL=7890
# 邮箱
# - 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
# 登录 key
- TOKEN_KEY=xxxx
# 是否开启队列任务。 1-开启0-关闭请求parentUrl去执行任务,单机时直接填1
- queueTask=1
- parentUrl=https://hostname/api/openapi/startEvents
# db
- MONGODB_URI=mongodb://username:passsword@0.0.0.0:27017/?authSource=admin
- MONGODB_NAME=xxx
- PG_HOST=0.0.0.0
@@ -206,7 +177,6 @@ services:
- PG_USER=xxx
- PG_PASSWORD=xxx
- PG_DB_NAME=xxx
# openai 账号
- OPENAIKEY=sk-xxxxx
nginx:
image: nginx:alpine3.17
@@ -246,7 +216,9 @@ services:
- /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
@@ -270,5 +242,15 @@ do
done
```
## Mac 可能的问题
> 因为教程有部分镜像不兼容arm64所以写个文档指导新手如何快速在mac上面搭建fast-gpt[如何在mac上面部署fastgpt](./docs/mac.md)
## 其他优化点
### 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

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

@@ -2,7 +2,7 @@
const nextConfig = {
output: 'standalone',
reactStrictMode: true,
reactStrictMode: false,
compress: true,
webpack(config) {

View File

@@ -1,5 +1,5 @@
### Fast GPT V2.8.1
* 优化 - 知识库升级,内容条数不上限!
* 优化 - 导入去重效果,可防止导出后的 csv 重复导入
* 优化 - 聊天框,电脑端复制删除图标
* 优化 - 聊天框,生成内容时,如果滚动条触底,则会自动向下滚动,不需要手动下滑
### Fast GPT V3.1
- 优化 - 模型结构设计,不再区分知识库和对话模型,而是通过开关的形式,手动选择手否需要进行知识库搜索
- 新增 - 模型共享市场,可以使用其他用户分享的模型
- 新增 - 邀请好友注册功能

BIN
public/imgs/modelAvatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 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

@@ -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 } from '@/types/mongoSchema';
import { ModelUpdateParams } from '@/types/model';
import { TrainingItemType } from '../types/training';
import { ModelUpdateParams, ShareModelItem } from '@/types/model';
import { RequestPaging } from '../types/index';
import { Obj2Query } from '@/utils/tools';
@@ -13,8 +12,7 @@ export const getMyModels = () => GET<ModelSchema[]>('/model/list');
/**
* 创建一个模型
*/
export const postCreateModel = (data: { name: string; serviceModelName: string }) =>
POST<ModelSchema>('/model/create', data);
export const postCreateModel = (data: { name: string }) => POST<string>('/model/create', data);
/**
* 根据 ID 删除模型
@@ -32,21 +30,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;
@@ -108,3 +92,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

@@ -7,7 +7,6 @@ export type InitChatResponse = {
name: string;
avatar: string;
intro: string;
chatModel: ModelSchema.service.chatModel; // 对话模型名
modelName: ModelSchema.service.modelName; // 底层模型
chatModel: ModelSchema['chat']['chatModel']; // 对话模型名
history: ChatItemType[];
};

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="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

@@ -13,12 +13,15 @@ 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
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;

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.replace('/login');
router.replace(
`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`
);
toast();
},
onSettled() {

View File

@@ -20,12 +20,19 @@ 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',

View File

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

View File

@@ -1,65 +1,35 @@
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 ModelNameEnum {
GPT35 = 'gpt-3.5-turbo',
VECTOR_GPT = 'VECTOR_GPT'
}
export const Model2ChatModelMap: Record<`${ModelNameEnum}`, `${ChatModelEnum}`> = {
[ModelNameEnum.GPT35]: 'gpt-3.5-turbo',
[ModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo'
export const ChatModelMap = {
// ui name
[ChatModelEnum.GPT35]: 'ChatGpt',
[ChatModelEnum.GPT4]: 'Gpt4',
[ChatModelEnum.GPT432k]: 'Gpt4-32k'
};
export type ModelConstantsData = {
icon: 'model' | 'dbModel';
name: string;
model: `${ModelNameEnum}`;
trainName: string; // 空字符串代表不能训练
export type ChatModelConstantType = {
chatModel: `${ChatModelEnum}`;
contextMaxToken: number;
maxTemperature: number;
price: number; // 多少钱 / 1token单位: 0.00001元
};
export const modelList: ModelConstantsData[] = [
export const modelList: ChatModelConstantType[] = [
{
icon: 'model',
name: 'chatGPT',
model: ModelNameEnum.GPT35,
trainName: '',
chatModel: ChatModelEnum.GPT35,
contextMaxToken: 4096,
maxTemperature: 1.5,
price: 3
},
{
icon: 'dbModel',
name: '知识库',
model: ModelNameEnum.VECTOR_GPT,
trainName: 'vector',
contextMaxToken: 4096,
maxTemperature: 1,
price: 3
}
];
export enum TrainingStatusEnum {
pending = 'pending',
succeed = 'succeed',
errored = 'errored',
canceled = 'canceled'
}
export enum ModelStatusEnum {
running = 'running',
training = 'training',
@@ -86,6 +56,11 @@ export const formatModelStatus = {
}
};
export enum ModelDataStatusEnum {
ready = 'ready',
waiting = 'waiting'
}
export const ModelDataStatusMap: Record<`${ModelDataStatusEnum}`, string> = {
ready: '训练完成',
waiting: '训练中'
@@ -120,23 +95,24 @@ export const ModelVectorSearchModeMap: Record<
};
export const defaultModel: ModelSchema = {
_id: '',
userId: '',
name: 'modelName',
avatar: '',
_id: 'modelId',
userId: 'userId',
name: '模型名称',
avatar: '/icon/logo.png',
status: ModelStatusEnum.pending,
updateTime: Date.now(),
trainingTimes: 0,
systemPrompt: '',
intro: '',
temperature: 5,
search: {
mode: ModelVectorSearchModeEnum.hightSimilarity
chat: {
useKb: false,
searchMode: ModelVectorSearchModeEnum.hightSimilarity,
systemPrompt: '',
temperature: 0,
chatModel: ChatModelEnum.GPT35
},
service: {
trainId: '',
chatModel: ModelNameEnum.GPT35,
modelName: ModelNameEnum.GPT35
share: {
isShare: false,
isShareDetail: false,
intro: '',
collection: 0
},
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

@@ -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,

View File

@@ -1,13 +1,14 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { getOpenAIApi, authChat } from '@/service/utils/auth';
import { httpsAgent, openaiChatFilter } from '@/service/utils/tools';
import { axiosConfig, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
import { modelList } from '@/constants/model';
import { modelList, ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { gpt35StreamResponse } from '@/service/utils/openai';
import { searchKb_openai } from '@/service/tools/searchKb';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -46,7 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
authorization
});
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
const modelConstantsData = modelList.find((item) => item.chatModel === model.chat.chatModel);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
@@ -54,31 +55,84 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 读取对话内容
const prompts = [...content, prompt];
// 如果有系统提示词,自动插入
if (model.systemPrompt) {
prompts.unshift({
obj: 'SYSTEM',
value: model.systemPrompt
// 使用了知识库搜索
if (model.chat.useKb) {
const { systemPrompts } = await searchKb_openai({
apiKey: userApiKey || systemKey,
isPay: !userApiKey,
text: prompt.value,
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22,
modelId,
userId
});
// filter system prompt
if (
systemPrompts.length === 0 &&
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity
) {
return res.send('对不起,你的问题不在知识库中。');
}
/* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */
if (
systemPrompts.length === 0 &&
model.chat.searchMode === ModelVectorSearchModeEnum.noContext
) {
prompts.unshift({
obj: 'SYSTEM',
value: model.chat.systemPrompt
});
} else {
// 有匹配情况下system 添加知识库内容。
// 系统提示词过滤,最多 2500 tokens
const filterSystemPrompt = systemPromptFilter({
model: model.chat.chatModel,
prompts: systemPrompts,
maxTokens: 2500
});
prompts.unshift({
obj: 'SYSTEM',
value: `
${model.chat.systemPrompt}
${
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity
? `不回答知识库外的内容.`
: ''
}
知识库内容为: ${filterSystemPrompt}'
`
});
}
} else {
// 没有用知识库搜索,仅用系统提示词
if (model.chat.systemPrompt) {
prompts.unshift({
obj: 'SYSTEM',
value: model.chat.systemPrompt
});
}
}
// 控制 tokens 数量,防止超出
// 控制 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
model: model.chat.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// console.log(filterPrompts);
// 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey || systemKey);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature,
model: model.chat.chatModel,
temperature: Number(temperature) || 0,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
@@ -88,7 +142,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{
timeout: 40000,
responseType: 'stream',
httpsAgent: httpsAgent(!userApiKey)
...axiosConfig()
}
);
@@ -105,7 +159,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 只有使用平台的 key 才计费
pushChatBill({
isPay: !userApiKey,
modelName: model.service.modelName,
chatModel: model.chat.chatModel,
userId,
chatId,
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
/* 获取历史记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import type { InitChatResponse } from '@/api/response/chat';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat';
import { authModel } from '@/service/utils/auth';
import mongoose from 'mongoose';
@@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
// 获取 model 数据
const { model } = await authModel(modelId, userId);
const { model } = await authModel({ modelId, userId, authUser: false, authOwner: false });
// 历史记录
let history: ChatItemType[] = [];
@@ -30,7 +30,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (chatId) {
// 获取 chat.content 数据
history = await Chat.aggregate([
{ $match: { _id: new mongoose.Types.ObjectId(chatId) } },
{
$match: {
_id: new mongoose.Types.ObjectId(chatId),
userId: new mongoose.Types.ObjectId(userId)
}
},
{ $unwind: '$content' },
{ $match: { 'content.deleted': false } },
{ $sort: { 'content._id': -1 } },
@@ -53,9 +58,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
modelId: modelId,
name: model.name,
avatar: model.avatar,
intro: model.intro,
modelName: model.service.modelName,
chatModel: model.service.chatModel,
intro: model.share.intro,
chatModel: model.chat.chatModel,
history
}
});

View File

@@ -2,7 +2,7 @@ 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';
import { authToken } from '@/service/utils/auth';
/* 获取历史记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -3,7 +3,7 @@ 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';
import { authToken } from '@/service/utils/auth';
/* 聊天内容存存储 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -27,9 +27,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
value: item.value
}));
await authModel({ modelId, userId, authOwner: false });
// 没有 chatId, 创建一个对话
if (!chatId) {
await authModel(modelId, userId);
const { _id } = await Chat.create({
userId,
modelId,

View File

@@ -1,188 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authChat } from '@/service/utils/auth';
import { httpsAgent, systemPromptFilter, openaiChatFilter } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
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';
import { PgClient } from '@/service/pg';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
let step = 0; // step=1时表示开始了流响应
const stream = new PassThrough();
stream.on('error', () => {
console.log('error: ', 'stream error');
stream.destroy();
});
res.on('close', () => {
stream.destroy();
});
res.on('error', () => {
console.log('error: ', 'request error');
stream.destroy();
});
try {
const { modelId, chatId, prompt } = req.body as {
modelId: string;
chatId: '' | string;
prompt: ChatItemType;
};
const { authorization } = req.headers;
if (!modelId || !prompt) {
throw new Error('缺少参数');
}
await connectToDatabase();
let startTime = Date.now();
const { model, content, userApiKey, systemKey, userId } = await authChat({
modelId,
chatId,
authorization
});
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
// 读取对话内容
const prompts = [...content, prompt];
// 获取提示词的向量
const { vector: promptVector, chatAPI } = await openaiCreateEmbedding({
isPay: !userApiKey,
apiKey: userApiKey || systemKey,
userId,
text: prompt.value
});
// 相似度搜素
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: [
['status', ModelDataStatusEnum.ready],
'AND',
['model_id', model._id],
'AND',
`vector <=> '[${promptVector}]' < ${similarity}`
],
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
limit: 20
});
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
/* 高相似度+退出,无法匹配时直接退出 */
if (
formatRedisPrompt.length === 0 &&
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
) {
return res.send('对不起,你的问题不在知识库中。');
}
/* 高相似度+无上下文,不添加额外知识 */
if (
formatRedisPrompt.length === 0 &&
model.search.mode === ModelVectorSearchModeEnum.noContext
) {
prompts.unshift({
obj: 'SYSTEM',
value: model.systemPrompt
});
} else {
// 有匹配情况下system 添加知识库内容。
// 系统提示词过滤,最多 2500 tokens
const systemPrompt = systemPromptFilter({
model: model.service.chatModel,
prompts: formatRedisPrompt,
maxTokens: 2500
});
prompts.unshift({
obj: 'SYSTEM',
value: `
${model.systemPrompt}
${
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
? `你只能从知识库选择内容回答.不在知识库内容拒绝回复`
: ''
}
知识库内容为: 当前时间为${dayjs().format('YYYY/MM/DD HH:mm:ss')}\n${systemPrompt}'
`
});
}
// 控制在 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// console.log(filterPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: true
},
{
timeout: 40000,
responseType: 'stream',
httpsAgent: httpsAgent(!userApiKey)
}
);
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
step = 1;
const { responseContent } = await gpt35StreamResponse({
res,
stream,
chatResponse
});
// 只有使用平台的 key 才计费
pushChatBill({
isPay: !userApiKey,
modelName: model.service.modelName,
userId,
chatId,
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});
// jsonRes(res);
} catch (err: any) {
if (step === 1) {
// 直接结束流
console.log('error结束');
stream.destroy();
} else {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
}
}

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,48 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, DataItem } 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 };
pageNum = +pageNum;
pageSize = +pageSize;
if (!dataId) {
throw new Error('参数错误');
}
await connectToDatabase();
const { authorization } = req.headers;
await authToken(authorization);
const dataItems = await DataItem.find({
dataId
})
.sort({ _id: -1 }) // 按照创建时间倒序排列
.skip((pageNum - 1) * pageSize)
.limit(pageSize);
jsonRes(res, {
data: {
pageNum,
pageSize,
data: dataItems,
total: await DataItem.countDocuments({
dataId
})
}
});
} 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 { countChatTokens } from '@/utils/tools';
/* 拆分数据成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 = countChatTokens({ messages: [{ role: 'system', content: splitText }] });
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

@@ -2,15 +2,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, ModelNameEnum, Model2ChatModelMap } from '@/constants/model';
import { authToken } from '@/service/utils/auth';
import { ModelStatusEnum } 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 {
const { name } = req.body as {
name: string;
serviceModelName: `${ModelNameEnum}`;
};
const { authorization } = req.headers;
@@ -18,46 +17,32 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('无权操作');
}
if (!name || !serviceModelName) {
if (!name) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
const modelItem = modelList.find((item) => item.model === serviceModelName);
if (!modelItem) {
throw new Error('模型不存在');
}
await connectToDatabase();
// 上限校验
const authCount = await Model.countDocuments({
userId
});
if (authCount >= 20) {
throw new Error('上限 20 个模型');
if (authCount >= 30) {
throw new Error('上限 30 个模型');
}
// 创建模型
const response = await Model.create({
name,
userId,
status: ModelStatusEnum.running,
service: {
trainId: '',
chatModel: Model2ChatModelMap[modelItem.model], // 聊天时用的模型
modelName: modelItem.model // 最底层的模型,不会变,用于计费等核心操作
}
status: ModelStatusEnum.running
});
// 根据 id 获取模型信息
const model = await Model.findById(response._id);
jsonRes(res, {
data: model
data: response._id
});
} catch (err) {
jsonRes(res, {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {

View File

@@ -1,9 +1,9 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
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

@@ -1,9 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
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,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const { model } = await authModel({
userId,
modelId,
authOwner: false
});
const where: any = [
['user_id', userId],
'AND',
...(model.share.isShareDetail ? [] : [['user_id', userId], 'AND']),
['model_id', modelId],
...(searchText ? ['AND', `(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%')`] : [])
];

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
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,15 +29,11 @@ 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]) => {

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { ModelDataSchema } from '@/types/mongoSchema';
import { generateVector } from '@/service/events/generateVector';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -28,15 +29,11 @@ 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('无权操作该模型');
}
// 插入记录
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 { authToken } from '@/service/utils/auth';
import { ModelDataStatusEnum } from '@/constants/model';
import { generateVector } from '@/service/events/generateVector';
import { PgClient } from '@/service/pg';

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { generateVector } from '@/service/events/generateVector';
import { generateQA } from '@/service/events/generateQA';
import { PgClient } from '@/service/pg';

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 { 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 { Chat, Model, connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
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

@@ -1,9 +1,8 @@
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 { authToken } from '@/service/utils/auth';
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

@@ -1,10 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
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/auth';
/* 模型收藏切换 */
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/auth';
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/auth';
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

@@ -1,15 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
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 } =
req.body as ModelUpdateParams;
const { name, avatar, chat, share, security } = req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string };
const { authorization } = req.headers;
@@ -17,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('无权操作');
}
if (!name || !service || !security || !modelId) {
if (!name || !chat || !security || !modelId) {
throw new Error('参数错误');
}
@@ -26,6 +26,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
await authModel({
modelId,
userId
});
// 更新模型
await Model.updateOne(
{
@@ -34,11 +39,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
},
{
name,
systemPrompt,
intro,
temperature,
search,
// service,
avatar,
chat,
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail,
'share.intro': share.intro,
security
}
);

View File

@@ -0,0 +1,202 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { getOpenAIApi, authOpenApiKey, authModel } from '@/service/utils/auth';
import { axiosConfig, openaiChatFilter, systemPromptFilter } 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 { pushChatBill } from '@/service/events/pushBill';
import { gpt35StreamResponse } from '@/service/utils/openai';
import { searchKb_openai } from '@/service/tools/searchKb';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
let step = 0; // step=1时表示开始了流响应
const stream = new PassThrough();
stream.on('error', () => {
console.log('error: ', 'stream error');
stream.destroy();
});
res.on('close', () => {
stream.destroy();
});
res.on('error', () => {
console.log('error: ', 'request error');
stream.destroy();
});
try {
const {
prompts,
modelId,
isStream = true
} = req.body as {
prompts: ChatItemType[];
modelId: string;
isStream: boolean;
};
if (!prompts || !modelId) {
throw new Error('缺少参数');
}
if (!Array.isArray(prompts)) {
throw new Error('prompts is not array');
}
if (prompts.length > 30 || prompts.length === 0) {
throw new Error('prompts length range 1-30');
}
await connectToDatabase();
let startTime = Date.now();
/* 凭证校验 */
const { apiKey, userId } = await authOpenApiKey(req);
const { model } = await authModel({
userId,
modelId
});
const modelConstantsData = modelList.find((item) => item.chatModel === model.chat.chatModel);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
// 使用了知识库搜索
if (model.chat.useKb) {
const similarity = ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22;
const { systemPrompts } = await searchKb_openai({
apiKey,
isPay: true,
text: prompts[prompts.length - 1].value,
similarity,
modelId,
userId
});
// filter system prompt
if (
systemPrompts.length === 0 &&
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity
) {
return jsonRes(res, {
code: 500,
message: '对不起,你的问题不在知识库中。',
data: '对不起,你的问题不在知识库中。'
});
}
/* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */
if (
systemPrompts.length === 0 &&
model.chat.searchMode === ModelVectorSearchModeEnum.noContext
) {
prompts.unshift({
obj: 'SYSTEM',
value: model.chat.systemPrompt
});
} else {
// 有匹配情况下system 添加知识库内容。
// 系统提示词过滤,最多 2500 tokens
const filterSystemPrompt = systemPromptFilter({
model: model.chat.chatModel,
prompts: systemPrompts,
maxTokens: 2500
});
prompts.unshift({
obj: 'SYSTEM',
value: `
${model.chat.systemPrompt}
${
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity
? `不回答知识库外的内容.`
: ''
}
知识库内容为: ${filterSystemPrompt}'
`
});
}
} else {
// 没有用知识库搜索,仅用系统提示词
if (model.chat.systemPrompt) {
prompts.unshift({
obj: 'SYSTEM',
value: model.chat.systemPrompt
});
}
}
// 控制总 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.chat.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// console.log(filterPrompts);
// 获取 chatAPI
const chatAPI = getOpenAIApi(apiKey);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.chat.chatModel,
temperature: Number(temperature) || 0,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: isStream,
stop: ['.!?。']
},
{
timeout: 180000,
responseType: isStream ? 'stream' : 'json',
...axiosConfig()
}
);
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
let responseContent = '';
if (isStream) {
step = 1;
const streamResponse = await gpt35StreamResponse({
res,
stream,
chatResponse
});
responseContent = streamResponse.responseContent;
} else {
responseContent = chatResponse.data.choices?.[0]?.message?.content || '';
jsonRes(res, {
data: responseContent
});
}
// 只有使用平台的 key 才计费
pushChatBill({
isPay: true,
chatModel: model.chat.chatModel,
userId,
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});
} catch (err: any) {
if (step === 1) {
// 直接结束流
console.log('error结束');
stream.destroy();
} else {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
}
}

View File

@@ -1,8 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase, Model } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/auth';
import { httpsAgent, openaiChatFilter, authOpenApiKey } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { getOpenAIApi, authOpenApiKey } from '@/service/utils/auth';
import { axiosConfig, openaiChatFilter } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
@@ -61,37 +60,38 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('无权使用该模型');
}
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
const modelConstantsData = modelList.find((item) => item.chatModel === model.chat.chatModel);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
// 如果有系统提示词,自动插入
if (model.systemPrompt) {
if (model.chat.systemPrompt) {
prompts.unshift({
obj: 'SYSTEM',
value: model.systemPrompt
value: model.chat.systemPrompt
});
}
// 控制在 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
model: model.chat.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// console.log(filterPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// 获取 chatAPI
const chatAPI = getOpenAIApi(apiKey);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature,
model: model.chat.chatModel,
temperature: Number(temperature) || 0,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
@@ -101,16 +101,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,
@@ -127,7 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 只有使用平台的 key 才计费
pushChatBill({
isPay: true,
modelName: model.service.modelName,
chatModel: model.chat.chatModel,
userId,
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});

View File

@@ -1,20 +1,14 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase, Model } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/auth';
import { authOpenApiKey } from '@/service/utils/tools';
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
import { getOpenAIApi, authOpenApiKey } from '@/service/utils/auth';
import { axiosConfig, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
import {
ModelNameEnum,
modelList,
ModelVectorSearchModeMap,
ChatModelEnum
} from '@/constants/model';
import { modelList, ModelVectorSearchModeMap, ChatModelEnum } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
import { PgClient } from '@/service/pg';
import { gpt35StreamResponse } from '@/service/utils/openai';
import { searchKb_openai } from '@/service/tools/searchKb';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -59,10 +53,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('找不到模型');
}
const modelConstantsData = modelList.find((item) => item.model === ModelNameEnum.VECTOR_GPT);
const modelConstantsData = modelList.find((item) => item.chatModel === model.chat.chatModel);
if (!modelConstantsData) {
throw new Error('模型已下架');
throw new Error('model is undefined');
}
console.log('laf gpt start');
// 获取 chatAPI
@@ -120,7 +115,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
{
timeout: 180000,
httpsAgent: httpsAgent(true)
...axiosConfig()
}
);
@@ -132,62 +127,48 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
prompt.value += ` ${promptResolve}`;
console.log('prompt resolve success, time:', `${(Date.now() - startTime) / 1000}s`);
// 获取提示词的向量
const { vector: promptVector } = await openaiCreateEmbedding({
isPay: true,
apiKey,
userId,
text: prompt.value
});
// 读取对话内容
const prompts = [prompt];
// 相似度搜索
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'],
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
where: [
['model_id', model._id],
'AND',
['user_id', userId],
'AND',
`vector <=> '[${promptVector}]' < ${similarity}`
],
limit: 30
// 获取向量匹配到的提示词
const { systemPrompts } = await searchKb_openai({
isPay: true,
apiKey,
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22,
text: prompt.value,
modelId,
userId
});
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
// system 筛选,最多 2500 tokens
const systemPrompt = systemPromptFilter({
model: model.service.chatModel,
prompts: formatRedisPrompt,
const filterSystemPrompt = systemPromptFilter({
model: model.chat.chatModel,
prompts: systemPrompts,
maxTokens: 2500
});
prompts.unshift({
obj: 'SYSTEM',
value: `${model.systemPrompt} 知识库是最新的,下面是知识库内容:${systemPrompt}`
value: `${model.chat.systemPrompt} 知识库是最新的,下面是知识库内容:${filterSystemPrompt}`
});
// 控制上下文 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
model: model.chat.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// console.log(filterPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature,
model: model.chat.chatModel,
temperature: Number(temperature) || 0,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
@@ -196,16 +177,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{
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,
@@ -223,7 +204,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
pushChatBill({
isPay: true,
modelName: model.service.modelName,
chatModel: model.chat.chatModel,
userId,
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});

View File

@@ -1,25 +1,14 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase, Model } from '@/service/mongo';
import {
httpsAgent,
systemPromptFilter,
authOpenApiKey,
openaiChatFilter
} from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { axiosConfig, systemPromptFilter, openaiChatFilter } from '@/service/utils/tools';
import { getOpenAIApi, authOpenApiKey } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
import {
modelList,
ModelVectorSearchModeMap,
ModelVectorSearchModeEnum,
ModelDataStatusEnum
} from '@/constants/model';
import { modelList, ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
import dayjs from 'dayjs';
import { PgClient } from '@/service/pg';
import { gpt35StreamResponse } from '@/service/utils/openai';
import { searchKb_openai } from '@/service/tools/searchKb';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -73,114 +62,105 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('无权使用该模型');
}
const modelConstantsData = modelList.find((item) => item.model === model?.service?.modelName);
const modelConstantsData = modelList.find((item) => item.chatModel === model.chat.chatModel);
if (!modelConstantsData) {
throw new Error('模型初始化异常');
}
// 获取提示词的向量
const { vector: promptVector, chatAPI } = await openaiCreateEmbedding({
// 获取向量匹配到的提示词
const { systemPrompts } = await searchKb_openai({
isPay: true,
apiKey,
userId,
text: prompts[prompts.length - 1].value // 取最后一个
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22,
text: prompts[prompts.length - 1].value,
modelId,
userId
});
// 相似度搜素
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: [
['status', ModelDataStatusEnum.ready],
'AND',
['model_id', model._id],
'AND',
`vector <=> '[${promptVector}]' < ${similarity}`
],
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
limit: 20
});
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
// system 合并
if (prompts[0].obj === 'SYSTEM') {
formatRedisPrompt.unshift(prompts.shift()?.value || '');
systemPrompts.unshift(prompts.shift()?.value || '');
}
/* 高相似度+退出,无法匹配时直接退出 */
if (
formatRedisPrompt.length === 0 &&
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
systemPrompts.length === 0 &&
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity
) {
return res.send('对不起,你的问题不在知识库中。');
return jsonRes(res, {
code: 500,
message: '对不起,你的问题不在知识库中。',
data: '对不起,你的问题不在知识库中。'
});
}
/* 高相似度+无上下文,不添加额外知识 */
if (
formatRedisPrompt.length === 0 &&
model.search.mode === ModelVectorSearchModeEnum.noContext
systemPrompts.length === 0 &&
model.chat.searchMode === ModelVectorSearchModeEnum.noContext
) {
prompts.unshift({
obj: 'SYSTEM',
value: model.systemPrompt
value: model.chat.systemPrompt
});
} else {
// 有匹配或者低匹配度模式情况下,添加知识库内容。
// 系统提示词过滤,最多 2500 tokens
const systemPrompt = systemPromptFilter({
model: model.service.chatModel,
prompts: formatRedisPrompt,
model: model.chat.chatModel,
prompts: systemPrompts,
maxTokens: 2500
});
prompts.unshift({
obj: 'SYSTEM',
value: `
${model.systemPrompt}
${model.chat.systemPrompt}
${
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
? `你只能从知识库选择内容回答.不在知识库内容拒绝回复`
: ''
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity ? `不回答知识库外的内容.` : ''
}
知识库内容为: 当前时间为${dayjs().format('YYYY/MM/DD HH:mm:ss')}\n${systemPrompt}'
知识库内容为: ${systemPrompt}'
`
});
}
// 控制在 tokens 数量,防止超出
const filterPrompts = openaiChatFilter({
model: model.service.chatModel,
model: model.chat.chatModel,
prompts,
maxTokens: modelConstantsData.contextMaxToken - 500
});
// console.log(filterPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
const chatAPI = getOpenAIApi(apiKey);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature,
model: model.chat.chatModel,
temperature: Number(temperature) || 0,
messages: filterPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: isStream
stream: isStream,
stop: ['.!?。']
},
{
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,
@@ -196,7 +176,7 @@ ${
pushChatBill({
isPay: true,
modelName: model.service.modelName,
chatModel: model.chat.chatModel,
userId,
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
});

View File

@@ -2,7 +2,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {

View File

@@ -2,7 +2,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { UserOpenApiKey } from '@/types/openapi';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -2,7 +2,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890');

View File

@@ -1,7 +1,7 @@
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 { authToken } from '@/service/utils/auth';
import { PaySchema, UserModelSchema } from '@/types/mongoSchema';
import dayjs from 'dayjs';
import { getPayResult } from '@/service/utils/wxpay';

View File

@@ -2,7 +2,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Bill } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import type { BillSchema } from '@/types/mongoSchema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { customAlphabet } from 'nanoid';
import { connectToDatabase, Pay } from '@/service/mongo';
import { PRICE_SCALE } from '@/constants/common';

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { connectToDatabase, Pay } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -2,7 +2,7 @@
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 { authToken } from '@/service/utils/auth';
import mongoose from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -2,7 +2,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, promotionRecord } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {

View File

@@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { User } from '@/service/models/user';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {

View File

@@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { User } from '@/service/models/user';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { authToken } from '@/service/utils/auth';
import { UserUpdateParams } from '@/types/user';
/* 更新一些基本信息 */

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, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useMemo } from 'react';
import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
import {
Box,
@@ -22,6 +22,7 @@ import { getToken } from '@/utils/user';
import MyIcon from '@/components/Icon';
import WxConcat from '@/components/WxConcat';
import { getChatHistory, delChatHistoryById } from '@/api/chat';
import { getCollectionModels } from '@/api/model';
import { modelList } from '@/constants/model';
const SlideBar = ({
@@ -45,10 +46,30 @@ const SlideBar = ({
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: 'model' as any
}));
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(() => {
if (chatId && preChatId.current === '') {
loadChatHistory();
@@ -56,8 +77,11 @@ const SlideBar = ({
preChatId.current = chatId;
}, [chatId, loadChatHistory]);
// init history
useEffect(() => {
loadChatHistory();
setTimeout(() => {
loadChatHistory();
}, 1000);
}, [loadChatHistory]);
const RenderHistory = () => (
@@ -165,9 +189,9 @@ const SlideBar = ({
{isSuccess && (
<>
<Box>
{myModels.map((item) => (
{models.map((item) => (
<Flex
key={item._id}
key={item.id}
alignItems={'center'}
p={3}
borderRadius={'md'}
@@ -178,28 +202,19 @@ const SlideBar = ({
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item._id === modelId
{...(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);
if (item.id === modelId) return;
resetChat(item.id);
onClose();
}}
>
<MyIcon
name={
modelList.find((model) => model.model === item.service.modelName)?.icon ||
'model'
}
mr={2}
color={'white'}
w={'16px'}
h={'16px'}
/>
<MyIcon name={item.icon} mr={2} color={'white'} w={'16px'} h={'16px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>

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,6 +1,5 @@
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
import { useRouter } from 'next/router';
import Image from 'next/image';
import { getInitChatSiteInfo, delChatRecordByIndex, postSaveChat } from '@/api/chat';
import type { InitChatResponse } from '@/api/response/chat';
import type { ChatItemType } from '@/types/chat';
@@ -16,12 +15,13 @@ import {
Menu,
MenuButton,
MenuList,
MenuItem
MenuItem,
Image
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import { ModelNameEnum } from '@/constants/model';
import { ChatModelEnum } from '@/constants/model';
import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global';
import { useCopyData } from '@/utils/tools';
@@ -36,6 +36,8 @@ 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 = {
@@ -55,14 +57,15 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
// 中断请求
const controller = useRef(new AbortController());
const isResetPage = useRef(false);
const [chatData, setChatData] = useState<ChatType>({
chatId,
modelId,
name: '',
avatar: '',
avatar: '/icon/logo.png',
intro: '',
chatModel: '',
modelName: '',
chatModel: ChatModelEnum.GPT35,
history: []
}); // 聊天框整体数据
@@ -152,7 +155,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
isClosable: true,
duration: 5000
});
router.replace('/model/list');
router.back();
}
setLoading(false);
return null;
@@ -164,7 +167,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const resetChat = useCallback(
async (modelId = chatData.modelId, chatId = '') => {
// 强制中断流
isResetPage.current = true;
controller.current?.abort();
try {
router.replace(`/chat?modelId=${modelId}&chatId=${chatId}`);
loadChatInfo({
@@ -187,24 +192,19 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
// gpt 对话
const gptChatPrompt = useCallback(
async (prompts: ChatSiteItemType) => {
const urlMap: Record<string, string> = {
[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({
url: urlMap[chatData.modelName],
const responseText = await streamFetch({
url: '/api/chat/chat',
data: {
prompt,
chatId,
@@ -226,22 +226,27 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
abortSignal
});
let id = '';
// 重置了页面,说明退出了当前聊天, 不缓存任何内容
if (isResetPage.current) {
return;
}
let newChatId = '';
// 保存对话信息
try {
id = await postSaveChat({
newChatId = await postSaveChat({
modelId,
chatId,
prompts: [
prompt,
{
obj: 'AI',
value: res as string
value: responseText
}
]
});
if (id) {
router.replace(`/chat?modelId=${modelId}&chatId=${id}`);
if (newChatId) {
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
}
} catch (err) {
toast({
@@ -252,10 +257,10 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
});
}
// 设置完成状态
// 设置聊天内容为完成状态
setChatData((state) => ({
...state,
chatId: id || state.chatId, // 如果有 Id说明是新创建的对话
chatId: newChatId || state.chatId, // 如果有 Id说明是新创建的对话
history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return {
@@ -265,7 +270,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
})
}));
},
[chatData.modelName, chatId, generatingMessage, modelId, router, toast]
[chatId, generatingMessage, modelId, router, toast]
);
/**
@@ -380,7 +385,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
// 更新流中断对象
useEffect(() => {
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
isResetPage.current = true;
controller.current?.abort();
};
}, []);
@@ -454,13 +459,18 @@ const Chat = ({ modelId, chatId }: { modelId: string; 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"
width={media(30, 20)}
height={media(30, 20)}
src={
item.obj === 'Human'
? '/icon/human.png'
: chatData.avatar || '/icon/logo.png'
}
alt="avatar"
w={['20px', '30px']}
maxH={'50px'}
objectFit={'contain'}
/>
</MenuButton>
<MenuList fontSize={'sm'}>
@@ -504,7 +514,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
</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')}>
@@ -559,19 +571,23 @@ const Chat = ({ modelId, chatId }: { modelId: string; 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
@@ -579,8 +595,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
width={['18px', '20px']}
height={['18px', '20px']}
cursor={'pointer'}
fill={useColorModeValue('#718096', 'white')}
></Icon>
color={useColorModeValue('gray.500', 'white')}
onClick={sendPrompt}
/>
)}
</Flex>
</Box>

View File

@@ -22,9 +22,9 @@ const Home = () => {
<Card p={5} mt={4} textAlign={'center'}>
<Box>
{/* <Link href="https://beian.miit.gov.cn/" target="_blank">
浙B2-20080101
</Link> */}
<Link href="https://beian.miit.gov.cn/" target="_blank">
ICP备2023011255号-1
</Link>
</Box>
<Box>Made by FastGpt Team.</Box>
</Card>

View File

@@ -8,6 +8,7 @@ import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
import { useToast } from '@/hooks/useToast';
import { useRouter } from 'next/router';
import { postCreateModel } from '@/api/model';
interface Props {
loginSuccess: (e: ResLogin) => void;
@@ -64,6 +65,10 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
title: `注册成功`,
status: 'success'
});
// aut register a model
postCreateModel({
name: '模型1'
});
} catch (error: any) {
toast({
title: error.message || '注册异常',

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}` }) {

View File

@@ -119,31 +119,27 @@ const InputDataModal = ({
pb={2}
>
<Box flex={2} mr={[0, 4]} mb={[4, 0]} h={['230px', '100%']}>
<Box h={'30px'}></Box>
<Box h={'30px'}>{'匹配的知识点'}</Box>
<Textarea
placeholder={
'相关问题,可以输入多个问法, 最多 1000 字。例如:\n1. laf 是什么?\n2. laf 可以做什么?\n3. laf怎么用'
}
maxLength={1000}
placeholder={'匹配的知识点。这部分内容会被搜索,请把控内容的质量。最多 1000 字。'}
maxLength={2000}
resize={'none'}
h={'calc(100% - 30px)'}
{...register(`q`, {
required: '相关问题,可以回车输入多个问法'
required: true
})}
/>
</Box>
<Box flex={3} h={['330px', '100%']}>
<Box h={'30px'}></Box>
<Box h={'30px'}></Box>
<Textarea
placeholder={
'知识点,最多 2000 字。例如:\n1. laf是一个云函数开发平台。\n2. laf 什么都能做。\n3. 下面是使用 laf 的例子: ……'
'补充知识。这部分内容不会被搜索,但会作为"匹配的知识点"的内容补充,你可以讲一些细节的内容填写在这里。最多 2000 字。'
}
maxLength={2000}
resize={'none'}
h={'calc(100% - 30px)'}
{...register(`a`, {
required: '知识点'
})}
{...register('a')}
/>
</Box>
</Box>

View File

@@ -39,15 +39,16 @@ import InputModal, { FormData as InputDataType } from './InputDataModal';
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
const ModelDataCard = ({ modelId }: { modelId: string }) => {
const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean }) => {
const { Loading, setIsLoading } = useLoading();
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
const tdStyles = useRef<BoxProps>({
fontSize: 'xs',
minW: '150px',
maxW: '500px',
whiteSpace: 'pre-wrap',
maxH: '250px',
whiteSpace: 'pre-wrap',
overflowY: 'auto'
});
const {
@@ -93,10 +94,16 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
(num = 1) => {
getData(num);
refetch();
return null;
},
[getData, refetch]
);
useQuery(['refetchData'], () => refetchData(pageNum), {
refetchInterval: 5000,
enabled: splitDataLen > 0
});
// 获取所有的数据,并导出 json
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({
mutationFn: () => getExportDataList(modelId),
@@ -126,51 +133,54 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<>
<Flex>
<Box fontWeight={'bold'} fontSize={'lg'} flex={1} mr={2}>
: {total}
<Box as={'span'} fontSize={'sm'}>
</Box>
: {total}
</Box>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={4}
size={'sm'}
onClick={() => refetchData(pageNum)}
/>
<Button
variant={'outline'}
mr={2}
size={'sm'}
isLoading={isLoadingExport}
title={'换行数据导出时,会进行格式转换'}
onClick={() => onclickExport()}
>
</Button>
<Menu autoSelect={false}>
<MenuButton as={Button} size={'sm'}>
</MenuButton>
<MenuList>
<MenuItem
onClick={() =>
setEditInputData({
a: '',
q: ''
})
}
{isOwner && (
<>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={4}
size={'sm'}
onClick={() => refetchData(pageNum)}
/>
<Button
variant={'outline'}
mr={2}
size={'sm'}
isLoading={isLoadingExport}
title={'换行数据导出时,会进行格式转换'}
onClick={() => onclickExport()}
>
</MenuItem>
<MenuItem onClick={onOpenSelectFileModal}>/</MenuItem>
<MenuItem onClick={onOpenSelectCsvModal}>csv </MenuItem>
</MenuList>
</Menu>
</Button>
<Menu autoSelect={false}>
<MenuButton as={Button} size={'sm'}>
</MenuButton>
<MenuList>
<MenuItem
onClick={() =>
setEditInputData({
a: '',
q: ''
})
}
>
</MenuItem>
<MenuItem onClick={onOpenSelectFileModal}>/</MenuItem>
<MenuItem onClick={onOpenSelectCsvModal}>csv </MenuItem>
</MenuList>
</Menu>
</>
)}
</Flex>
<Flex mt={4}>
{splitDataLen > 0 && <Box fontSize={'xs'}>{splitDataLen}...</Box>}
{isOwner && splitDataLen > 0 && (
<Box fontSize={'xs'}>{splitDataLen}...</Box>
)}
<Box flex={1} />
<Input
maxW={'240px'}
@@ -198,10 +208,10 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<Table variant={'simple'} w={'100%'}>
<Thead>
<Tr>
<Th>{'匹配内容(问题)'}</Th>
<Th></Th>
<Th>{'匹配的知识点'}</Th>
<Th></Th>
<Th></Th>
<Th></Th>
{isOwner && <Th></Th>}
</Tr>
</Thead>
<Tbody>
@@ -214,33 +224,35 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<Box {...tdStyles.current}>{item.a || '-'}</Box>
</Td>
<Td>{ModelDataStatusMap[item.status]}</Td>
<Td>
<IconButton
mr={5}
icon={<EditIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
onClick={() =>
setEditInputData({
dataId: item.id,
q: item.q,
a: item.a
})
}
/>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
await delOneModelData(item.id);
refetchData(pageNum);
}}
/>
</Td>
{isOwner && (
<Td>
<IconButton
mr={5}
icon={<EditIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
onClick={() =>
setEditInputData({
dataId: item.id,
q: item.q,
a: item.a
})
}
/>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
await delOneModelData(item.id);
refetchData(pageNum);
}}
/>
</Td>
)}
</Tr>
))}
</Tbody>

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import {
Box,
Card,
@@ -13,22 +13,28 @@ import {
SliderMark,
Tooltip,
Button,
Select
Select,
Grid,
Switch,
Image
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { ModelSchema } from '@/types/mongoSchema';
import { UseFormReturn } from 'react-hook-form';
import { modelList, ModelVectorSearchModeMap } from '@/constants/model';
import { ChatModelMap, modelList, ModelVectorSearchModeMap } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import { useConfirm } from '@/hooks/useConfirm';
import { useSelectFile } from '@/hooks/useSelectFile';
import { useToast } from '@/hooks/useToast';
import { fileToBase64 } from '@/utils/file';
const ModelEditForm = ({
formHooks,
canTrain,
isOwner,
handleDelModel
}: {
formHooks: UseFormReturn<ModelSchema>;
canTrain: boolean;
isOwner: boolean;
handleDelModel: () => void;
}) => {
const { openConfirm, ConfirmChild } = useConfirm({
@@ -36,12 +42,55 @@ const ModelEditForm = ({
});
const { register, setValue, getValues } = formHooks;
const [refresh, setRefresh] = useState(false);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const { toast } = useToast();
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
if (file.size > 100 * 1024) {
return toast({
title: '头像需小于 100kb',
status: 'warning'
});
}
const base64 = (await fileToBase64(file)) as string;
setValue('avatar', base64);
setRefresh((state) => !state);
},
[setValue, toast]
);
return (
<>
<Card p={4}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<Box fontWeight={'bold'}></Box>
<Box fontWeight={'bold'}></Box>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 80px'} w={0}>
modelId:
</Box>
<Box>{getValues('_id')}</Box>
</Flex>
<Flex mt={4} alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Image
src={getValues('avatar') || '/icon/logo.png'}
alt={'avatar'}
w={['28px', '36px']}
h={['28px', '36px']}
objectFit={'cover'}
cursor={isOwner ? 'pointer' : 'default'}
title={'点击切换头像'}
onClick={() => isOwner && onOpenSelectFile()}
/>
</Flex>
<FormControl mt={4}>
<Flex alignItems={'center'}>
@@ -49,23 +98,19 @@ const ModelEditForm = ({
:
</Box>
<Input
isDisabled={!isOwner}
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
modelId:
</Box>
<Box>{getValues('_id')}</Box>
</Flex>
</FormControl>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
:
:
</Box>
<Box>{modelList.find((item) => item.model === getValues('service.modelName'))?.name}</Box>
<Box>{ChatModelMap[getValues('chat.chatModel')]}</Box>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
@@ -73,23 +118,31 @@ const ModelEditForm = ({
</Box>
<Box>
{formatPrice(
modelList.find((item) => item.model === getValues('service.modelName'))?.price || 0,
modelList.find((item) => item.chatModel === getValues('chat.chatModel'))?.price || 0,
1000
)}
/1K tokens()
</Box>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 150px'}></Box>
<Button
colorScheme={'gray'}
variant={'outline'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Box>{getValues('share.collection')}</Box>
</Flex>
{isOwner && (
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 150px'}></Box>
<Button
colorScheme={'gray'}
variant={'outline'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
</Flex>
)}
</Card>
<Card p={4}>
<Box fontWeight={'bold'}></Box>
@@ -109,14 +162,15 @@ const ModelEditForm = ({
min={0}
max={10}
step={1}
value={getValues('temperature')}
value={getValues('chat.temperature')}
isDisabled={!isOwner}
onChange={(e) => {
setValue('temperature', e);
setValue('chat.temperature', e);
setRefresh(!refresh);
}}
>
<SliderMark
value={getValues('temperature')}
value={getValues('chat.temperature')}
textAlign="center"
bg="blue.500"
color="white"
@@ -126,7 +180,7 @@ const ModelEditForm = ({
fontSize={'xs'}
transform={'translate(-50%, -200%)'}
>
{getValues('temperature')}
{getValues('chat.temperature')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack />
@@ -135,34 +189,103 @@ const ModelEditForm = ({
</Slider>
</Flex>
</FormControl>
{canTrain && (
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Select {...register('search.mode', { required: '搜索模式不能为空' })}>
{Object.entries(ModelVectorSearchModeMap).map(([key, { text }]) => (
<option key={key} value={key}>
{text}
</option>
))}
</Select>
</Flex>
</FormControl>
<Flex mt={4} alignItems={'center'}>
<Box mr={4}></Box>
<Switch
isChecked={getValues('chat.useKb')}
onChange={() => {
setValue('chat.useKb', !getValues('chat.useKb'));
setRefresh(!refresh);
}}
/>
</Flex>
{getValues('chat.useKb') && (
<Flex mt={4} alignItems={'center'}>
<Box mr={4} whiteSpace={'nowrap'}>
&emsp;
</Box>
<Select
isDisabled={!isOwner}
{...register('chat.searchMode', { required: '搜索模式不能为空' })}
>
{Object.entries(ModelVectorSearchModeMap).map(([key, { text }]) => (
<option key={key} value={key}>
{text}
</option>
))}
</Select>
</Flex>
)}
<Box mt={4}>
<Box mb={1}></Box>
<Textarea
rows={6}
rows={8}
maxLength={-1}
{...register('systemPrompt')}
placeholder={
canTrain
? '训练的模型会根据知识库内容,生成一部分系统提示词,因此在对话时需要消耗更多的 tokens。你可以增加提示词让效果更符合预期。例如: \n1. 请根据知识库内容回答用户问题。\n2. 知识库是电影《铃芽之旅》的内容,根据知识库内容回答。无关问题,拒绝回复!'
: '模型默认的 prompt 词,通过调整该内容,可以生成一个限定范围的模型。\n注意改功能会影响对话的整体朝向'
}
isDisabled={!isOwner}
placeholder={'模型默认的 prompt 词,通过调整该内容,可以引导模型聊天方向。'}
{...register('chat.systemPrompt')}
/>
</Box>
</Card>
{isOwner && (
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<Box fontWeight={'bold'}></Box>
<Grid gridTemplateColumns={['1fr', '1fr 410px']} gridGap={5}>
<Box>
<Flex mt={5} alignItems={'center'}>
<Box mr={3}>:</Box>
<Switch
isChecked={getValues('share.isShare')}
onChange={() => {
setValue('share.isShare', !getValues('share.isShare'));
setRefresh(!refresh);
}}
/>
<Box ml={12} mr={3}>
:
</Box>
<Switch
isChecked={getValues('share.isShareDetail')}
onChange={() => {
setValue('share.isShareDetail', !getValues('share.isShareDetail'));
setRefresh(!refresh);
}}
/>
</Flex>
<Box mt={5}>
<Box></Box>
<Textarea
mt={1}
rows={6}
maxLength={150}
{...register('share.intro')}
placeholder={'介绍模型的功能、场景等吸引更多人来使用最多150字。'}
/>
</Box>
</Box>
<Box
textAlign={'justify'}
fontSize={'sm'}
border={'1px solid #f4f4f4'}
borderRadius={'sm'}
p={3}
>
<Box fontWeight={'bold'}>Tips</Box>
<Box mt={1} as={'ul'} pl={4}>
<li>
FastGpt
使使 tokens使 tokens
</li>
<li></li>
</Box>
</Box>
</Grid>
</Card>
)}
<File onSelect={onSelectFile} />
{/* <Card p={4}>
<Box fontWeight={'bold'}>安全策略</Box>
<FormControl mt={2}>

View File

@@ -62,36 +62,38 @@ const SelectFileModal = ({
const { openConfirm, ConfirmChild } = useConfirm({
content: `确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,未完成的任务会被直接清除。一共 ${
splitRes.chunks.length
} 组,大约 ${splitRes.tokens} 个tokens, 约 ${formatPrice(
} 组,大约 ${splitRes.tokens || '数量太多,未计算'} 个tokens, 约 ${formatPrice(
splitRes.tokens * modeMap[mode].price
)}`
});
const fileText = useMemo(() => fileTextArr.join(''), [fileTextArr]);
const onSelectFile = useCallback(
async (e: File[]) => {
setSelecting(true);
try {
const fileTexts = await Promise.all(
e.map((file) => {
// @ts-ignore
const extension = file?.name?.split('.').pop().toLowerCase();
let promise = Promise.resolve();
e.map((file) => {
promise = promise.then(async () => {
const extension = file?.name?.split('.')?.pop()?.toLowerCase();
let text = '';
switch (extension) {
case 'txt':
case 'md':
return readTxtContent(file);
text = await readTxtContent(file);
break;
case 'pdf':
return readPdfContent(file);
text = await readPdfContent(file);
break;
case 'doc':
case 'docx':
return readDocContent(file);
default:
return '';
text = await readDocContent(file);
break;
}
})
);
setFileTextArr(fileTexts);
text && setFileTextArr((state) => [text].concat(state));
return;
});
});
await promise;
} catch (error: any) {
console.log(error);
toast({
@@ -131,6 +133,7 @@ const SelectFileModal = ({
const onclickImport = useCallback(() => {
const chunks = fileTextArr
.filter((item) => item)
.map((item) =>
splitText({
text: item,
@@ -138,10 +141,15 @@ const SelectFileModal = ({
})
)
.flat();
// count tokens
const tokens = chunks.map((item) =>
countChatTokens({ messages: [{ role: 'system', content: item }] })
);
let tokens: number[] = [];
// just count 100 sets of tokens
if (chunks.length < 100) {
tokens = chunks.map((item) =>
countChatTokens({ messages: [{ role: 'system', content: item }] })
);
}
setSplitRes({
tokens: tokens.reduce((sum, item) => sum + item, 0),
@@ -169,7 +177,7 @@ const SelectFileModal = ({
>
<Box mt={2} px={5} maxW={['100%', '70%']} textAlign={'justify'} color={'blackAlpha.600'}>
{fileExtension} QA
tokens
tokens{fileTextArr.length}
</Box>
{/* 拆分模式 */}
<Flex w={'100%'} px={5} alignItems={'center'} mt={4}>
@@ -200,11 +208,11 @@ const SelectFileModal = ({
)}
{/* 文本内容 */}
<Box flex={'1 0 0'} px={5} h={0} w={'100%'} overflowY={'auto'} mt={4}>
{fileTextArr.map((item, i) => (
{fileTextArr.slice(0, 100).map((item, i) => (
<Box key={i} mb={5}>
<Box mb={1}>{i + 1}</Box>
<Textarea
placeholder="文件内容"
placeholder="文件内容,空内容会自动忽略"
maxLength={-1}
rows={10}
fontSize={'xs'}
@@ -231,7 +239,11 @@ const SelectFileModal = ({
<Button variant={'outline'} colorScheme={'gray'} mr={3} onClick={onClose}>
</Button>
<Button isLoading={isLoading} isDisabled={fileText === ''} onClick={onclickImport}>
<Button
isLoading={isLoading}
isDisabled={selecting || fileTextArr[0] === ''}
onClick={onclickImport}
>
</Button>
</Flex>

View File

@@ -1,70 +0,0 @@
import React, { useEffect, useCallback, useState } from 'react';
import { Box, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import type { ModelSchema } from '@/types/mongoSchema';
import { getModelTrainings } from '@/api/model';
import type { TrainingItemType } from '@/types/training';
const Training = ({ model }: { model: ModelSchema }) => {
const columns: {
title: string;
key: keyof TrainingItemType;
dataIndex: string;
}[] = [
{
title: '训练ID',
key: 'tuneId',
dataIndex: 'tuneId'
},
{
title: '状态',
key: 'status',
dataIndex: 'status'
}
];
const [records, setRecords] = useState<TrainingItemType[]>([]);
const loadTrainingRecords = useCallback(async (id: string) => {
try {
const res = await getModelTrainings(id);
setRecords(res);
} catch (error) {
console.log('error->', error);
}
}, []);
useEffect(() => {
model._id && loadTrainingRecords(model._id);
}, [loadTrainingRecords, model]);
return (
<>
<Box fontWeight={'bold'} fontSize={'lg'}>
: {model.trainingTimes}
</Box>
<TableContainer mt={4}>
<Table variant={'simple'}>
<Thead>
<Tr>
{columns.map((item) => (
<Th key={item.key}>{item.title}</Th>
))}
</Tr>
</Thead>
<Tbody>
{records.map((item) => (
<Tr key={item._id}>
{columns.map((col) => (
// @ts-ignore
<Td key={col.key}>{item[col.dataIndex]}</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</>
);
};
export default Training;

View File

@@ -1,15 +1,16 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { useRouter } from 'next/router';
import { getModelById, delModelById, putModelTrainingStatus, putModelById } from '@/api/model';
import { getModelById, delModelById, putModelById } from '@/api/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useForm } from 'react-hook-form';
import { formatModelStatus, ModelStatusEnum, modelList, defaultModel } from '@/constants/model';
import { formatModelStatus, modelList, defaultModel } from '@/constants/model';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/store/user';
const ModelEditForm = dynamic(() => import('./components/ModelEditForm'));
const ModelDataCard = dynamic(() => import('./components/ModelDataCard'));
@@ -18,6 +19,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { isPc } = useScreen();
const { userInfo } = useUserStore();
const { setLoading } = useGlobalStore();
const [model, setModel] = useState<ModelSchema>(defaultModel);
@@ -25,26 +27,24 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
defaultValues: model
});
const canTrain = useMemo(() => {
const openai = modelList.find((item) => item.model === model?.service.modelName);
return !!(openai && openai.trainName);
}, [model]);
const isOwner = useMemo(() => model.userId === userInfo?._id, [model.userId, userInfo?._id]);
/* 加载模型数据 */
const loadModel = useCallback(async () => {
setLoading(true);
try {
const res = await getModelById(modelId);
// console.log(res);
res.security.expiredTime /= 60 * 60 * 1000;
setModel(res);
formHooks.reset(res);
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err?.message || '获取模型异常',
status: 'error'
});
}
setLoading(false);
return null;
}, [formHooks, modelId, setLoading]);
}, [formHooks, modelId, setLoading, toast]);
useQuery([modelId], loadModel);
@@ -59,45 +59,19 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
status: 'success'
});
router.replace('/model/list');
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setLoading(false);
}, [setLoading, model, router, toast]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
setLoading(true);
try {
router.push(`/chat?modelId=${modelId}`);
} catch (err) {
console.log('error->', err);
}
setLoading(false);
}, [setLoading, router, modelId]);
/* 点击更新模型状态 */
const handleClickUpdateStatus = useCallback(async () => {
if (!model || model.status !== ModelStatusEnum.training) return;
setLoading(true);
try {
const res = await putModelTrainingStatus(model._id);
typeof res === 'string' &&
toast({
title: res,
status: 'info'
});
loadModel();
} catch (error: any) {
console.log('error->', error);
toast({
title: error.message || '更新失败',
status: 'error'
});
}
setLoading(false);
}, [model, setLoading, loadModel, toast]);
router.push(`/chat?modelId=${modelId}`);
}, [router, modelId]);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
@@ -106,22 +80,19 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
try {
await putModelById(data._id, {
name: data.name,
systemPrompt: data.systemPrompt,
intro: data.intro,
temperature: data.temperature,
search: data.search,
service: data.service,
avatar: data.avatar || '/icon/logo.png',
chat: data.chat,
share: data.share,
security: data.security
});
toast({
title: '更新成功',
status: 'success'
});
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err as string,
status: 'success'
title: err?.message || '更新失败',
status: 'error'
});
}
setLoading(false);
@@ -168,22 +139,18 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
<Box fontSize={'xl'} fontWeight={'bold'}>
{model.name}
</Box>
<Tag
ml={2}
variant="solid"
colorScheme={formatModelStatus[model.status].colorTheme}
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
onClick={handleClickUpdateStatus}
>
<Tag ml={2} variant="solid" colorScheme={formatModelStatus[model.status].colorTheme}>
{formatModelStatus[model.status].text}
</Tag>
<Box flex={1} />
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
{isOwner && (
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
)}
</Flex>
) : (
<>
@@ -196,22 +163,28 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
</Tag>
</Flex>
<Box mt={4} textAlign={'right'}>
<Button variant={'outline'} onClick={handlePreviewChat}>
<Button variant={'outline'} size={'sm'} onClick={handlePreviewChat}>
</Button>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
{isOwner && (
<Button
ml={4}
size={'sm'}
onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}
>
</Button>
)}
</Box>
</>
)}
</Card>
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={5}>
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} canTrain={canTrain} />
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} isOwner={isOwner} />
{canTrain && !!model._id && (
{modelId && (
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<ModelDataCard modelId={model._id} />
<ModelDataCard modelId={modelId} isOwner={isOwner} />
</Card>
)}
</Grid>

View File

@@ -1,138 +0,0 @@
import React, { Dispatch, useState, useCallback, useMemo } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
FormControl,
FormErrorMessage,
Button,
useToast,
Input,
Select,
Box
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { postCreateModel } from '@/api/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { modelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
interface CreateFormType {
name: string;
serviceModelName: string;
}
const CreateModel = ({
setCreateModelOpen,
onSuccess
}: {
setCreateModelOpen: Dispatch<boolean>;
onSuccess: Dispatch<ModelSchema>;
}) => {
const [requesting, setRequesting] = useState(false);
const [refresh, setRefresh] = useState(false);
const toast = useToast({
duration: 2000,
position: 'top'
});
const {
getValues,
register,
handleSubmit,
formState: { errors }
} = useForm<CreateFormType>({
defaultValues: {
serviceModelName: modelList[0].model
}
});
const handleCreateModel = useCallback(
async (data: CreateFormType) => {
setRequesting(true);
try {
const res = await postCreateModel(data);
toast({
title: '创建成功',
status: 'success'
});
onSuccess(res);
setCreateModelOpen(false);
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err.message || '出现了意外',
status: 'error'
});
}
setRequesting(false);
},
[onSuccess, setCreateModelOpen, toast]
);
return (
<>
<Modal isOpen={true} onClose={() => setCreateModelOpen(false)}>
<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 isInvalid={!!errors.serviceModelName}>
<Select
placeholder="选择基础模型类型"
{...register('serviceModelName', {
required: '底层模型不能为空',
onChange() {
setRefresh(!refresh);
}
})}
>
{modelList.map((item) => (
<option key={item.model} value={item.model}>
{item.name}
</option>
))}
</Select>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.serviceModelName && errors.serviceModelName.message}
</FormErrorMessage>
</FormControl>
<Box mt={3} textAlign={'center'} fontSize={'sm'} color={'blackAlpha.600'}>
{formatPrice(
modelList.find((item) => item.model === getValues('serviceModelName'))?.price || 0,
1000
)}
/1K tokens()
</Box>
</ModalBody>
<ModalFooter>
<Button mr={3} colorScheme={'gray'} onClick={() => setCreateModelOpen(false)}>
</Button>
<Button isLoading={requesting} onClick={handleSubmit(handleCreateModel)}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
export default CreateModel;

View File

@@ -2,8 +2,8 @@ import React, { useEffect } from 'react';
import { Box, Button, Flex, Tag } from '@chakra-ui/react';
import type { ModelSchema } from '@/types/mongoSchema';
import { formatModelStatus } from '@/constants/model';
import dayjs from 'dayjs';
import { useRouter } from 'next/router';
import { ChatModelMap } from '@/constants/model';
const ModelPhoneList = ({
models,
@@ -42,12 +42,12 @@ const ModelPhoneList = ({
</Tag>
</Flex>
<Flex mt={5}>
<Box flex={'0 0 100px'}>: </Box>
<Box color={'blackAlpha.500'}>{dayjs(model.updateTime).format('YYYY-MM-DD HH:mm')}</Box>
<Box flex={'0 0 100px'}>: </Box>
<Box color={'blackAlpha.500'}>{ChatModelMap[model.chat.chatModel]}</Box>
</Flex>
<Flex mt={5}>
<Box flex={'0 0 100px'}>AI模型: </Box>
<Box color={'blackAlpha.500'}>{model.service.modelName}</Box>
<Box flex={'0 0 100px'}>: </Box>
<Box color={'blackAlpha.500'}>{model.chat.temperature}</Box>
</Flex>
<Flex mt={5} justifyContent={'flex-end'}>
<Button

View File

@@ -13,10 +13,9 @@ import {
Box
} from '@chakra-ui/react';
import { formatModelStatus } from '@/constants/model';
import dayjs from 'dayjs';
import type { ModelSchema } from '@/types/mongoSchema';
import { useRouter } from 'next/router';
import { modelList } from '@/constants/model';
import { ChatModelMap } from '@/constants/model';
const ModelTable = ({
models = [],
@@ -33,18 +32,18 @@ const ModelTable = ({
dataIndex: 'name'
},
{
title: '模型类型',
title: '对话模型',
key: 'service',
render: (model: ModelSchema) => (
<Box fontWeight={'bold'} whiteSpace={'pre-wrap'} maxW={'200px'}>
{modelList.find((item) => item.model === model.service.modelName)?.name}
{ChatModelMap[model.chat.chatModel]}
</Box>
)
},
{
title: '最后更新时间',
key: 'updateTime',
render: (item: ModelSchema) => dayjs(item.updateTime).format('YYYY-MM-DD HH:mm')
title: '温度',
key: 'temperature',
render: (model: ModelSchema) => <>{model.chat.temperature}</>
},
{
title: '状态',

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useCallback } from 'react';
import { Box, Button, Flex, Card } from '@chakra-ui/react';
import type { ModelSchema } from '@/types/mongoSchema';
import { useRouter } from 'next/router';
@@ -7,30 +7,37 @@ import ModelPhoneList from './components/ModelPhoneList';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import dynamic from 'next/dynamic';
import { useToast } from '@/hooks/useToast';
import { useUserStore } from '@/store/user';
const CreateModel = dynamic(() => import('./components/CreateModel'));
import { postCreateModel } from '@/api/model';
const modelList = () => {
const { toast } = useToast();
const { isPc } = useScreen();
const router = useRouter();
const { myModels, setMyModels, getMyModels } = useUserStore();
const [openCreateModel, setOpenCreateModel] = useState(false);
const { myModels, getMyModels } = useUserStore();
const { Loading, setIsLoading } = useLoading();
/* 加载模型 */
const { isLoading } = useQuery(['loadModels'], getMyModels);
/* 创建成功回调 */
const createModelSuccess = useCallback(
(data: ModelSchema) => {
setMyModels([data, ...myModels]);
},
[myModels, setMyModels]
);
const handleCreateModel = useCallback(async () => {
setIsLoading(true);
try {
const id = await postCreateModel({ name: `模型${myModels.length}` });
toast({
title: '创建成功',
status: 'success'
});
router.push(`/model/detail?modelId=${id}`);
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err.message || '出现了意外',
status: 'error'
});
}
setIsLoading(false);
}, [myModels.length, router, setIsLoading, toast]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(
@@ -61,7 +68,7 @@ const modelList = () => {
</Box>
<Button flex={'0 0 145px'} variant={'outline'} onClick={() => setOpenCreateModel(true)}>
<Button flex={'0 0 145px'} variant={'outline'} onClick={handleCreateModel}>
</Button>
</Flex>
@@ -74,10 +81,6 @@ const modelList = () => {
<ModelPhoneList models={myModels} handlePreviewChat={handlePreviewChat} />
)}
</Box>
{/* 创建弹窗 */}
{openCreateModel && (
<CreateModel setCreateModelOpen={setOpenCreateModel} onSuccess={createModelSuccess} />
)}
<Loading loading={isLoading} />
</Box>

View File

@@ -0,0 +1,83 @@
import React from 'react';
import { Box, Flex, Image, Button } from '@chakra-ui/react';
import type { ShareModelItem } from '@/types/model';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon';
import styles from '../index.module.scss';
const ShareModelList = ({
models = [],
onclickCollection
}: {
models: ShareModelItem[];
onclickCollection: (modelId: string) => void;
}) => {
const router = useRouter();
return (
<>
{models.map((model) => (
<Box
key={model._id}
p={4}
border={'1px solid'}
borderColor={'gray.200'}
borderRadius={'md'}
>
<Flex alignItems={'center'}>
<Image
src={model.avatar}
alt={'avatar'}
w={['28px', '36px']}
h={['28px', '36px']}
objectFit={'cover'}
/>
<Box fontWeight={'bold'} fontSize={'lg'} ml={5}>
{model.name}
</Box>
</Flex>
<Box className={styles.intro} my={4} fontSize={'sm'} color={'blackAlpha.600'}>
{model.share.intro || '这个模型没有介绍~'}
</Box>
<Flex justifyContent={'space-between'}>
<Flex
alignItems={'center'}
cursor={'pointer'}
color={model.isCollection ? 'blue.600' : 'alphaBlack.700'}
onClick={() => onclickCollection(model._id)}
>
<MyIcon
mr={1}
name={model.isCollection ? 'collectionSolid' : 'collectionLight'}
w={'16px'}
/>
{model.share.collection}
</Flex>
<Box>
<Button
size={'sm'}
variant={'outline'}
w={'80px'}
onClick={() => router.push(`/chat?modelId=${model._id}`)}
>
</Button>
{model.share.isShareDetail && (
<Button
ml={4}
size={'sm'}
w={'80px'}
onClick={() => router.push(`/model/detail?modelId=${model._id}`)}
>
</Button>
)}
</Box>
</Flex>
</Box>
))}
</>
);
};
export default ShareModelList;

View File

@@ -0,0 +1,7 @@
.intro {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}

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