Compare commits

..

27 Commits
v0.9 ... 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
17cbfa05d3 perf: 表格样式。代理配置 2023-03-12 00:06:12 +08:00
Archer
a7b9940d7a Merge branch 'main' into beian 2023-03-11 16:58:46 +08:00
Archer
fd8135f50c feat: md数学表达式;perf: 字体样式;fix: 发送验证码错误提醒。聊天二次加载 2023-03-11 16:56:27 +08:00
Archer
9f96593136 README.md 2023-03-11 13:19:00 +08:00
Archer
ed9e72ec9a perf: logs 2023-03-10 22:12:13 +08:00
Archer
7fb76cde0b feat: 类型声明 2023-03-10 20:18:31 +08:00
Archer
38d49ea05f feat: 错误提示 2023-03-10 19:44:06 +08:00
Archer
7db87c2d09 Merge branch 'dev1.1' into beian 2023-03-10 18:57:18 +08:00
Archer
453f3be8ce fix: mongo内存泄漏 2023-03-10 18:54:51 +08:00
Archer
f023f63103 feat: 合并 2023-03-10 03:02:46 +08:00
Archer
65da4653bc feat: stream流响应 2023-03-10 02:57:13 +08:00
Archer
26888e855b rename 2023-03-09 21:37:56 +08:00
Archer
e132c622a6 feat: 文案内容 2023-03-09 21:23:58 +08:00
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
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
2390823282 feat: 限流配置 2023-03-04 13:30:20 +08:00
63 changed files with 1151 additions and 819 deletions

4
.gitignore vendored
View File

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

View File

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

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

128
README.md
View File

@@ -1,4 +1,6 @@
# Doc GPT
# Fast GPT
Fast GPT 允许你是用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
## 初始化
复制 .env.template 成 .env.local ,填写核心参数
@@ -6,68 +8,96 @@
```
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
```
```bash
pnpm dev
```
## 部署
```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
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
# 介绍页
## 部署
## 欢迎使用 Doc GPT
### docker 模式
请准备好 docker mongo代理, 和nginx。 镜像走本机的代理,所以用 network=hostport 改成代理的端口clash 一般都是 7890。
时间比较赶,介绍没来得及完善,先直接上怎么使用:
#### docker 打包
```bash
docker build -t imageName:tag .
docker push imageName:tag
```
1. 使用邮箱注册账号。
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
4. 在模型列表点击【对话】,即可使用 API 进行聊天。
#### 服务器拉取镜像和运行
```bash
# 服务器拉取部署, imageName 替换成镜像名
docker pull imageName:tag
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\
-e MAILE_CODE=your email code \
-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 安装
```bash
# 安装docker
curl -sSL https://get.daocloud.io/docker | sh
sudo systemctl start docker
```
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。
#### 软件教程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
# 检查 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
2. **单句最大长度**:每个聊天,单次输入内容的最大长度。
# 运行脚本: 删除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"
```
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拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg)
#### 软件教程Nginx
...没写,这个百度吧。

View File

@@ -1,5 +1,5 @@
{
"name": "docgpt",
"name": "fastgpt",
"version": "0.1.0",
"private": true,
"scripts": {
@@ -13,18 +13,15 @@
"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",
"crypto": "^1.0.1",
"dayjs": "^1.11.7",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"eventsource-parser": "^0.1.0",
"formidable": "^2.1.1",
"framer-motion": "^9.0.6",
"hyperdown": "^2.4.29",
@@ -40,11 +37,12 @@
"react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sass": "^1.58.3",
"sharp": "^0.31.3",
"tunnel": "^0.0.6",
"typescript": "4.9.5",
"uuid": "^9.0.0",
"zustand": "^4.3.5"
},
@@ -58,9 +56,12 @@
"@types/react-syntax-highlighter": "^15.5.6",
"@types/tunnel": "^0.0.3",
"@types/uuid": "^9.0.1",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"husky": "^8.0.3",
"lint-staged": "^13.1.2",
"prettier": "^2.8.4"
"prettier": "^2.8.4",
"typescript": "4.9.5"
},
"lint-staged": {
"./src/**/*.{ts,tsx,scss}": "npm run format"

692
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 { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
import axios from 'axios';
/**
* 获取一个聊天框的ID
@@ -35,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}`);
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

