diff --git a/README.md b/README.md
index 545156d90..6a9f458f7 100644
--- a/README.md
+++ b/README.md
@@ -15,22 +15,62 @@ TOKEN_KEY=随便填一个,用于生成和校验token
```bash
pnpm dev
```
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## 部署
+
```bash
# 本地 docker 打包
-docker build -t imageName .
-docker push imageName
-
-# 服务器拉取部署
-docker pull imageName
-docker stop doc-gpt || true
-docker rm doc-gpt || true
-# 运行时才把参数写入
-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 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 \
+ -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.
# 介绍页
diff --git a/src/api/chat.ts b/src/api/chat.ts
index 9184b415a..2d49c674f 100644
--- a/src/api/chat.ts
+++ b/src/api/chat.ts
@@ -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()}`);
/**
* 删除最后一句
diff --git a/src/api/request.ts b/src/api/request.ts
index 5f8db54cc..a5df11ee5 100644
--- a/src/api/request.ts
+++ b/src/api/request.ts
@@ -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);
}
/* 创建请求实例 */
diff --git a/src/components/Layout/navbar.tsx b/src/components/Layout/navbar.tsx
index 97bc13b28..dd9dc8517 100644
--- a/src/components/Layout/navbar.tsx
+++ b/src/components/Layout/navbar.tsx
@@ -46,6 +46,7 @@ const Navbar = ({
alignItems={'center'}
justifyContent={'center'}
onClick={() =>
+ !item.activeLink.includes(router.pathname) &&
router.push(item.link, undefined, {
shallow: true
})
diff --git a/src/components/Markdown/index.module.scss b/src/components/Markdown/index.module.scss
index 4273e0d42..d3b64603a 100644
--- a/src/components/Markdown/index.module.scss
+++ b/src/components/Markdown/index.module.scss
@@ -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;
diff --git a/src/pages/api/chat/chatGpt.ts b/src/pages/api/chat/chatGpt.ts
index 1627c7b1b..82fca36e7 100644
--- a/src/pages/api/chat/chatGpt.ts
+++ b/src/pages/api/chat/chatGpt.ts
@@ -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, '
'); // 无法直接传输\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, '
'); // 无法直接传输\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);
}
diff --git a/src/pages/api/chat/init.ts b/src/pages/api/chat/init.ts
index 27b75e826..c32e57fd8 100644
--- a/src/pages/api/chat/init.ts
+++ b/src/pages/api/chat/init.ts
@@ -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
diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx
index d79164384..00a7b8cb1 100644
--- a/src/pages/chat/index.tsx
+++ b/src/pages/chat/index.tsx
@@ -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(/
/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(/
/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 = () => {
))}
+ {/* 空内容提示 */}
+ {/* {
+ chatList.length === 0 && (
+ <>
+
+内容太长
+
+ >
+ )
+ } */}