Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be69cfb966 | ||
|
|
6244f6c1fb | ||
|
|
e12f97a73b | ||
|
|
7f96c4ff9b | ||
|
|
138b607ac7 | ||
|
|
b204c55bd1 | ||
|
|
a7b9940d7a | ||
|
|
7db87c2d09 | ||
|
|
f023f63103 | ||
|
|
e5fe670a6e | ||
|
|
7807b26707 | ||
|
|
16775430ea | ||
|
|
dd5217d8a5 | ||
|
|
9f8d696bbe | ||
|
|
bf81d23de4 | ||
|
|
52a752dab5 | ||
|
|
78903baefa | ||
|
|
45ad3ba22a | ||
|
|
c03a7db633 | ||
|
|
2cc32d1806 |
6
Makefile
6
Makefile
@@ -1,4 +1,4 @@
|
||||
SERVICE_NAME=doc-gpt
|
||||
SERVICE_NAME=fast-gpt
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= $(SERVICE_NAME):latest
|
||||
|
||||
@@ -34,13 +34,13 @@ run: ## Run a dev service from host.
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build: ## Build docker image with the desktop-frontend.
|
||||
docker build -t c121914yu/doc-gpt:latest .
|
||||
docker build -t c121914yu/fast-gpt:latest .
|
||||
|
||||
##@ Deployment
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run: ## Push docker image.
|
||||
docker run -d -p 8008:3000 --name doc-gpt -v /web_project/yjl/doc-gpt/logs:/app/.next/logs c121914yu/doc-gpt:latest
|
||||
docker run -d -p 8008:3000 --name fast-gpt -v /web_project/yjl/fast-gpt/logs:/app/.next/logs c121914yu/fast-gpt:latest
|
||||
|
||||
#TODO: add support of docker push
|
||||
|
||||
|
||||
108
README.md
108
README.md
@@ -8,9 +8,9 @@ Fast GPT 允许你是用自己的 openai API KEY 来快速的调用 openai 接
|
||||
```
|
||||
AXIOS_PROXY_HOST=axios代理地址,目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
|
||||
AXIOS_PROXY_PORT=代理端口
|
||||
MONGODB_URI=mongo数据库地址
|
||||
MONGODB_URI=mongo数据库地址(例如:mongodb://username:password@ip:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false)
|
||||
MY_MAIL=发送验证码邮箱
|
||||
MAILE_CODE=邮箱秘钥
|
||||
MAILE_CODE=邮箱秘钥(代理里设置的是QQ邮箱,不知道怎么找这个 code 的,可以百度搜"nodemailer发送邮件")
|
||||
TOKEN_KEY=随便填一个,用于生成和校验token
|
||||
```
|
||||
|
||||
@@ -21,23 +21,22 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
||||
|
||||
## 部署
|
||||
|
||||
### docker 模式
|
||||
请准备好 docker, mongo,代理, 和nginx。 镜像走本机的代理,所以用 network=host,port 改成代理的端口,clash 一般都是 7890。
|
||||
|
||||
#### docker 打包
|
||||
```bash
|
||||
# 本地 docker 打包
|
||||
docker build -t imageName:tag .
|
||||
docker push imageName:tag
|
||||
```
|
||||
|
||||
服务器请准备好 docker, mongo,nginx和代理。 镜像走本机的代理,所以用 host,port改成代理的端口,clash一般都是7890。
|
||||
|
||||
#### 服务器拉取镜像和运行
|
||||
```bash
|
||||
# 服务器拉取部署, imageName 替换成镜像名
|
||||
docker pull imageName:tag
|
||||
# 获取本地旧镜像ID
|
||||
OLD_IMAGE_ID=$(docker images imageName -f "dangling=true" -q)
|
||||
docker stop doc-gpt || true
|
||||
docker rm doc-gpt || true
|
||||
docker run -d --network=host --name doc-gpt \
|
||||
-e MAX_USER=50 \
|
||||
docker stop fast-gpt || true
|
||||
docker rm fast-gpt || true
|
||||
docker run -d --network=host --name fast-gpt \
|
||||
-e AXIOS_PROXY_HOST=127.0.0.1 \
|
||||
-e AXIOS_PROXY_PORT=7890 \
|
||||
-e MY_MAIL=your email\
|
||||
@@ -45,23 +44,16 @@ docker run -d --network=host --name doc-gpt \
|
||||
-e TOKEN_KEY=任意一个内容 \
|
||||
-e MONGODB_URI="mongodb://user:password@127.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false" \
|
||||
imageName:tag
|
||||
docker logs doc-gpt
|
||||
|
||||
|
||||
# 删除本地旧镜像
|
||||
if [ ! -z "$OLD_IMAGE_ID" ]; then
|
||||
docker rmi $OLD_IMAGE_ID
|
||||
fi
|
||||
```
|
||||
|
||||
### docker 安装
|
||||
#### 软件教程:docker 安装
|
||||
```bash
|
||||
# 安装docker
|
||||
curl -sSL https://get.daocloud.io/docker | sh
|
||||
sudo systemctl start docker
|
||||
```
|
||||
|
||||
### mongo 安装
|
||||
#### 软件教程:mongo 安装
|
||||
```bash
|
||||
docker pull mongo:6.0.4
|
||||
docker stop mongo
|
||||
@@ -71,45 +63,41 @@ docker run -d --name mongo \
|
||||
-e MONGO_INITDB_ROOT_PASSWORD= \
|
||||
-v /root/service/mongo:/data/db \
|
||||
mongo:6.0.4
|
||||
|
||||
# 检查 mongo 运行情况, 有成功的 logs 代表访问成功
|
||||
docker logs mongo
|
||||
```
|
||||
#### 软件教程: 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"
|
||||
```
|
||||
|
||||
|
||||
# 介绍页
|
||||
|
||||
## 欢迎使用 Fast GPT
|
||||
|
||||
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
||||
|
||||
1. 使用邮箱注册账号。
|
||||
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
||||
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
||||
4. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
||||
|
||||
### 模型配置
|
||||
|
||||
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。
|
||||
|
||||
|
||||
2. **单句最大长度**:每个聊天,单次输入内容的最大长度。
|
||||
|
||||
|
||||
3. **上下文最大长度**:每个聊天,最多的轮数除以2,建议设置为偶数。可以持续聊天,但是旧的聊天内容会被截断,AI 就不会知道被截取的内容。
|
||||
例如:上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。
|
||||
|
||||
4. **过期时间**:生成对话框后,这个对话框多久过期。
|
||||
|
||||
5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制,正数代表只能加载 n 次,防止被盗刷。
|
||||
|
||||
### 对话框介绍
|
||||
|
||||
1. 每个对话框以 windowId 作为标识。
|
||||
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。
|
||||
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。
|
||||
4. 如果想分享一个纯的对话框,可以把链接里 windowId 参数去掉。例如:
|
||||
|
||||
* 当前网页链接:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764&windowId=6402c94cb5d6283f76fb49
|
||||
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
|
||||
|
||||
### 其他问题
|
||||
还有其他问题,可以加我 wx,拉个交流群大家一起聊聊。
|
||||

|
||||
#### 软件教程:Nginx
|
||||
...没写,这个百度吧。
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "docgpt",
|
||||
"name": "fastgpt",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -13,11 +13,9 @@
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.17",
|
||||
"@chakra-ui/react": "^2.5.1",
|
||||
"@chakra-ui/system": "^2.5.1",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@next/font": "13.1.6",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@tanstack/react-query": "^4.24.10",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"axios": "^1.3.3",
|
||||
|
||||
70
pnpm-lock.yaml
generated
70
pnpm-lock.yaml
generated
@@ -3,11 +3,9 @@ lockfileVersion: 5.4
|
||||
specifiers:
|
||||
'@chakra-ui/icons': ^2.0.17
|
||||
'@chakra-ui/react': ^2.5.1
|
||||
'@chakra-ui/system': ^2.5.1
|
||||
'@emotion/react': ^11.10.6
|
||||
'@emotion/styled': ^11.10.6
|
||||
'@next/font': 13.1.6
|
||||
'@reduxjs/toolkit': ^1.9.3
|
||||
'@tanstack/react-query': ^4.24.10
|
||||
'@types/formidable': ^2.0.5
|
||||
'@types/jsonwebtoken': ^9.0.1
|
||||
@@ -54,13 +52,11 @@ specifiers:
|
||||
zustand: ^4.3.5
|
||||
|
||||
dependencies:
|
||||
'@chakra-ui/icons': registry.npmmirror.com/@chakra-ui/icons/2.0.17_n3dxrjldmk5gnycgnw7noyo5tu
|
||||
'@chakra-ui/icons': registry.npmmirror.com/@chakra-ui/icons/2.0.17_react@18.2.0
|
||||
'@chakra-ui/react': registry.npmmirror.com/@chakra-ui/react/2.5.1_e6pzu3hsaqmql4fl7jx73ckiym
|
||||
'@chakra-ui/system': registry.npmmirror.com/@chakra-ui/system/2.5.1_xqp3pgpqjlfxxa3zxu4zoc4fba
|
||||
'@emotion/react': registry.npmmirror.com/@emotion/react/11.10.6_pmekkgnqduwlme35zpnqhenc34
|
||||
'@emotion/styled': registry.npmmirror.com/@emotion/styled/11.10.6_oouaibmszuch5k64ms7uxp2aia
|
||||
'@next/font': registry.npmmirror.com/@next/font/13.1.6
|
||||
'@reduxjs/toolkit': registry.npmmirror.com/@reduxjs/toolkit/1.9.3_react@18.2.0
|
||||
'@tanstack/react-query': registry.npmmirror.com/@tanstack/react-query/4.24.10_biqbaboplfbrettd7655fr4n2y
|
||||
'@types/nprogress': registry.npmmirror.com/@types/nprogress/0.2.0
|
||||
axios: registry.npmmirror.com/axios/1.3.3
|
||||
@@ -1455,7 +1451,20 @@ packages:
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/icons/2.0.17_n3dxrjldmk5gnycgnw7noyo5tu:
|
||||
registry.npmmirror.com/@chakra-ui/icon/3.0.16_react@18.2.0:
|
||||
resolution: {integrity: sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icon/-/icon-3.0.16.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/icon/3.0.16
|
||||
name: '@chakra-ui/icon'
|
||||
version: 3.0.16
|
||||
peerDependencies:
|
||||
'@chakra-ui/system': '>=2.0.0'
|
||||
react: '>=18'
|
||||
dependencies:
|
||||
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/icons/2.0.17_react@18.2.0:
|
||||
resolution: {integrity: sha512-HMJP0WrJgAmFR9+Xh/CBH0nVnGMsJ4ZC8MK6tMgxPKd9/muvn0I4hsicHqdPlLpmB0TlxlhkBAKaVMtOdz6F0w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icons/-/icons-2.0.17.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/icons/2.0.17
|
||||
name: '@chakra-ui/icons'
|
||||
@@ -1464,8 +1473,7 @@ packages:
|
||||
'@chakra-ui/system': '>=2.0.0'
|
||||
react: '>=18'
|
||||
dependencies:
|
||||
'@chakra-ui/icon': registry.npmmirror.com/@chakra-ui/icon/3.0.16_n3dxrjldmk5gnycgnw7noyo5tu
|
||||
'@chakra-ui/system': registry.npmmirror.com/@chakra-ui/system/2.5.1_xqp3pgpqjlfxxa3zxu4zoc4fba
|
||||
'@chakra-ui/icon': registry.npmmirror.com/@chakra-ui/icon/3.0.16_react@18.2.0
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
@@ -2925,27 +2933,6 @@ packages:
|
||||
version: 2.11.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@reduxjs/toolkit/1.9.3_react@18.2.0:
|
||||
resolution: {integrity: sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-1.9.3.tgz}
|
||||
id: registry.npmmirror.com/@reduxjs/toolkit/1.9.3
|
||||
name: '@reduxjs/toolkit'
|
||||
version: 1.9.3
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17.0.0 || ^18
|
||||
react-redux: ^7.2.1 || ^8.0.2
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-redux:
|
||||
optional: true
|
||||
dependencies:
|
||||
immer: registry.npmmirror.com/immer/9.0.19
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
redux: registry.npmmirror.com/redux/4.2.1
|
||||
redux-thunk: registry.npmmirror.com/redux-thunk/2.4.2_redux@4.2.1
|
||||
reselect: registry.npmmirror.com/reselect/4.1.7
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@rushstack/eslint-patch/1.2.0:
|
||||
resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz}
|
||||
name: '@rushstack/eslint-patch'
|
||||
@@ -7226,25 +7213,6 @@ packages:
|
||||
picomatch: registry.npmmirror.com/picomatch/2.3.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/redux-thunk/2.4.2_redux@4.2.1:
|
||||
resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/redux-thunk/-/redux-thunk-2.4.2.tgz}
|
||||
id: registry.npmmirror.com/redux-thunk/2.4.2
|
||||
name: redux-thunk
|
||||
version: 2.4.2
|
||||
peerDependencies:
|
||||
redux: ^4
|
||||
dependencies:
|
||||
redux: registry.npmmirror.com/redux/4.2.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/redux/4.2.1:
|
||||
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/redux/-/redux-4.2.1.tgz}
|
||||
name: redux
|
||||
version: 4.2.1
|
||||
dependencies:
|
||||
'@babel/runtime': registry.npmmirror.com/@babel/runtime/7.21.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/refractor/3.6.0:
|
||||
resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/refractor/-/refractor-3.6.0.tgz}
|
||||
name: refractor
|
||||
@@ -7351,12 +7319,6 @@ packages:
|
||||
unified: registry.npmmirror.com/unified/10.1.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/reselect/4.1.7:
|
||||
resolution: {integrity: sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/reselect/-/reselect-4.1.7.tgz}
|
||||
name: reselect
|
||||
version: 4.1.7
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/resolve-from/4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz}
|
||||
name: resolve-from
|
||||
|
||||
@@ -36,28 +36,10 @@ export const postGPT3SendPrompt = ({
|
||||
});
|
||||
|
||||
/**
|
||||
* 预发 prompt 进行存储
|
||||
* 存储一轮对话
|
||||
*/
|
||||
export const postChatGptPrompt = ({
|
||||
prompt,
|
||||
windowId,
|
||||
chatId
|
||||
}: {
|
||||
prompt: ChatSiteItemType;
|
||||
windowId: string;
|
||||
chatId: string;
|
||||
}) =>
|
||||
POST<string>(`/chat/preChat`, {
|
||||
windowId,
|
||||
prompt: {
|
||||
obj: prompt.obj,
|
||||
value: prompt.value
|
||||
},
|
||||
chatId
|
||||
});
|
||||
/* 获取 Chat 的 Event 对象,进行持续通信 */
|
||||
export const getChatGPTSendEvent = (chatId: string, windowId: string) =>
|
||||
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}&date=${Date.now()}`);
|
||||
export const postSaveChat = (data: { windowId: string; prompts: ChatItemType[] }) =>
|
||||
POST('/chat/saveChat', data);
|
||||
|
||||
/**
|
||||
* 删除最后一句
|
||||
|
||||
47
src/api/fetch.ts
Normal file
47
src/api/fetch.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
interface StreamFetchProps {
|
||||
url: string;
|
||||
data: any;
|
||||
onMessage: (text: string) => void;
|
||||
}
|
||||
export const streamFetch = ({ url, data, onMessage }: StreamFetchProps) =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const reader = res.body?.getReader();
|
||||
if (!reader) return;
|
||||
const decoder = new TextDecoder();
|
||||
let responseText = '';
|
||||
|
||||
const read = async () => {
|
||||
const { done, value } = await reader?.read();
|
||||
if (done) {
|
||||
if (res.status === 200) {
|
||||
resolve(responseText);
|
||||
} else {
|
||||
try {
|
||||
const parseError = JSON.parse(responseText);
|
||||
reject(parseError?.message || '请求异常');
|
||||
} catch (err) {
|
||||
reject('请求异常');
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
const text = decoder.decode(value).replace(/<br\/>/g, '\n');
|
||||
res.status === 200 && onMessage(text);
|
||||
responseText += text;
|
||||
read();
|
||||
};
|
||||
read();
|
||||
} catch (err: any) {
|
||||
console.log(err, '====');
|
||||
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import Link from 'next/link';
|
||||
import Navbar from './navbar';
|
||||
import NavbarPhone from './navbarPhone';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
@@ -82,6 +82,9 @@ export const theme = extendTheme({
|
||||
'8xl': '6rem',
|
||||
'9xl': '8rem'
|
||||
},
|
||||
fonts: {
|
||||
body: '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||
},
|
||||
components: {
|
||||
Modal: ModalTheme,
|
||||
Button
|
||||
|
||||
@@ -6,23 +6,19 @@ import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { openaiError } from '@/service/errorCode';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { PassThrough } from 'stream';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
|
||||
res.on('close', () => {
|
||||
res.end();
|
||||
});
|
||||
|
||||
const { chatId, windowId } = req.query as { chatId: string; windowId: string };
|
||||
const { chatId, windowId, prompt } = req.body as {
|
||||
prompt: ChatItemType;
|
||||
windowId: string;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
try {
|
||||
if (!windowId || !chatId) {
|
||||
if (!windowId || !chatId || !prompt) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
@@ -32,15 +28,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const model: ModelType = chat.modelId;
|
||||
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
// 读取对话内容
|
||||
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content;
|
||||
prompts.push(prompt);
|
||||
|
||||
// 长度过滤
|
||||
// 上下文长度过滤
|
||||
const maxContext = model.security.contextMaxLen;
|
||||
const filterPrompts =
|
||||
prompts.length > maxContext + 2
|
||||
@@ -48,6 +40,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
: prompts.slice(0, prompts.length);
|
||||
|
||||
// 格式化文本内容
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||
(item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
@@ -59,9 +56,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||
content: '如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
|
||||
});
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey);
|
||||
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
@@ -81,63 +79,40 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
'response success'
|
||||
);
|
||||
|
||||
let AIResponse = '';
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
|
||||
const pass = new PassThrough();
|
||||
pass.pipe(res);
|
||||
|
||||
// 解析数据
|
||||
const decoder = new TextDecoder();
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type === 'event') {
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') {
|
||||
// 存入库
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$push: {
|
||||
content: {
|
||||
obj: 'AI',
|
||||
value: AIResponse
|
||||
}
|
||||
},
|
||||
updateTime: Date.now()
|
||||
});
|
||||
res.write('event: done\ndata: \n\n');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '\n';
|
||||
// console.log('content:', content)
|
||||
res.write(`event: responseData\ndata: ${content.replace(/\n/g, '<br/>')}\n\n`);
|
||||
AIResponse += content;
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
if (!content) return;
|
||||
// console.log('content:', content)
|
||||
pass.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error, '====');
|
||||
throw new Error('错误了');
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decodeURIComponent(chunk));
|
||||
}
|
||||
pass.push(null);
|
||||
} catch (err: any) {
|
||||
console.log('error->', err?.response, '===');
|
||||
let errorText = 'OpenAI 服务器访问超时';
|
||||
if (err.code === 'ECONNRESET' || err?.response?.status === 502) {
|
||||
errorText = '服务器代理出错';
|
||||
} else if (err?.response?.statusText && openaiError[err.response.statusText]) {
|
||||
errorText = openaiError[err.response.statusText];
|
||||
}
|
||||
console.log('error->', errorText);
|
||||
res.write(`event: serviceError\ndata: ${errorText}\n\n`);
|
||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$pop: { content: 1 },
|
||||
updateTime: Date.now()
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,34 +2,31 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { authChat } from '@/service/utils/chat';
|
||||
|
||||
/* 聊天预请求,存储聊天内容 */
|
||||
/* 聊天内容存存储 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { windowId, prompt, chatId } = req.body as {
|
||||
const { windowId, prompts } = req.body as {
|
||||
windowId: string;
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
prompts: ChatItemType[];
|
||||
};
|
||||
|
||||
if (!windowId || !prompt || !chatId) {
|
||||
if (!windowId || !prompts) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { chat } = await authChat(chatId);
|
||||
|
||||
// 长度校验
|
||||
const model: ModelType = chat.modelId;
|
||||
if (prompt.value.length > model.security.contentMaxLen) {
|
||||
throw new Error('输入内容超长');
|
||||
}
|
||||
|
||||
// 存入库
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$push: { content: prompt },
|
||||
$push: {
|
||||
content: {
|
||||
$each: prompts.map((item) => ({
|
||||
obj: item.obj,
|
||||
value: item.value
|
||||
}))
|
||||
}
|
||||
},
|
||||
updateTime: Date.now()
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少参数');
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
getInitChatSiteInfo,
|
||||
postGPT3SendPrompt,
|
||||
getChatGPTSendEvent,
|
||||
postChatGptPrompt,
|
||||
delLastMessage
|
||||
} from '@/api/chat';
|
||||
import { getInitChatSiteInfo, postGPT3SendPrompt, delLastMessage, postSaveChat } from '@/api/chat';
|
||||
import { ChatSiteItemType, ChatSiteType } from '@/types/chat';
|
||||
import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
@@ -17,6 +11,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { OpenAiModelEnum } from '@/constants/model';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { streamFetch } from '@/api/fetch';
|
||||
|
||||
const Markdown = dynamic(() => import('@/components/Markdown'));
|
||||
|
||||
@@ -128,69 +123,64 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
|
||||
const chatGPTPrompt = useCallback(
|
||||
async (newChatList: ChatSiteItemType[]) => {
|
||||
if (!windowId) return;
|
||||
/* 预请求,把消息存入库 */
|
||||
await postChatGptPrompt({
|
||||
windowId,
|
||||
prompt: newChatList[newChatList.length - 1],
|
||||
chatId
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const event = getChatGPTSendEvent(chatId, windowId);
|
||||
// 30s 收不到消息就报错
|
||||
let timer = setTimeout(() => {
|
||||
event.close();
|
||||
reject('服务器超时');
|
||||
}, 30000);
|
||||
event.addEventListener('responseData', ({ data }) => {
|
||||
/* 重置定时器 */
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
event.close();
|
||||
reject('服务器超时');
|
||||
}, 30000);
|
||||
|
||||
const msg = data.replace(/<br\/>/g, '\n');
|
||||
const prompt = {
|
||||
obj: newChatList[newChatList.length - 1].obj,
|
||||
value: newChatList[newChatList.length - 1].value
|
||||
};
|
||||
// 流请求,获取数据
|
||||
const res = await streamFetch({
|
||||
url: '/api/chat/chatGpt',
|
||||
data: {
|
||||
windowId,
|
||||
prompt,
|
||||
chatId
|
||||
},
|
||||
onMessage: (text: string) => {
|
||||
setChatList((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
value: item.value + msg
|
||||
value: item.value + text
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
event.addEventListener('done', () => {
|
||||
console.log('done');
|
||||
clearTimeout(timer);
|
||||
event.close();
|
||||
setChatList((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
);
|
||||
resolve('');
|
||||
});
|
||||
event.addEventListener('serviceError', ({ data: err }) => {
|
||||
clearTimeout(timer);
|
||||
event.close();
|
||||
console.log('error->', err, '===');
|
||||
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
|
||||
});
|
||||
event.onerror = (err) => {
|
||||
clearTimeout(timer);
|
||||
event.close();
|
||||
console.log('error->', err);
|
||||
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 保存对话信息
|
||||
try {
|
||||
await postSaveChat({
|
||||
windowId,
|
||||
prompts: [
|
||||
prompt,
|
||||
{
|
||||
obj: 'AI',
|
||||
value: res as string
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: '存储对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
|
||||
// 设置完成状态
|
||||
setChatList((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
[chatId, windowId]
|
||||
[chatId, toast, windowId]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -243,9 +233,9 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
|
||||
if (typeof fnMap[chatSiteData.chatModel] === 'function') {
|
||||
await fnMap[chatSiteData.chatModel](requestPrompt);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : '聊天出错了~',
|
||||
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
@@ -340,6 +330,16 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{/* 空内容提示 */}
|
||||
{/* {
|
||||
chatList.length === 0 && (
|
||||
<>
|
||||
<Card>
|
||||
内容太长
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
} */}
|
||||
<Box
|
||||
m={media('20px auto', '0 auto')}
|
||||
w={media('100vw', '100%')}
|
||||
|
||||
@@ -12,8 +12,10 @@ import {
|
||||
Td,
|
||||
TableContainer,
|
||||
Select,
|
||||
Input
|
||||
Input,
|
||||
IconButton
|
||||
} from '@chakra-ui/react';
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
import { useForm, useFieldArray } from 'react-hook-form';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
import { putUserInfo } from '@/api/user';
|
||||
@@ -130,7 +132,15 @@ const NumberSetting = () => {
|
||||
></Input>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button onClick={() => removeAccount(i)}>删除</Button>
|
||||
<IconButton
|
||||
aria-label="删除账号"
|
||||
icon={<DeleteIcon />}
|
||||
colorScheme={'red'}
|
||||
onClick={() => {
|
||||
removeAccount(i);
|
||||
handleSubmit(onclickSave)();
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
@@ -2,5 +2,9 @@ export const openaiError: Record<string, string> = {
|
||||
context_length_exceeded: '内容超长了,请重置对话',
|
||||
Unauthorized: 'API-KEY 不合法',
|
||||
rate_limit_reached: '同时访问用户过多,请稍后再试',
|
||||
'Bad Request': '内容太多了~'
|
||||
'Bad Request': '上下文太多了,请重开对话~'
|
||||
};
|
||||
export const proxyError: Record<string, boolean> = {
|
||||
ECONNABORTED: true,
|
||||
ECONNRESET: true
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextApiResponse } from 'next';
|
||||
import { openaiError } from './errorCode';
|
||||
import { openaiError, proxyError } from './errorCode';
|
||||
|
||||
export interface ResponseType<T = any> {
|
||||
code: number;
|
||||
@@ -23,12 +23,13 @@ export const jsonRes = (
|
||||
msg = error?.message || '请求错误';
|
||||
if (typeof error === 'string') {
|
||||
msg = error;
|
||||
} else if (error?.response?.data?.message in openaiError) {
|
||||
msg = openaiError[error?.response?.data?.message];
|
||||
} else if (proxyError[error?.code]) {
|
||||
msg = '服务器代理出错';
|
||||
} else if (openaiError[error?.response?.statusText]) {
|
||||
msg = openaiError[error.response.statusText];
|
||||
}
|
||||
|
||||
console.log('error->', error);
|
||||
console.log('error->', msg);
|
||||
console.log('error->', error.code, error?.response?.statusText, msg);
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -35,7 +35,7 @@ export const authChat = async (chatId: string) => {
|
||||
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value;
|
||||
|
||||
if (!userApiKey) {
|
||||
return Promise.reject('该用户缺少ApiKey, 无法请求');
|
||||
return Promise.reject('缺少ApiKey, 无法请求');
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const tokenKey = 'doc-gpt-token';
|
||||
const tokenKey = 'fast-gpt-token';
|
||||
|
||||
export const setToken = (val: string) => {
|
||||
localStorage.setItem(tokenKey, val);
|
||||
|
||||
Reference in New Issue
Block a user