@@ -34,7 +34,7 @@ function responseSuccess(response: AxiosResponse<ResponseDataType>) {
*/
function checkRes(data: ResponseDataType) {
if (data === undefined) {
console.log(data, 'data is empty');
console.log('error->', data, 'data is empty');
return Promise.reject('服务器异常');
} else if (data.code < 200 || data.code >= 400) {
return Promise.reject(data.message);
@@ -46,24 +46,23 @@ function checkRes(data: ResponseDataType) {
* 响应错误
*/
function responseError(err: any) {
console.error('请求错误', err);
console.log('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

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

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

@@ -34,7 +34,7 @@ const Navbar = ({
>
{/* logo */}
<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 flex={1}>
@@ -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

@@ -48,7 +48,7 @@ const NavbarPhone = ({
<DrawerContent maxWidth={'50vw'}>
<DrawerBody p={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>
{navbarList.map((item) => (
<Flex

View File

@@ -1,5 +1,43 @@
import React from 'react';
export const codeLight: { [key: string]: React.CSSProperties } = {
'code[class*=language-]': {
color: '#d4d4d4',
textShadow: 'none',
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',
textShadow: 'none',
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': {
textShadow: 'none',
background: '#264f78'

View File

@@ -8,7 +8,7 @@
animation: blink 0.6s infinite;
}
.animation {
:last-child::after {
> :last-child::after {
display: inline-block;
content: '';
width: 4px;
@@ -107,7 +107,6 @@
font-size: 28px;
}
.markdown h2 {
border-bottom: 1px solid #cccccc;
color: #000000;
font-size: 24px;
}
@@ -235,12 +234,10 @@
}
.markdown table th,
.markdown table td {
border: 1px solid #cccccc;
padding: 6px 13px;
}
.markdown table tr {
background-color: #ffffff;
border-top: 1px solid #cccccc;
}
.markdown table tr:nth-child(2n) {
background-color: #f0f0f0;
@@ -329,7 +326,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;
@@ -343,7 +339,7 @@
background-color: #f0f0f0;
border: 1px solid #cccccc;
border-radius: 3px 3px 3px 3px;
font-size: 13px;
font-size: max(0.9em, 14px);
line-height: 19px;
overflow: auto;
padding: 6px 10px;
@@ -354,10 +350,11 @@
border: medium none;
}
.markdown {
font-size: 14px;
line-height: 1.6;
letter-spacing: 0.5px;
text-align: justify;
word-break: break-all;
overflow-y: hidden;
tab-size: 4;
word-spacing: normal;
pre {
display: block;
@@ -371,9 +368,57 @@
}
pre code {
background-color: #222;
background-color: #222 !important;
color: #fff;
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';
}
a {
text-decoration: underline;
color: var(--chakra-colors-blue-600);
}
table {
border-collapse: separate;
border-spacing: 0px;
thead tr:first-child th {
border-bottom-width: 1px;
border-left-width: 1px;
border-top-width: 1px;
border-color: #ccc;
background-color: rgba(236, 236, 241, 0.2);
overflow: hidden;
&:first-child {
border-top-left-radius: 0.375rem;
}
&:last-child {
border-right-width: 1px;
border-top-right-radius: 0.375rem;
}
}
td {
border-bottom-width: 1px;
border-left-width: 1px;
border-color: #ccc;
&:last-of-type {
border-right-width: 1px;
}
}
tbody tr:last-child {
overflow: hidden;
td {
&:first-child {
border-bottom-left-radius: 0.375rem;
}
&:last-child {
border-bottom-right-radius: 0.375rem;
}
}
}
}
}

View File

@@ -1,30 +1,36 @@
import React, { memo } from 'react';
import React, { memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import styles from './index.module.scss';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { codeLight } from './codeLight';
import { Box, Flex } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools';
import Icon from '@/components/Icon';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
import styles from './index.module.scss';
import { codeLight } from './codeLight';
const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => {
// const formatSource = useMemo(() => source.replace(/\n/g, '\n'), [source]);
const formatSource = useMemo(() => source, [source]);
const { copyData } = useCopyData();
// console.log(source);
return (
<ReactMarkdown
className={`${styles.markdown} ${
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
}`}
rehypePlugins={[remarkGfm]}
remarkPlugins={[remarkMath]}
rehypePlugins={[remarkGfm, rehypeKatex]}
components={{
pre: 'div',
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const code = String(children).replace(/\n$/, '');
return !inline ? (
<Box my={3} borderRadius={'md'} overflow={'hidden'}>
const code = String(children);
return !inline || match ? (
<Box my={3} borderRadius={'md'} overflow={'hidden'} backgroundColor={'#222'}>
<Flex
py={2}
px={5}
@@ -50,13 +56,14 @@ const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean
</Box>
) : (
<code className={className} {...props}>
{children}
{code}
</code>
);
}
}}
linkTarget="_blank"
>
{source}
{formatSource}
</ReactMarkdown>
);
};

View File

@@ -4,7 +4,9 @@ export enum EmailTypeEnum {
}
export const introPage = `
## 欢迎使用 Doc GPT
## 欢迎使用 Fast GPT
[Git 仓库](https://github.com/c121914yu/FastGPT)
时间比较赶,介绍没来得及完善,先直接上怎么使用:
1. 使用邮箱注册账号。
@@ -38,6 +40,5 @@ export const introPage = `
* 分享链接应为http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
### 其他问题
还有其他问题,可以加我 wx拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg)
还有其他问题,可以加我 wx: YNyiqi,拉个交流群大家一起聊聊。
`;

View File

@@ -20,24 +20,27 @@ const Button = defineStyleConfig({
baseStyle: {},
sizes: {
sm: {
fontSize: 'sm',
fontSize: 'xs',
px: 3,
py: 0,
fontWeight: 'normal',
height: '26px'
height: '26px',
lineHeight: '26px'
},
md: {
fontSize: 'md',
fontSize: 'sm',
px: 6,
py: 0,
height: '34px',
lineHeight: '34px',
fontWeight: 'normal'
},
lg: {
fontSize: 'lg',
fontSize: 'md',
px: 8,
py: 0,
height: '42px',
lineHeight: '42px',
fontWeight: 'normal'
}
},
@@ -58,17 +61,12 @@ export const theme = extendTheme({
global: {
'html, body': {
color: 'blackAlpha.800',
fontSize: '14px',
fontFamily:
'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',
height: '100%',
overflowY: 'auto'
maxHeight: '100vh',
overflowY: 'hidden'
}
}
},
fonts: {
body: 'system-ui, sans-serif'
},
fontSizes: {
xs: '0.8rem',
sm: '0.9rem',
@@ -84,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

@@ -1,4 +1,4 @@
import { useState, useRef } from 'react';
import { useCallback, useRef } from 'react';
import {
AlertDialog,
AlertDialogBody,
@@ -17,45 +17,51 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
const cancelCb = useRef<any>();
return {
openConfirm: (confirm?: any, cancel?: any) => {
onOpen();
confirmCb.current = confirm;
cancelCb.current = cancel;
},
ConfirmChild: () => (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
openConfirm: useCallback(
(confirm?: any, cancel?: any) => {
onOpen();
confirmCb.current = confirm;
cancelCb.current = cancel;
},
[onOpen]
),
ConfirmChild: useCallback(
() => (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
<AlertDialogBody>{content}</AlertDialogBody>
<AlertDialogBody>{content}</AlertDialogBody>
<AlertDialogFooter>
<Button
colorScheme={'gray'}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
}}
>
</Button>
<Button
colorScheme="blue"
ml={3}
onClick={() => {
onClose();
typeof confirmCb.current === 'function' && confirmCb.current();
}}
>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
<AlertDialogFooter>
<Button
colorScheme={'gray'}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
}}
>
</Button>
<Button
colorScheme="blue"
ml={4}
onClick={() => {
onClose();
typeof confirmCb.current === 'function' && confirmCb.current();
}}
>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</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';
export const useLoading = (props?: { defaultLoading: boolean }) => {
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
const Loading = ({
loading,
fixed = true
}: {
loading?: boolean;
fixed?: boolean;
}): JSX.Element | null => {
return isLoading || loading ? (
<Flex
position={fixed ? 'fixed' : 'absolute'}
zIndex={100}
backgroundColor={'rgba(255,255,255,0.5)'}
top={0}
left={0}
right={0}
bottom={0}
alignItems={'center'}
justifyContent={'center'}
>
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
</Flex>
) : null;
};
const Loading = useCallback(
({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
return isLoading || loading ? (
<Flex
position={fixed ? 'fixed' : 'absolute'}
zIndex={100}
backgroundColor={'rgba(255,255,255,0.5)'}
top={0}
left={0}
right={0}
bottom={0}
alignItems={'center'}
justifyContent={'center'}
>
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
</Flex>
) : null;
},
[isLoading]
);
return {
isLoading,
setIsLoading,
Loading: memo(Loading)
Loading
};
};

View File

@@ -1,14 +1,11 @@
import { useState, useMemo, useCallback } from 'react';
import { sendCodeToEmail } from '@/api/user';
import { EmailTypeEnum } from '@/constants/common';
import { useToast } from '@chakra-ui/react';
let timer: any;
import { useToast } from './useToast';
export const useSendCode = () => {
const toast = useToast({
position: 'top',
duration: 2000
});
const { toast } = useToast();
const [codeSending, setCodeSending] = useState(false);
const [codeCountDown, setCodeCountDown] = useState(0);
const sendCodeText = useMemo(() => {
@@ -43,13 +40,11 @@ export const useSendCode = () => {
status: 'success',
position: 'top'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
} catch (error: any) {
toast({
title: error.message || '发送验证码异常',
status: 'error'
});
}
setCodeSending(false);
},

View File

@@ -15,23 +15,23 @@ Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
export default function App({ Component, pageProps }: AppProps) {
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
cacheTime: 0
}
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
cacheTime: 0
}
});
}
});
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>Doc GPT</title>
<meta name="description" content="Generated by Doc GPT" />
<title>Fast GPT</title>
<meta name="description" content="Generated by Fast GPT" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"

View File

@@ -1,24 +1,24 @@
// 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 { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
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';
import { httpsAgent } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
/* 发送提示词 */
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'
});
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('缺少参数');
}
@@ -28,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
@@ -44,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],
@@ -53,12 +54,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 第一句话,强调代码类型
formatPrompts.unshift({
role: ChatCompletionRequestMessageRoleEnum.System,
content:
'If the content is code or code blocks, please label the code type as accurately as possible.'
content: '如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
});
// 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
@@ -67,50 +68,51 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
messages: formatPrompts,
stream: true
},
openaiProxy
{
timeout: 20000,
responseType: 'stream',
httpsAgent
}
);
console.log(
formatPrompts.reduce((sum, item) => sum + item.content.length, 0),
'response success'
);
// 截取字符串内容
const reg = /{"content"(.*)"}/g;
// @ts-ignore
const match = chatResponse.data.match(reg);
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.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 onParse = async (event: ParsedEvent | ReconnectInterval) => {
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;
}
};
for await (const chunk of chatResponse.data as any) {
const parser = createParser(onParse);
parser.feed(decodeURIComponent(chunk));
}
res.write(`data: [DONE]\n\n`);
// 存入库
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
res.end();
pass.push(null);
} catch (err: any) {
console.log(err?.response?.data || err);
// 删除最一条数据库记录, 也就是预发送的那一条
await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 },
updateTime: Date.now()
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
res.end();
}
}

View File

@@ -5,7 +5,7 @@ import { connectToDatabase, Chat } from '@/service/mongo';
import type { ModelType } from '@/types/model';
import { getOpenAIApi } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -66,7 +66,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
presence_penalty: 0.6,
stop: ['###']
},
openaiProxy
{
httpsAgent
}
);
const responseMessage = response.data.choices[0]?.text;

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.log(err);
jsonRes(res, {
code: 500,
error: err

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

@@ -5,7 +5,7 @@ import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import { TrainingStatusEnum } from '@/constants/model';
import { getOpenAIApi } from '@/service/utils/chat';
import { TrainingItemType } from '@/types/training';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -47,12 +47,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
if (training) {
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
// 获取训练记录
const tuneRecord = await openai.retrieveFineTune(training.tuneId, openaiProxy);
const tuneRecord = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
// 删除训练文件
openai.deleteFile(tuneRecord.data.training_files[0].id, openaiProxy);
openai.deleteFile(tuneRecord.data.training_files[0].id, { httpsAgent });
// 取消训练
openai.cancelFineTune(training.tuneId, openaiProxy);
openai.cancelFineTune(training.tuneId, { httpsAgent });
}
// 删除对应训练记录

View File

@@ -9,7 +9,7 @@ import fs from 'fs';
import type { ModelType } from '@/types/model';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式
export const config = {

View File

@@ -7,7 +7,7 @@ import type { ModelType } from '@/types/model';
import { TrainingItemType } from '@/types/training';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
/* 更新训练状态 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -46,11 +46,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
// 获取 openai 的训练情况
const { data } = await openai.retrieveFineTune(training.tuneId, openaiProxy);
const { data } = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
if (data.status === OpenAiTuneStatusEnum.succeeded) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, openaiProxy);
openai.deleteFile(data.training_files[0].id, { httpsAgent });
// 更新模型
await Model.findByIdAndUpdate(modelId, {
@@ -74,7 +74,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (data.status === OpenAiTuneStatusEnum.cancelled) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, openaiProxy);
openai.deleteFile(data.training_files[0].id, { httpsAgent });
// 更新模型
await Model.findByIdAndUpdate(modelId, {

View File

@@ -10,7 +10,7 @@ import fs from 'fs';
import type { ModelType } from '@/types/model';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式
export const config = {
@@ -73,7 +73,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// @ts-ignore
fs.createReadStream(file.filepath),
'fine-tune',
openaiProxy
{ httpsAgent }
);
uploadFileId = uploadRes.data.id; // 记录上传文件的 ID
@@ -84,7 +84,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: trainingType,
suffix: model.name
},
openaiProxy
{ httpsAgent }
);
trainId = trainRes.data.id; // 记录训练 ID
@@ -114,9 +114,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// @ts-ignore
if (openai) {
// @ts-ignore
uploadFileId && openai.deleteFile(uploadFileId, openaiProxy);
uploadFileId && openai.deleteFile(uploadFileId, { httpsAgent });
// @ts-ignore
trainId && openai.cancelFineTune(trainId, openaiProxy);
trainId && openai.cancelFineTune(trainId, { httpsAgent });
}
jsonRes(res, {

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

@@ -8,7 +8,7 @@ import { getOpenAIApi } from '@/service/utils/chat';
import { getUserOpenaiKey } from '@/service/utils/tools';
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
import { sendTrainSucceed } from '@/service/utils/sendEmail';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -23,10 +23,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const response = await Promise.all(
trainingRecords.map(async (item) => {
const { data } = await openai.retrieveFineTune(item.tuneId, openaiProxy);
const { data } = await openai.retrieveFineTune(item.tuneId, { httpsAgent });
if (data.status === OpenAiTuneStatusEnum.succeeded) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, openaiProxy);
openai.deleteFile(data.training_files[0].id, { httpsAgent });
const model = await Model.findById(item.modelId).populate({
path: 'userId',

View File

@@ -20,7 +20,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (type === EmailTypeEnum.register) {
const maxCount = process.env.MAX_USER ? +process.env.MAX_USER : Infinity;
const userCount = await User.count();
if (userCount >= maxCount) {
throw new Error('当前注册用户已满,请等待名额~');
}

View File

@@ -10,7 +10,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

@@ -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,33 +1,26 @@
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';
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';
import { streamFetch } from '@/api/fetch';
const Markdown = dynamic(() => import('@/components/Markdown'));
const textareaMinH = '22px';
const Chat = () => {
const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
const { toast } = useToast();
const router = useRouter();
const { isPc, media } = useScreen();
const { chatId, windowId } = router.query as { chatId: string; windowId?: string };
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
@@ -41,7 +34,6 @@ const Chat = () => {
// 滚动到底部
const scrollToBottom = useCallback(() => {
// 滚动到底部
setTimeout(() => {
ChatBox.current &&
ChatBox.current.scrollTo({
@@ -53,16 +45,14 @@ const Chat = () => {
// 初始化聊天框
useQuery(
[chatId, windowId],
['initData'],
() => {
if (!chatId) return null;
setLoading(true);
return getInitChatSiteInfo(chatId, windowId);
},
{
cacheTime: 5 * 60 * 1000,
onSuccess(res) {
if (!res) return;
// 可能没有 windowId给它设置一下
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
setChatSiteData(res.chatSite);
@@ -73,20 +63,38 @@ const Chat = () => {
}))
);
scrollToBottom();
setLoading(false);
},
onError() {
onError(e: any) {
toast({
title: '初始化异常,请刷新',
title: e?.message || '初始化异常,请检查地址',
status: 'error',
isClosable: true,
duration: 5000
});
},
onSettled() {
setLoading(false);
}
}
);
// 重置输入内容
const resetInputVal = useCallback((val: string) => {
setInputVal(val);
setTimeout(() => {
/* 回到最小高度 */
if (TextareaDom.current) {
TextareaDom.current.style.height =
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
}
}, 100);
}, []);
// 重载对话
const resetChat = useCallback(() => {
window.open(`/chat?chatId=${chatId}`, '_self');
}, [chatId]);
// gpt3 方法
const gpt3ChatPrompt = useCallback(
async (newChatList: ChatSiteItemType[]) => {
@@ -115,49 +123,64 @@ const Chat = () => {
const chatGPTPrompt = useCallback(
async (newChatList: ChatSiteItemType[]) => {
if (!windowId) return;
/* 预请求,把消息存入库 */
await postChatGptPrompt({
windowId,
prompt: newChatList[newChatList.length - 1],
chatId
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 + text
};
})
);
}
});
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, '===');
event.close();
reject('对话出现错误');
};
});
// 保存对话信息
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]
);
/**
@@ -191,16 +214,8 @@ const Chat = () => {
// 插入内容
setChatList(newChatList);
setInputVal('');
// 滚动到底部
setTimeout(() => {
scrollToBottom();
/* 回到最小高度 */
if (TextareaDom.current) {
TextareaDom.current.style.height = textareaMinH;
}
}, 100);
resetInputVal('');
scrollToBottom();
const fnMap: { [key: string]: any } = {
[OpenAiModelEnum.GPT35]: chatGPTPrompt,
@@ -218,15 +233,15 @@ const Chat = () => {
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
});
setInputVal(storeInput);
resetInputVal(storeInput);
setChatList(newChatList.slice(0, newChatList.length - 2));
}
@@ -237,6 +252,7 @@ const Chat = () => {
gpt3ChatPrompt,
inputVal,
isChatting,
resetInputVal,
scrollToBottom,
toast
]);
@@ -245,19 +261,13 @@ const Chat = () => {
const reEdit = useCallback(async () => {
if (chatList[chatList.length - 1]?.obj !== 'Human') return;
// 删除数据库最后一句
delLastMessage(windowId);
await delLastMessage(windowId);
const val = chatList[chatList.length - 1].value;
setInputVal(val);
resetInputVal(val);
setChatList(chatList.slice(0, -1));
setTimeout(() => {
if (TextareaDom.current) {
TextareaDom.current.style.height = val.split('\n').length * 22 + 'px';
}
}, 100);
}, [chatList, windowId]);
}, [chatList, resetInputVal, windowId]);
return (
<Flex height={'100%'} flexDirection={'column'}>
@@ -271,13 +281,9 @@ const Chat = () => {
zIndex={1}
>
<Box flex={1}>{chatSiteData?.name}</Box>
{/* 重置按键 */}
<Box cursor={'pointer'} onClick={() => router.replace(`/chat?chatId=${chatId}`)}>
<Icon name={'icon-zhongzhi'} width={20} height={20} color={'#718096'}></Icon>
</Box>
{/* 滚动到底部按键 */}
{ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && (
<Box ml={10} cursor={'pointer'} onClick={scrollToBottom}>
<Box mr={10} cursor={'pointer'} onClick={scrollToBottom}>
<Icon
name={'icon-xiangxiazhankai-xianxingyuankuang'}
width={25}
@@ -286,6 +292,10 @@ const Chat = () => {
></Icon>
</Box>
)}
{/* 重置按键 */}
<Button size={'sm'} colorScheme={'gray'} onClick={resetChat}>
</Button>
</Flex>
{/* 聊天内容 */}
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} px={0} pb={10} overflowY={'auto'}>
@@ -293,20 +303,20 @@ const Chat = () => {
<Box
key={index}
py={media(9, 6)}
px={media(4, 3)}
px={media(4, 2)}
backgroundColor={index % 2 === 0 ? 'rgba(247,247,248,1)' : '#fff'}
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}>
<Box mr={media(4, 1)}>
<Image
src={item.obj === 'Human' ? '/imgs/human.png' : '/imgs/modelAvatar.png'}
alt="/imgs/modelAvatar.png"
width={30}
height={30}
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
alt="/icon/logo.png"
width={media(30, 20)}
height={media(30, 20)}
/>
</Box>
<Box flex={'1 0 0'} w={0} overflowX={'auto'}>
<Box flex={'1 0 0'} w={0} overflow={'hidden'}>
{item.obj === 'AI' ? (
<Markdown
source={item.value}
@@ -320,6 +330,16 @@ const Chat = () => {
</Box>
))}
</Box>
{/* 空内容提示 */}
{/* {
chatList.length === 0 && (
<>
<Card>
内容太长
</Card>
</>
)
} */}
<Box
m={media('20px auto', '0 auto')}
w={media('100vw', '100%')}
@@ -331,11 +351,7 @@ const Chat = () => {
<Box textAlign={'center'}>
<Box color={'red'}></Box>
<Flex py={5} justifyContent={'center'}>
<Button
mr={20}
onClick={() => router.replace(`/chat?chatId=${chatId}`)}
colorScheme={'green'}
>
<Button mr={20} onClick={resetChat} colorScheme={'green'}>
</Button>
<Button onClick={reEdit}></Button>
@@ -409,3 +425,12 @@ const Chat = () => {
};
export default Chat;
export async function getServerSideProps(context: any) {
const chatId = context.query?.chatId || '';
const windowId = context.query?.windowId || '';
return {
props: { chatId, windowId }
};
}

View File

@@ -50,10 +50,6 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, code, password }: RegisterType) => {
setRequesting(true);
try {
toast({
title: `密码已找回`,
status: 'success'
});
loginSuccess(
await postFindPassword({
email,
@@ -61,13 +57,15 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
password
})
);
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
toast({
title: `密码已找回`,
status: 'success'
});
} catch (error: any) {
toast({
title: error.message || '修改密码异常',
status: 'error'
});
}
setRequesting(false);
},
@@ -77,7 +75,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT
FastGPT
</Box>
<form onSubmit={handleSubmit(onclickFindPassword)}>
<FormControl mt={8} isInvalid={!!errors.email}>

View File

@@ -32,23 +32,21 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, password }: LoginFormType) => {
setRequesting(true);
try {
toast({
title: '登录成功',
status: 'success'
});
loginSuccess(
await postLogin({
email,
password
})
);
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
toast({
title: '登录成功',
status: 'success'
});
} catch (error: any) {
toast({
title: error.message || '登录异常',
status: 'error'
});
}
setRequesting(false);
},
@@ -58,7 +56,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT
FastGPT
</Box>
<form onSubmit={handleSubmit(onclickLogin)}>
<FormControl mt={8} isInvalid={!!errors.email}>

View File

@@ -50,10 +50,6 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
async ({ email, password, code }: RegisterType) => {
setRequesting(true);
try {
toast({
title: `注册成功`,
status: 'success'
});
loginSuccess(
await postRegister({
email,
@@ -61,14 +57,15 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
password
})
);
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
duration: 4000,
isClosable: true
});
toast({
title: `注册成功`,
status: 'success'
});
} catch (error: any) {
toast({
title: error.message || '注册异常',
status: 'error'
});
}
setRequesting(false);
},
@@ -78,7 +75,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT
FastGPT
</Box>
<form onSubmit={handleSubmit(onclickRegister)}>
<FormControl mt={8} isInvalid={!!errors.email}>

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import styles from './index.module.scss';
import { Box, Flex, Image } from '@chakra-ui/react';
import { PageTypeEnum } from '@/constants/user';
@@ -21,7 +21,7 @@ const Login = () => {
const loginSuccess = useCallback(
(res: ResLogin) => {
setUserInfo(res.user, res.token);
router.push('/');
router.push('/model/list');
},
[router, setUserInfo]
);
@@ -38,6 +38,10 @@ const Login = () => {
return <Component setPageType={setPageType} loginSuccess={loginSuccess} />;
}
useEffect(() => {
router.prefetch('/model/list');
}, [router]);
return (
<Box className={styles.loginPage} h={'100%'} p={isPc ? '10vh 10vw' : 0}>
<Flex

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Button, Flex, Heading, Tag } from '@chakra-ui/react';
import React, { useEffect } from 'react';
import { Box, Button, Flex, Tag } from '@chakra-ui/react';
import type { ModelType } from '@/types/model';
import { formatModelStatus } from '@/constants/model';
import dayjs from 'dayjs';
@@ -14,6 +14,10 @@ const ModelPhoneList = ({
}) => {
const router = useRouter();
useEffect(() => {
router.prefetch('/chat');
}, [router]);
return (
<Box borderRadius={'md'} overflow={'hidden'} mb={5}>
{models.map((model) => (

View File

@@ -1,4 +1,4 @@
import React from 'react';
import { useEffect } from 'react';
import {
Button,
Table,
@@ -84,6 +84,10 @@ const ModelTable = ({
}
];
useEffect(() => {
router.prefetch('/chat');
}, [router]);
return (
<Card py={3}>
<TableContainer>

View File

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

View File

@@ -41,17 +41,16 @@ const ModelDetail = () => {
const res = await getModelById(modelId as string);
res.security.expiredTime /= 60 * 60 * 1000;
setModel(res);
console.log(res);
} catch (err) {
console.log(err);
console.log('error->', err);
}
setLoading(false);
}, [modelId, setLoading]);
useEffect(() => {
loadModel();
}, [loadModel, modelId]);
router.prefetch('/chat');
}, [loadModel, modelId, router]);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
@@ -65,7 +64,7 @@ const ModelDetail = () => {
});
router.replace('/model/list');
} catch (err) {
console.log(err);
console.log('error->', err);
}
setLoading(false);
}, [setLoading, model, router, toast]);
@@ -79,7 +78,7 @@ const ModelDetail = () => {
router.push(`/chat?chatId=${chatId}`);
} catch (err) {
console.log(err);
console.log('error->', err);
}
setLoading(false);
}, [setLoading, model, router]);
@@ -107,7 +106,7 @@ const ModelDetail = () => {
title: typeof err === 'string' ? err : '文件格式错误',
status: 'error'
});
console.log(err);
console.log('error->', err);
}
setLoading(false);
},
@@ -122,11 +121,15 @@ const ModelDetail = () => {
try {
await putModelTrainingStatus(model._id);
loadModel();
} catch (error) {
console.log(error);
} catch (error: any) {
console.log('error->', error);
toast({
title: error.message || '更新失败',
status: 'error'
});
}
setLoading(false);
}, [setLoading, loadModel, model]);
}, [model, setLoading, loadModel, toast]);
return (
<>

View File

@@ -10,10 +10,12 @@ import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import dynamic from 'next/dynamic';
import { useToast } from '@/hooks/useToast';
const CreateModel = dynamic(() => import('./components/CreateModel'));
const ModelList = () => {
const { toast } = useToast();
const { isPc } = useScreen();
const router = useRouter();
const [models, setModels] = useState<ModelType[]>([]);
@@ -43,12 +45,16 @@ const ModelList = () => {
router.push(`/chat?chatId=${chatId}`, undefined, {
shallow: true
});
} catch (err) {
console.log(err);
} catch (err: any) {
console.log('error->', err);
toast({
title: err.message || '出现一些异常',
status: 'error'
});
}
setIsLoading(false);
},
[router, setIsLoading]
[router, setIsLoading, toast]
);
return (

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

@@ -1,3 +1,10 @@
export const openaiError: Record<string, string> = {
context_length_exceeded: '内容超出长度'
context_length_exceeded: '内容超长了,请重置对话',
Unauthorized: 'API-KEY 不合法',
rate_limit_reached: '同时访问用户过多,请稍后再试',
'Bad Request': '上下文太多了,请重开对话~'
};
export const proxyError: Record<string, boolean> = {
ECONNABORTED: true,
ECONNRESET: true
};

View File

@@ -1,18 +1,28 @@
import mongoose from 'mongoose';
import type { Mongoose } from 'mongoose';
let cachedClient: Mongoose;
export async function connectToDatabase() {
if (cachedClient && cachedClient.connection.readyState === 1) {
return cachedClient;
/**
* 连接 MongoDB 数据库
*/
export async function connectToDatabase(): Promise<void> {
if (global.mongodb) {
return;
}
cachedClient = await mongoose.connect(process.env.MONGODB_URI as string, {
dbName: 'doc_gpt'
});
return cachedClient;
global.mongodb = 'connecting';
console.log('connect mongo');
try {
mongoose.set('strictQuery', true);
global.mongodb = await mongoose.connect(process.env.MONGODB_URI as string, {
bufferCommands: true,
dbName: 'doc_gpt',
maxPoolSize: 5,
minPoolSize: 1,
maxConnecting: 5
});
} catch (error) {
console.log('error->', 'mongo connect error');
global.mongodb = null;
}
}
export * from './models/authCode';

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;
@@ -20,12 +20,16 @@ export const jsonRes = (
let msg = message;
if ((code < 200 || code >= 400) && !message) {
msg =
typeof error === 'string'
? error
: openaiError[error?.response?.data?.message] || error?.message || '请求错误';
msg = error?.message || '请求错误';
if (typeof error === 'string') {
msg = error;
} else if (proxyError[error?.code]) {
msg = '服务器代理出错';
} else if (openaiError[error?.response?.statusText]) {
msg = openaiError[error.response.statusText];
}
console.log(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

@@ -15,26 +15,26 @@ let mailTransport = nodemailer.createTransport({
const emailMap: { [key: string]: any } = {
[EmailTypeEnum.register]: {
subject: '注册 DocGPT 账号',
html: (code: string) => `<div>您正在注册 DocGPT 账号,验证码为:${code}</div>`
subject: '注册 FastGPT 账号',
html: (code: string) => `<div>您正在注册 FastGPT 账号,验证码为:${code}</div>`
},
[EmailTypeEnum.findPassword]: {
subject: '修改 DocGPT 密码',
html: (code: string) => `<div>您正在修改 DocGPT 账号密码,验证码为:${code}</div>`
subject: '修改 FastGPT 密码',
html: (code: string) => `<div>您正在修改 FastGPT 账号密码,验证码为:${code}</div>`
}
};
export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`) => {
return new Promise((resolve, reject) => {
const options = {
from: `"DocGPT" ${myEmail}`,
from: `"FastGPT" ${myEmail}`,
to: email,
subject: emailMap[type]?.subject,
html: emailMap[type]?.html(code)
};
mailTransport.sendMail(options, function (err, msg) {
if (err) {
console.log(err);
console.log('error->', err);
reject('邮箱异常');
} else {
resolve('');
@@ -46,14 +46,14 @@ export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`)
export const sendTrainSucceed = (email: string, modelName: string) => {
return new Promise((resolve, reject) => {
const options = {
from: `"DocGPT" ${myEmail}`,
from: `"FastGPT" ${myEmail}`,
to: email,
subject: '模型训练完成通知',
html: `你的模型 ${modelName} 已于 ${dayjs().format('YYYY-MM-DD HH:mm')} 训练完成!`
};
mailTransport.sendMail(options, function (err, msg) {
if (err) {
console.log(err);
console.log('error->', err);
reject('邮箱异常');
} else {
resolve('');

View File

@@ -49,15 +49,12 @@ export const getUserOpenaiKey = async (userId: string) => {
};
/* 代理 */
export const openaiProxy: any =
process.env.AXIOS_PROXY_PORT && process.env.AXIOS_PROXY_HOST
? {
httpsAgent: tunnel.httpsOverHttp({
proxy: {
host: process.env.AXIOS_PROXY_HOST,
port: +process.env.AXIOS_PROXY_PORT
}
}),
proxy: false
}
export const httpsAgent =
process.env.AXIOS_PROXY_HOST && process.env.AXIOS_PROXY_PORT
? tunnel.httpsOverHttp({
proxy: {
host: process.env.AXIOS_PROXY_HOST,
port: +process.env.AXIOS_PROXY_PORT
}
})
: undefined;

View File

@@ -50,6 +50,9 @@ svg {
}
@media (max-width: 900px) {
html {
font-size: 14px;
}
::-webkit-scrollbar,
::-webkit-scrollbar {
width: 2px;

6
src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
import type { Mongoose } from 'mongoose';
declare global {
var mongodb: Mongoose | string | null;
}
export {};

View File

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

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

View File

@@ -19,6 +19,6 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts"],
"exclude": ["node_modules"]
}