Compare commits

..

20 Commits
v1.1 ... v1.2

Author SHA1 Message Date
Archer
be69cfb966 fix: 请求头 2023-03-14 15:13:22 +08:00
Archer
6244f6c1fb perf: 流响应 2023-03-14 14:58:35 +08:00
Archer
e12f97a73b perf: 文案提示;删除账号 2023-03-14 12:54:37 +08:00
Archer
7f96c4ff9b perf: 包和文档内容 2023-03-13 23:58:17 +08:00
Archer
138b607ac7 perf:提示文案;错误处理 2023-03-13 19:57:37 +08:00
Archer
b204c55bd1 Merge branch 'dev1.1' into beian 2023-03-12 00:07:24 +08:00
Archer
a7b9940d7a Merge branch 'main' into beian 2023-03-11 16:58:46 +08:00
Archer
7db87c2d09 Merge branch 'dev1.1' into beian 2023-03-10 18:57:18 +08:00
Archer
f023f63103 feat: 合并 2023-03-10 03:02:46 +08:00
Archer
e5fe670a6e beian 2023-03-09 20:27:11 +08:00
Archer
7807b26707 feat: 流优化 2023-03-09 10:09:49 +08:00
archer
16775430ea perf: 登录顺序 2023-03-06 20:40:10 +08:00
archer
dd5217d8a5 feat: 图片地址 2023-03-06 17:20:14 +08:00
archer
9f8d696bbe perf: 图片cdn 2023-03-06 10:56:46 +08:00
archer
bf81d23de4 perf: 去除console, 2023-03-05 23:08:23 +08:00
archer
52a752dab5 perf: 懒加载和动态加载优化 2023-03-05 21:16:19 +08:00
archer
78903baefa perf: ui调整 2023-03-05 15:56:40 +08:00
archer
45ad3ba22a perf: md解析样式 2023-03-05 15:28:46 +08:00
archer
c03a7db633 perf: 聊天页优化 2023-03-05 13:16:56 +08:00
archer
2cc32d1806 feat: 注册限流配置
feat: 页面加载动画
feat: md样式优化
feat: 移动端全屏覆盖
2023-03-05 12:47:09 +08:00
17 changed files with 267 additions and 299 deletions

View File

@@ -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
View File

@@ -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=hostport 改成代理的端口clash 一般都是 7890。
#### docker 打包
```bash
# 本地 docker 打包
docker build -t imageName:tag .
docker push imageName:tag
```
服务器请准备好 docker mongonginx和代理。 镜像走本机的代理,所以用 hostport改成代理的端口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拉个交流群大家一起聊聊。
![](/icon/erweima.jpg)
#### 软件教程Nginx
...没写,这个百度吧。

View File

@@ -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
View File

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

View File

@@ -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
View 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 || '请求异常');
}
});

View File

@@ -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';

View File

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

View File

@@ -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();
}
}

View File

@@ -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()
});

View File

@@ -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);

View File

@@ -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%')}

View File

@@ -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>
))}

View File

@@ -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
};

View File

@@ -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({

View File

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

View File

@@ -1,4 +1,4 @@
const tokenKey = 'doc-gpt-token';
const tokenKey = 'fast-gpt-token';
export const setToken = (val: string) => {
localStorage.setItem(tokenKey, val);