Compare commits

..

2 Commits
v0.9 ... v1.0

Author SHA1 Message Date
Archer
17364e9da3 conflict
perf: 聊天页优化

perf: md解析样式

perf: ui调整

perf: 懒加载和动态加载优化

perf: 去除console,

perf: 图片cdn

feat: 图片地址

perf: 登录顺序

feat: 流优化
2023-03-09 20:44:54 +08:00
archer
2390823282 feat: 限流配置 2023-03-04 13:30:20 +08:00
37 changed files with 773 additions and 468 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,5 +0,0 @@
{
"editor.formatOnType": true,
"editor.formatOnSave": true ,
"prettier.tabWidth": 2
}

View File

@@ -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 pull imageName 服务器请准备好 docker mongonginx和代理。 镜像走本机的代理,所以用 hostport改成代理的端口clash一般都是7890。
docker stop doc-gpt || true
docker rm doc-gpt || true ```bash
# 运行时才把参数写入 # 服务器拉取部署, imageName 替换成镜像名
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 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 \
-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.
# 介绍页 # 介绍页
@@ -70,4 +110,4 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
### 其他问题 ### 其他问题
还有其他问题,可以加我 wx拉个交流群大家一起聊聊。 还有其他问题,可以加我 wx拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg) ![](/icon/erweima.jpg)

View File

@@ -6,7 +6,17 @@ const isDev = process.env.NODE_ENV === 'development';
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone',
reactStrictMode: false, reactStrictMode: false,
compress: true compress: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'docgpt-1301319986.cos.ap-shanghai.myqcloud.com',
port: '',
pathname: '/**'
}
]
}
}; };
module.exports = nextConfig; module.exports = nextConfig;

View File

@@ -23,8 +23,6 @@
"axios": "^1.3.3", "axios": "^1.3.3",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"framer-motion": "^9.0.6", "framer-motion": "^9.0.6",
"hyperdown": "^2.4.29", "hyperdown": "^2.4.29",
@@ -40,7 +38,9 @@
"react-hook-form": "^7.43.1", "react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sass": "^1.58.3", "sass": "^1.58.3",
"sharp": "^0.31.3", "sharp": "^0.31.3",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
@@ -58,6 +58,8 @@
"@types/react-syntax-highlighter": "^15.5.6", "@types/react-syntax-highlighter": "^15.5.6",
"@types/tunnel": "^0.0.3", "@types/tunnel": "^0.0.3",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^13.1.2", "lint-staged": "^13.1.2",
"prettier": "^2.8.4" "prettier": "^2.8.4"

612
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

BIN
public/icon/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -1,5 +1,6 @@
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 = ({
}); });
/* 获取 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()}`);
/** /**
* 删除最后一句 * 删除最后一句

View File

@@ -34,7 +34,7 @@ function responseSuccess(response: AxiosResponse<ResponseDataType>) {
*/ */
function checkRes(data: ResponseDataType) { function checkRes(data: ResponseDataType) {
if (data === undefined) { if (data === undefined) {
console.log(data, 'data is empty'); console.error(data, 'data is empty');
return Promise.reject('服务器异常'); return Promise.reject('服务器异常');
} else if (data.code < 200 || data.code >= 400) { } else if (data.code < 200 || data.code >= 400) {
return Promise.reject(data.message); return Promise.reject(data.message);
@@ -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);
} }
/* 创建请求实例 */ /* 创建请求实例 */

View File

@@ -39,7 +39,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
} }
}, },
onError(error) { onError(error) {
console.log(error); console.error(error);
router.push('/login'); router.push('/login');
toast(); toast();
}, },

View File

@@ -34,7 +34,7 @@ const Navbar = ({
> >
{/* logo */} {/* logo */}
<Box pb={4}> <Box pb={4}>
<Image src={'/logo.png'} width={'35'} height={'35'} alt=""></Image> <Image src={'/icon/logo.png'} width={'35'} height={'35'} alt=""></Image>
</Box> </Box>
{/* 导航列表 */} {/* 导航列表 */}
<Box flex={1}> <Box flex={1}>
@@ -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
}) })

