Commit 7807b267 by Archer

feat: 流优化

parent 16775430
...@@ -15,22 +15,62 @@ TOKEN_KEY=随便填一个,用于生成和校验token ...@@ -15,22 +15,62 @@ TOKEN_KEY=随便填一个,用于生成和校验token
```bash ```bash
pnpm dev pnpm dev
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## 部署 ## 部署
```bash ```bash
# 本地 docker 打包 # 本地 docker 打包
docker build -t imageName . docker build -t imageName:tag .
docker push imageName docker push imageName:tag
```
# 服务器拉取部署 服务器请准备好 docker, mongo,nginx和代理。 镜像走本机的代理,所以用 host,port改成代理的端口,clash一般都是7890。
docker pull imageName
```bash
# 服务器拉取部署, imageName 替换成镜像名
docker pull imageName:tag
# 获取本地旧镜像ID
OLD_IMAGE_ID=$(docker images imageName -f "dangling=true" -q)
docker stop doc-gpt || true docker stop doc-gpt || true
docker rm doc-gpt || true docker rm doc-gpt || true
# 运行时才把参数写入 docker run -d --network=host --name doc-gpt \
docker run -d --network=host --name doc-gpt -e AXIOS_PROXY_HOST= -e AXIOS_PROXY_PORT= -e MAILE_CODE= -e TOKEN_KEY= -e MONGODB_URI= imageName -e MAX_USER=50 \
-e AXIOS_PROXY_HOST=127.0.0.1 \
-e AXIOS_PROXY_PORT=7890 \
-e MY_MAIL=your email\
-e MAILE_CODE=your email code \
-e TOKEN_KEY=任意一个内容 \
-e MONGODB_URI="mongodb://aha:ROOT_root123@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 安装
```bash
# 安装docker
curl -sSL https://get.daocloud.io/docker | sh
sudo systemctl start docker
```
### mongo 安装
```bash
docker pull mongo:6.0.4
docker stop mongo
docker rm mongo
docker run -d --name mongo \
-e MONGO_INITDB_ROOT_USERNAME= \
-e MONGO_INITDB_ROOT_PASSWORD= \
-v /root/service/mongo:/data/db \
mongo:6.0.4
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
# 介绍页 # 介绍页
......
import { GET, POST, DELETE } from './request'; import { GET, POST, DELETE } from './request';
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat'; import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
import axios from 'axios';
/** /**
* 获取一个聊天框的ID * 获取一个聊天框的ID
...@@ -56,7 +57,7 @@ export const postChatGptPrompt = ({ ...@@ -56,7 +57,7 @@ export const postChatGptPrompt = ({
}); });
/* 获取 Chat 的 Event 对象,进行持续通信 */ /* 获取 Chat 的 Event 对象,进行持续通信 */
export const getChatGPTSendEvent = (chatId: string, windowId: string) => export const getChatGPTSendEvent = (chatId: string, windowId: string) =>
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}`); new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}&date=${Date.now()}`);
/** /**
* 删除最后一句 * 删除最后一句
......
...@@ -49,21 +49,20 @@ function responseError(err: any) { ...@@ -49,21 +49,20 @@ function responseError(err: any) {
console.error('请求错误', err); console.error('请求错误', err);
if (!err) { if (!err) {
return Promise.reject('未知错误'); return Promise.reject({ message: '未知错误' });
} }
if (typeof err === 'string') { if (typeof err === 'string') {
return Promise.reject(err); return Promise.reject({ message: err });
} }
if (err.response) { if (err.response) {
// 有报错响应 // 有报错响应
const res = err.response; const res = err.response;
/* token过期,判断请求token与本地是否相同,若不同需要重发 */
if (res.data.code in TOKEN_ERROR_CODE) { if (res.data.code in TOKEN_ERROR_CODE) {
clearToken(); clearToken();
return Promise.reject('token过期,重新登录'); return Promise.reject({ message: 'token过期,重新登录' });
} }
} }
return Promise.reject('未知错误'); return Promise.reject(err);
} }
/* 创建请求实例 */ /* 创建请求实例 */
......
...@@ -46,6 +46,7 @@ const Navbar = ({ ...@@ -46,6 +46,7 @@ const Navbar = ({
alignItems={'center'} alignItems={'center'}
justifyContent={'center'} justifyContent={'center'}
onClick={() => onClick={() =>
!item.activeLink.includes(router.pathname) &&
router.push(item.link, undefined, { router.push(item.link, undefined, {
shallow: true shallow: true
}) })
......
...@@ -328,7 +328,6 @@ ...@@ -328,7 +328,6 @@
border-radius: 3px 3px 3px 3px; border-radius: 3px 3px 3px 3px;
margin: 0 2px; margin: 0 2px;
padding: 0 5px; padding: 0 5px;
white-space: nowrap;
} }
.markdown pre > code { .markdown pre > code {
background: none repeat scroll 0 0 transparent; background: none repeat scroll 0 0 transparent;
......
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo'; import { Readable } from 'stream';
import { connectToDatabase, ChatWindow } from '@/service/mongo';
import type { ModelType } from '@/types/model'; import type { ModelType } from '@/types/model';
import { getOpenAIApi, authChat } from '@/service/utils/chat'; import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { openaiProxy } from '@/service/utils/tools'; import { openaiProxy } from '@/service/utils/tools';
...@@ -9,12 +9,23 @@ import { ChatItemType } from '@/types/chat'; ...@@ -9,12 +9,23 @@ import { ChatItemType } from '@/types/chat';
/* 发送提示词 */ /* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.writeHead(200, { res.setHeader('Connection', 'keep-alive');
Connection: 'keep-alive', res.setHeader('Cache-Control', 'no-cache');
'Content-Encoding': 'none', res.setHeader('Content-Type', 'text/event-stream');
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream' const responseData: string[] = [];
const stream = new Readable({
read(size) {
const data = responseData.shift() || null;
this.push(data);
}
});
res.on('close', () => {
res.end();
stream.destroy();
}); });
const { chatId, windowId } = req.query as { chatId: string; windowId: string }; const { chatId, windowId } = req.query as { chatId: string; windowId: string };
try { try {
...@@ -47,14 +58,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -47,14 +58,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map( const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
(item: ChatItemType) => ({ (item: ChatItemType) => ({
role: map[item.obj], role: map[item.obj],
content: item.value content: item.value.replace(/(\n| )/g, '')
}) })
); );
// 第一句话,强调代码类型 // 第一句话,强调代码类型
formatPrompts.unshift({ formatPrompts.unshift({
role: ChatCompletionRequestMessageRoleEnum.System, role: ChatCompletionRequestMessageRoleEnum.System,
content: content:
'If the content is code or code blocks, please label the code type as accurately as possible.' 'If the content is code or code blocks, please mark the code type as accurately as possible!'
}); });
// 获取 chatAPI // 获取 chatAPI
...@@ -74,8 +85,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -74,8 +85,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const reg = /{"content"(.*)"}/g; const reg = /{"content"(.*)"}/g;
// @ts-ignore // @ts-ignore
const match = chatResponse.data.match(reg); const match = chatResponse.data.match(reg);
if (!match) return;
let AIResponse = ''; let AIResponse = '';
if (match) {
// 循环给 stream push 内容
match.forEach((item: string, i: number) => { match.forEach((item: string, i: number) => {
try { try {
const json = JSON.parse(item); const json = JSON.parse(item);
...@@ -83,15 +97,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -83,15 +97,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (i === 0 && json.content?.startsWith('\n')) return; if (i === 0 && json.content?.startsWith('\n')) return;
AIResponse += json.content; AIResponse += json.content;
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
content && res.write(`data: ${content}\n\n`); if (content) {
responseData.push(`event: responseData\ndata: ${content}\n\n`);
// res.write(`event: responseData\n`)
// res.write(`data: ${content}\n\n`)
}
} catch (err) { } catch (err) {
err; err;
} }
}); });
}
res.write(`data: [DONE]\n\n`);
responseData.push(`event: done\ndata: \n\n`);
// 存入库 // 存入库
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, { await ChatWindow.findByIdAndUpdate(windowId, {
$push: { $push: {
content: { content: {
...@@ -101,16 +119,41 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -101,16 +119,41 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
updateTime: Date.now() updateTime: Date.now()
}); });
})();
res.end();
} catch (err: any) { } catch (err: any) {
console.error(err?.response?.data || err); let errorText = err;
if (err.code === 'ECONNRESET') {
errorText = '服务器代理出错';
} else {
switch (err?.response?.data?.error?.code) {
case 'invalid_api_key':
errorText = 'API-KEY不合法';
break;
case 'context_length_exceeded':
errorText = '内容超长了,请重置对话';
break;
case 'rate_limit_reached':
errorText = '同时访问用户过多,请稍后再试';
break;
case null:
errorText = 'OpenAI 服务器访问超时';
break;
default:
errorText = '服务器异常';
}
}
console.error(errorText);
responseData.push(`event: serviceError\ndata: ${errorText}\n\n`);
// 删除最一条数据库记录, 也就是预发送的那一条 // 删除最一条数据库记录, 也就是预发送的那一条
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, { await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 }, $pop: { content: 1 },
updateTime: Date.now() updateTime: Date.now()
}); });
})();
res.end();
} }
// 开启 stream 传输
stream.pipe(res);
} }
...@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
// 安全校验 // 安全校验
if (chat.loadAmount === 0 || chat.expiredTime < Date.now()) { if (!chat || chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
throw new Error('聊天框已过期'); throw new Error('聊天框已过期');
} }
...@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
}); });
} catch (err) { } catch (err) {
console.error(err);
jsonRes(res, { jsonRes(res, {
code: 500, code: 500,
error: err error: err
......
...@@ -14,7 +14,6 @@ import { useToast } from '@/hooks/useToast'; ...@@ -14,7 +14,6 @@ import { useToast } from '@/hooks/useToast';
import Icon from '@/components/Icon'; import Icon from '@/components/Icon';
import { useScreen } from '@/hooks/useScreen'; import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import { OpenAiModelEnum } from '@/constants/model'; import { OpenAiModelEnum } from '@/constants/model';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
...@@ -75,9 +74,9 @@ const Chat = () => { ...@@ -75,9 +74,9 @@ const Chat = () => {
scrollToBottom(); scrollToBottom();
setLoading(false); setLoading(false);
}, },
onError() { onError(e: any) {
toast({ toast({
title: '初始化异常,请刷新', title: e?.message || '初始化异常,请检查地址',
status: 'error', status: 'error',
isClosable: true, isClosable: true,
duration: 5000 duration: 5000
...@@ -124,36 +123,55 @@ const Chat = () => { ...@@ -124,36 +123,55 @@ const Chat = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const event = getChatGPTSendEvent(chatId, windowId); const event = getChatGPTSendEvent(chatId, windowId);
event.onmessage = ({ data }) => { // 30s 收不到消息就报错
if (data === '[DONE]') { let timer = setTimeout(() => {
event.close(); event.close();
reject('服务器超时');
}, 300000);
event.addEventListener('responseData', ({ data }) => {
/* 重置定时器 */
clearTimeout(timer);
timer = setTimeout(() => {
event.close();
reject('服务器超时');
}, 300000);
const msg = data.replace(/<br\/>/g, '\n');
setChatList((state) => setChatList((state) =>
state.map((item, index) => { state.map((item, index) => {
if (index !== state.length - 1) return item; if (index !== state.length - 1) return item;
return { return {
...item, ...item,
status: 'finish' value: item.value + msg
}; };
}) })
); );
resolve(''); });
} else if (data) { event.addEventListener('done', () => {
const msg = data.replace(/<br\/>/g, '\n'); clearTimeout(timer);
event.close();
setChatList((state) => setChatList((state) =>
state.map((item, index) => { state.map((item, index) => {
if (index !== state.length - 1) return item; if (index !== state.length - 1) return item;
return { return {
...item, ...item,
value: item.value + msg status: 'finish'
}; };
}) })
); );
} resolve('');
}; });
event.onerror = (err) => { event.addEventListener('serviceError', ({ data: err }) => {
clearTimeout(timer);
event.close();
console.error(err, '==='); console.error(err, '===');
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
});
event.onerror = (err) => {
clearTimeout(timer);
event.close(); event.close();
reject('对话出现错误'); console.error(err);
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
}; };
}); });
}, },
...@@ -320,6 +338,16 @@ const Chat = () => { ...@@ -320,6 +338,16 @@ const Chat = () => {
</Box> </Box>
))} ))}
</Box> </Box>
{/* 空内容提示 */}
{/* {
chatList.length === 0 && (
<>
<Card>
内容太长
</Card>
</>
)
} */}
<Box <Box
m={media('20px auto', '0 auto')} m={media('20px auto', '0 auto')}
w={media('100vw', '100%')} w={media('100vw', '100%')}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment