feat: 流优化

This commit is contained in:
Archer
2023-03-09 10:09:49 +08:00
parent 16775430ea
commit 7807b26707
8 changed files with 203 additions and 93 deletions

View File

@@ -1,5 +1,6 @@
import { GET, POST, DELETE } from './request';
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
import axios from 'axios';
/**
* 获取一个聊天框的ID
@@ -56,7 +57,7 @@ export const postChatGptPrompt = ({
});
/* 获取 Chat 的 Event 对象,进行持续通信 */
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()}`);
/**
* 删除最后一句

View File

@@ -49,21 +49,20 @@ function responseError(err: any) {
console.error('请求错误', err);
if (!err) {
return Promise.reject('未知错误');
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject(err);
return Promise.reject({ message: err });
}
if (err.response) {
// 有报错响应
const res = err.response;
/* token过期,判断请求token与本地是否相同若不同需要重发 */
if (res.data.code in TOKEN_ERROR_CODE) {
clearToken();
return Promise.reject('token过期重新登录');
return Promise.reject({ message: 'token过期重新登录' });
}
}
return Promise.reject('未知错误');
return Promise.reject(err);
}
/* 创建请求实例 */

View File

@@ -46,6 +46,7 @@ const Navbar = ({
alignItems={'center'}
justifyContent={'center'}
onClick={() =>
!item.activeLink.includes(router.pathname) &&
router.push(item.link, undefined, {
shallow: true
})

View File

@@ -328,7 +328,6 @@
border-radius: 3px 3px 3px 3px;
margin: 0 2px;
padding: 0 5px;
white-space: nowrap;
}
.markdown pre > code {
background: none repeat scroll 0 0 transparent;

View File

@@ -1,6 +1,6 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
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 { getOpenAIApi, authChat } from '@/service/utils/chat';
import { openaiProxy } from '@/service/utils/tools';
@@ -9,12 +9,23 @@ import { ChatItemType } from '@/types/chat';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Encoding': 'none',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream'
res.setHeader('Connection', 'keep-alive');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('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 };
try {
@@ -47,14 +58,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
(item: ChatItemType) => ({
role: map[item.obj],
content: item.value
content: item.value.replace(/(\n| )/g, '')
})
);
// 第一句话,强调代码类型
formatPrompts.unshift({
role: ChatCompletionRequestMessageRoleEnum.System,
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
@@ -74,43 +85,75 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const reg = /{"content"(.*)"}/g;
// @ts-ignore
const match = chatResponse.data.match(reg);
if (!match) return;
let AIResponse = '';
if (match) {
match.forEach((item: string, i: number) => {
try {
const json = JSON.parse(item);
// 开头的换行忽略
if (i === 0 && json.content?.startsWith('\n')) return;
AIResponse += json.content;
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
content && res.write(`data: ${content}\n\n`);
} catch (err) {
err;
}
});
}
res.write(`data: [DONE]\n\n`);
// 循环给 stream push 内容
match.forEach((item: string, i: number) => {
try {
const json = JSON.parse(item);
// 开头的换行忽略
if (i === 0 && json.content?.startsWith('\n')) return;
AIResponse += json.content;
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
if (content) {
responseData.push(`event: responseData\ndata: ${content}\n\n`);
// res.write(`event: responseData\n`)
// res.write(`data: ${content}\n\n`)
}
} catch (err) {
err;
}
});
responseData.push(`event: done\ndata: \n\n`);
// 存入库
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
res.end();
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
})();
} catch (err: any) {
console.error(err?.response?.data || err);
// 删除最一条数据库记录, 也就是预发送的那一条
await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 },
updateTime: Date.now()
});
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`);
res.end();
// 删除最一条数据库记录, 也就是预发送的那一条
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 },
updateTime: Date.now()
});
})();
}
// 开启 stream 传输
stream.pipe(res);
}

View File

@@ -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('聊天框已过期');
}
@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
});
} catch (err) {
console.error(err);
jsonRes(res, {
code: 500,
error: err

View File

@@ -14,7 +14,6 @@ import { useToast } from '@/hooks/useToast';
import Icon from '@/components/Icon';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import { OpenAiModelEnum } from '@/constants/model';
import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global';
@@ -75,9 +74,9 @@ const Chat = () => {
scrollToBottom();
setLoading(false);
},
onError() {
onError(e: any) {
toast({
title: '初始化异常,请刷新',
title: e?.message || '初始化异常,请检查地址',
status: 'error',
isClosable: true,
duration: 5000
@@ -124,36 +123,55 @@ const Chat = () => {
return new Promise((resolve, reject) => {
const event = getChatGPTSendEvent(chatId, windowId);
event.onmessage = ({ data }) => {
if (data === '[DONE]') {
event.close();
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
resolve('');
} else if (data) {
const msg = data.replace(/<br\/>/g, '\n');
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
value: item.value + msg
};
})
);
}
};
event.onerror = (err) => {
console.error(err, '===');
// 30s 收不到消息就报错
let timer = setTimeout(() => {
event.close();
reject('对话出现错误');
reject('服务器超时');
}, 300000);
event.addEventListener('responseData', ({ data }) => {
/* 重置定时器 */
clearTimeout(timer);
timer = setTimeout(() => {
event.close();
reject('服务器超时');
}, 300000);
const msg = data.replace(/<br\/>/g, '\n');
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
value: item.value + msg
};
})
);
});
event.addEventListener('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.error(err, '===');
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
});
event.onerror = (err) => {
clearTimeout(timer);
event.close();
console.error(err);
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
};
});
},
@@ -320,6 +338,16 @@ const Chat = () => {
</Box>
))}
</Box>
{/* 空内容提示 */}
{/* {
chatList.length === 0 && (
<>
<Card>
内容太长
</Card>
</>
)
} */}
<Box
m={media('20px auto', '0 auto')}
w={media('100vw', '100%')}