View File

@@ -48,7 +48,7 @@ const NavbarPhone = ({
<DrawerContent maxWidth={'50vw'}> <DrawerContent maxWidth={'50vw'}>
<DrawerBody p={4}> <DrawerBody p={4}>
<Box py={4}> <Box py={4}>
<Image src={'/logo.png'} margin={'auto'} w={'35'} h={'35'} alt=""></Image> <Image src={'/icon/logo.png'} margin={'auto'} w={'35'} h={'35'} alt=""></Image>
</Box> </Box>
{navbarList.map((item) => ( {navbarList.map((item) => (
<Flex <Flex

View File

@@ -1,5 +1,47 @@
import React from 'react'; import React from 'react';
export const codeLight: { [key: string]: React.CSSProperties } = { export const codeLight: { [key: string]: React.CSSProperties } = {
'code[class*=language-]': {
color: '#d4d4d4',
fontSize: '13px',
textShadow: 'none',
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
direction: 'ltr',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
lineHeight: '1.5',
MozTabSize: '4',
OTabSize: '4',
tabSize: '4',
WebkitHyphens: 'none',
MozHyphens: 'none',
msHyphens: 'none',
hyphens: 'none'
},
'pre[class*=language-]': {
color: '#d4d4d4',
fontSize: '13px',
textShadow: 'none',
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
direction: 'ltr',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
lineHeight: '1.5',
MozTabSize: '4',
OTabSize: '4',
tabSize: '4',
WebkitHyphens: 'none',
MozHyphens: 'none',
msHyphens: 'none',
hyphens: 'none',
padding: '1em',
margin: '.5em 0',
overflow: 'auto',
background: '#1e1e1e'
},
'code[class*=language-] ::selection': { 'code[class*=language-] ::selection': {
textShadow: 'none', textShadow: 'none',
background: '#264f78' background: '#264f78'

View File

@@ -107,7 +107,6 @@
font-size: 28px; font-size: 28px;
} }
.markdown h2 { .markdown h2 {
border-bottom: 1px solid #cccccc;
color: #000000; color: #000000;
font-size: 24px; font-size: 24px;
} }
@@ -329,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;
@@ -376,4 +374,9 @@
width: 100%; width: 100%;
font-family: 'Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji'; font-family: 'Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji';
} }
a {
text-decoration: underline;
color: var(--chakra-colors-blue-600);
}
} }

View File

@@ -1,23 +1,26 @@
import React, { memo } from 'react'; import React, { memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { codeLight } from './codeLight'; import { codeLight } from './codeLight';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools'; import { useCopyData } from '@/utils/tools';
import Icon from '@/components/Icon'; import Icon from '@/components/Icon';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => { const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => {
// const formatSource = useMemo(() => source.replace(/\n/g, '\n'), [source]); const formatSource = useMemo(() => source.replace(/\n/g, ' \n'), [source]);
const { copyData } = useCopyData(); const { copyData } = useCopyData();
// console.log(source);
return ( return (
<ReactMarkdown <ReactMarkdown
className={`${styles.markdown} ${ className={`${styles.markdown} ${
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : '' isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
}`} }`}
rehypePlugins={[remarkGfm]} remarkPlugins={[remarkMath]}
rehypePlugins={[remarkGfm, rehypeKatex]}
components={{ components={{
pre: 'div', pre: 'div',
code({ node, inline, className, children, ...props }) { code({ node, inline, className, children, ...props }) {
@@ -55,8 +58,9 @@ const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean
); );
} }
}} }}
linkTarget="_blank"
> >
{source} {formatSource}
</ReactMarkdown> </ReactMarkdown>
); );
}; };

View File

@@ -38,6 +38,5 @@ export const introPage = `
* 分享链接应为http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764 * 分享链接应为http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
### 其他问题 ### 其他问题
还有其他问题,可以加我 wx拉个交流群大家一起聊聊。 还有其他问题,可以加我 wx: YNyiqi,拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg)
`; `;

View File

@@ -1,4 +1,4 @@
import { useState, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { import {
AlertDialog, AlertDialog,
AlertDialogBody, AlertDialogBody,
@@ -17,45 +17,51 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
const cancelCb = useRef<any>(); const cancelCb = useRef<any>();
return { return {
openConfirm: (confirm?: any, cancel?: any) => { openConfirm: useCallback(
onOpen(); (confirm?: any, cancel?: any) => {
confirmCb.current = confirm; onOpen();
cancelCb.current = cancel; confirmCb.current = confirm;
}, cancelCb.current = cancel;
ConfirmChild: () => ( },
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}> [onOpen]
<AlertDialogOverlay> ),
<AlertDialogContent> ConfirmChild: useCallback(
<AlertDialogHeader fontSize="lg" fontWeight="bold"> () => (
{title} <AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
</AlertDialogHeader> <AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
<AlertDialogBody>{content}</AlertDialogBody> <AlertDialogBody>{content}</AlertDialogBody>
<AlertDialogFooter> <AlertDialogFooter>
<Button <Button
colorScheme={'gray'} colorScheme={'gray'}
onClick={() => { onClick={() => {
onClose(); onClose();
typeof cancelCb.current === 'function' && cancelCb.current(); typeof cancelCb.current === 'function' && cancelCb.current();
}} }}
> >
</Button> </Button>
<Button <Button
colorScheme="blue" colorScheme="blue"
ml={3} ml={4}
onClick={() => { onClick={() => {
onClose(); onClose();
typeof confirmCb.current === 'function' && confirmCb.current(); typeof confirmCb.current === 'function' && confirmCb.current();
}} }}
> >
</Button> </Button>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialogOverlay> </AlertDialogOverlay>
</AlertDialog> </AlertDialog>
),
[content, isOpen, onClose, title]
) )
}; };
}; };

View File

@@ -1,36 +1,33 @@
import { useState, memo } from 'react'; import { useState, useCallback } from 'react';
import { Spinner, Flex } from '@chakra-ui/react'; import { Spinner, Flex } from '@chakra-ui/react';
export const useLoading = (props?: { defaultLoading: boolean }) => { export const useLoading = (props?: { defaultLoading: boolean }) => {
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false); const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
const Loading = ({ const Loading = useCallback(
loading, ({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
fixed = true return isLoading || loading ? (
}: { <Flex
loading?: boolean; position={fixed ? 'fixed' : 'absolute'}
fixed?: boolean; zIndex={100}
}): JSX.Element | null => { backgroundColor={'rgba(255,255,255,0.5)'}
return isLoading || loading ? ( top={0}
<Flex left={0}
position={fixed ? 'fixed' : 'absolute'} right={0}
zIndex={100} bottom={0}
backgroundColor={'rgba(255,255,255,0.5)'} alignItems={'center'}
top={0} justifyContent={'center'}
left={0} >
right={0} <Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
bottom={0} </Flex>
alignItems={'center'} ) : null;
justifyContent={'center'} },
> [isLoading]
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" /> );
</Flex>
) : null;
};
return { return {
isLoading, isLoading,
setIsLoading, setIsLoading,
Loading: memo(Loading) Loading
}; };
}; };

View File

@@ -15,18 +15,18 @@ Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done()); Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done()); Router.events.on('routeChangeError', () => NProgress.done());
export default function App({ Component, pageProps }: AppProps) { // Create a client
// Create a client const queryClient = new QueryClient({
const queryClient = new QueryClient({ defaultOptions: {
defaultOptions: { queries: {
queries: { refetchOnWindowFocus: false,
refetchOnWindowFocus: false, retry: false,
retry: false, cacheTime: 0
cacheTime: 0
}
} }
}); }
});
export default function App({ Component, pageProps }: AppProps) {
return ( return (
<> <>
<Head> <Head>

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 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';
/* 发送提示词 */ /* 发送提示词 */
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)
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,43 +85,75 @@ 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) {
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, { (async () => {
$push: { await ChatWindow.findByIdAndUpdate(windowId, {
content: { $push: {
obj: 'AI', content: {
value: AIResponse obj: 'AI',
} value: AIResponse
}, }
updateTime: Date.now() },
}); updateTime: Date.now()
});
res.end(); })();
} catch (err: any) { } catch (err: any) {
console.log(err?.response?.data || err); let errorText = err;
// 删除最一条数据库记录, 也就是预发送的那一条 if (err.code === 'ECONNRESET') {
await ChatWindow.findByIdAndUpdate(windowId, { errorText = '服务器代理出错';
$pop: { content: 1 }, } else {
updateTime: Date.now() 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('聊天框已过期'); throw new Error('聊天框已过期');
} }
@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
}); });
} catch (err) { } catch (err) {
console.log(err);
jsonRes(res, { jsonRes(res, {
code: 500, code: 500,
error: err error: err

View File

@@ -1,24 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
if (req.method !== 'GET') return;
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Encoding': 'none',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream'
});
let val = 0;
const timer = setInterval(() => {
console.log('发送消息', val);
res.write(`data: ${val++}\n\n`);
if (val > 30) {
clearInterval(timer);
res.write(`data: [DONE]\n\n`);
res.end();
}
}, 500);
}

View File

@@ -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 = () => {
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 = () => {
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();
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, '===');
event.close(); 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 : '对话出现不知名错误~');
}; };
}); });
}, },
@@ -300,8 +318,8 @@ const Chat = () => {
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}> <Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}>
<Box mr={media(4, 1)}> <Box mr={media(4, 1)}>
<Image <Image
src={item.obj === 'Human' ? '/imgs/human.png' : '/imgs/modelAvatar.png'} src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
alt="/imgs/modelAvatar.png" alt="/icon/logo.png"
width={30} width={30}
height={30} height={30}
/> />
@@ -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%')}

View File

@@ -50,10 +50,6 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, code, password }: RegisterType) => { async ({ email, code, password }: RegisterType) => {
setRequesting(true); setRequesting(true);
try { try {
toast({
title: `密码已找回`,
status: 'success'
});
loginSuccess( loginSuccess(
await postFindPassword({ await postFindPassword({
email, email,
@@ -61,6 +57,10 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
password password
}) })
); );
toast({
title: `密码已找回`,
status: 'success'
});
} catch (error) { } catch (error) {
typeof error === 'string' && typeof error === 'string' &&
toast({ toast({

View File

@@ -32,16 +32,16 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, password }: LoginFormType) => { async ({ email, password }: LoginFormType) => {
setRequesting(true); setRequesting(true);
try { try {
toast({
title: '登录成功',
status: 'success'
});
loginSuccess( loginSuccess(
await postLogin({ await postLogin({
email, email,
password password
}) })
); );
toast({
title: '登录成功',
status: 'success'
});
} catch (error) { } catch (error) {
typeof error === 'string' && typeof error === 'string' &&
toast({ toast({

View File

@@ -50,10 +50,6 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, password, code }: RegisterType) => { async ({ email, password, code }: RegisterType) => {
setRequesting(true); setRequesting(true);
try { try {
toast({
title: `注册成功`,
status: 'success'
});
loginSuccess( loginSuccess(
await postRegister({ await postRegister({
email, email,
@@ -61,6 +57,10 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
password password
}) })
); );
toast({
title: `注册成功`,
status: 'success'
});
} catch (error) { } catch (error) {
typeof error === 'string' && typeof error === 'string' &&
toast({ toast({

View File

@@ -34,7 +34,7 @@ const ModelEditForm = ({ model }: { model?: ModelType }) => {
status: 'success' status: 'success'
}); });
} catch (err) { } catch (err) {
console.log(err); console.error(err);
toast({ toast({
title: err as string, title: err as string,
status: 'success' status: 'success'

View File

@@ -29,7 +29,7 @@ const Training = ({ model }: { model: ModelType }) => {
const res = await getModelTrainings(id); const res = await getModelTrainings(id);
setRecords(res); setRecords(res);
} catch (error) { } catch (error) {
console.log(error); console.error(error);
} }
}, []); }, []);

View File

@@ -41,10 +41,8 @@ const ModelDetail = () => {
const res = await getModelById(modelId as string); const res = await getModelById(modelId as string);
res.security.expiredTime /= 60 * 60 * 1000; res.security.expiredTime /= 60 * 60 * 1000;
setModel(res); setModel(res);
console.log(res);
} catch (err) { } catch (err) {
console.log(err); console.error(err);
} }
setLoading(false); setLoading(false);
}, [modelId, setLoading]); }, [modelId, setLoading]);
@@ -65,7 +63,7 @@ const ModelDetail = () => {
}); });
router.replace('/model/list'); router.replace('/model/list');
} catch (err) { } catch (err) {
console.log(err); console.error(err);
} }
setLoading(false); setLoading(false);
}, [setLoading, model, router, toast]); }, [setLoading, model, router, toast]);
@@ -79,7 +77,7 @@ const ModelDetail = () => {
router.push(`/chat?chatId=${chatId}`); router.push(`/chat?chatId=${chatId}`);
} catch (err) { } catch (err) {
console.log(err); console.error(err);
} }
setLoading(false); setLoading(false);
}, [setLoading, model, router]); }, [setLoading, model, router]);
@@ -107,7 +105,7 @@ const ModelDetail = () => {
title: typeof err === 'string' ? err : '文件格式错误', title: typeof err === 'string' ? err : '文件格式错误',
status: 'error' status: 'error'
}); });
console.log(err); console.error(err);
} }
setLoading(false); setLoading(false);
}, },
@@ -123,7 +121,7 @@ const ModelDetail = () => {
await putModelTrainingStatus(model._id); await putModelTrainingStatus(model._id);
loadModel(); loadModel();
} catch (error) { } catch (error) {
console.log(error); console.error(error);
} }
setLoading(false); setLoading(false);
}, [setLoading, loadModel, model]); }, [setLoading, loadModel, model]);

View File

@@ -44,7 +44,7 @@ const ModelList = () => {
shallow: true shallow: true
}); });
} catch (err) { } catch (err) {
console.log(err); console.error(err);
} }
setIsLoading(false); setIsLoading(false);
}, },

View File

@@ -24,8 +24,8 @@ export const jsonRes = (
typeof error === 'string' typeof error === 'string'
? error ? error
: openaiError[error?.response?.data?.message] || error?.message || '请求错误'; : openaiError[error?.response?.data?.message] || error?.message || '请求错误';
console.error(error);
console.log(msg); console.error(msg);
} }
res.json({ res.json({

View File

@@ -34,7 +34,7 @@ export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`)
}; };
mailTransport.sendMail(options, function (err, msg) { mailTransport.sendMail(options, function (err, msg) {
if (err) { if (err) {
console.log(err); console.error(err);
reject('邮箱异常'); reject('邮箱异常');
} else { } else {
resolve(''); resolve('');
@@ -53,7 +53,7 @@ export const sendTrainSucceed = (email: string, modelName: string) => {
}; };
mailTransport.sendMail(options, function (err, msg) { mailTransport.sendMail(options, function (err, msg) {
if (err) { if (err) {
console.log(err); console.error(err);
reject('邮箱异常'); reject('邮箱异常');
} else { } else {
resolve(''); resolve('');

View File

@@ -21,7 +21,7 @@ export const useCopyData = () => {
duration: 1000 duration: 1000
}); });
} catch (error) { } catch (error) {
console.log(error); console.error(error);
toast({ toast({
title: '复制失败', title: '复制失败',
status: 'error' status: 'error'