feat: 合并

This commit is contained in:
Archer 2023-03-10 03:02:46 +08:00
commit f023f63103
No known key found for this signature in database
GPG Key ID: A3F5915562F98511
12 changed files with 5249 additions and 1230 deletions

View File

@ -1,4 +1,6 @@
# Doc GPT # Fast GPT
Fast GPT 允许你是用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
## 初始化 ## 初始化
复制 .env.template 成 .env.local ,填写核心参数 复制 .env.template 成 .env.local ,填写核心参数
@ -74,7 +76,7 @@ docker run -d --name mongo \
# 介绍页 # 介绍页
## 欢迎使用 Doc GPT ## 欢迎使用 Fast GPT
时间比较赶,介绍没来得及完善,先直接上怎么使用: 时间比较赶,介绍没来得及完善,先直接上怎么使用:

View File

@ -23,6 +23,7 @@
"axios": "^1.3.3", "axios": "^1.3.3",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"eventsource-parser": "^0.1.0",
"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",

6312
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -356,7 +356,7 @@
line-height: 1.6; line-height: 1.6;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-align: justify; text-align: justify;
word-break: break-all;
pre { pre {
display: block; display: block;
width: 100%; width: 100%;
@ -369,7 +369,7 @@
} }
pre code { pre code {
background-color: #222; background-color: #222 !important;
color: #fff; color: #fff;
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';

View File

@ -26,8 +26,8 @@ const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean
code({ node, inline, className, children, ...props }) { code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || ''); const match = /language-(\w+)/.exec(className || '');
const code = String(children).replace(/\n$/, ''); const code = String(children).replace(/\n$/, '');
return !inline ? ( return !inline || match ? (
<Box my={3} borderRadius={'md'} overflow={'hidden'}> <Box my={3} borderRadius={'md'} overflow={'hidden'} backgroundColor={'#222'}>
<Flex <Flex
py={2} py={2}
px={5} px={5}

View File

@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { Readable } from 'stream'; import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
import { connectToDatabase, ChatWindow } from '@/service/mongo'; 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';
@ -9,21 +9,13 @@ import { ChatItemType } from '@/types/chat';
/* 发送提示词 */ /* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Connection', 'keep-alive'); res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
const responseData: string[] = [];
const stream = new Readable({
read(size) {
const data = responseData.shift() || null;
this.push(data);
}
});
res.on('close', () => { res.on('close', () => {
res.end(); res.end();
stream.destroy();
}); });
const { chatId, windowId } = req.query as { chatId: string; windowId: string }; const { chatId, windowId } = req.query as { chatId: string; windowId: string };
@ -58,16 +50,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.replace(/(\n| )/g, '') 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 mark the code type as accurately as possible!'
}); });
// 获取 chatAPI // 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey); const chatAPI = getOpenAIApi(userApiKey);
const chatResponse = await chatAPI.createChatCompletion( const chatResponse = await chatAPI.createChatCompletion(
@ -78,48 +68,57 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
messages: formatPrompts, messages: formatPrompts,
stream: true stream: true
}, },
openaiProxy {
responseType: 'stream',
httpsAgent: openaiProxy?.httpsAgent
}
); );
// 截取字符串内容
const reg = /{"content"(.*)"}/g;
// @ts-ignore
const match = chatResponse.data.match(reg);
if (!match) return;
let AIResponse = ''; let AIResponse = '';
// 循环给 stream push 内容 // 解析数据
match.forEach((item: string, i: number) => { const decoder = new TextDecoder();
try { new ReadableStream({
const json = JSON.parse(item); async start(controller) {
// 开头的换行忽略 // callback
if (i === 0 && json.content?.startsWith('\n')) return; async function onParse(event: ParsedEvent | ReconnectInterval) {
AIResponse += json.content; if (event.type === 'event') {
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n const data = event.data;
if (content) { if (data === '[DONE]') {
responseData.push(`event: responseData\ndata: ${content}\n\n`); controller.close();
// res.write(`event: responseData\n`) res.write('event: done\ndata: \n\n');
// res.write(`data: ${content}\n\n`) res.end();
// 存入库
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
return;
}
try {
const json = JSON.parse(data);
const content: string = json.choices[0].delta.content || '';
res.write(`event: responseData\ndata: ${content.replace(/\n/g, '<br/>')}\n\n`);
AIResponse += content;
} catch (e) {
// maybe parse error
controller.error(e);
res.end();
}
}
}
const parser = createParser(onParse);
for await (const chunk of chatResponse.data as any) {
parser.feed(decoder.decode(chunk));
} }
} catch (err) {
err;
} }
}); });
responseData.push(`event: done\ndata: \n\n`);
// 存入库
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
})();
} catch (err: any) { } catch (err: any) {
let errorText = err; let errorText = err;
if (err.code === 'ECONNRESET') { if (err.code === 'ECONNRESET') {
@ -143,17 +142,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
} }
console.error(errorText); console.error(errorText);
responseData.push(`event: serviceError\ndata: ${errorText}\n\n`); res.write(`event: serviceError\ndata: ${errorText}\n\n`);
res.end();
// 删除最一条数据库记录, 也就是预发送的那一条 // 删除最一条数据库记录, 也就是预发送的那一条
(async () => { await ChatWindow.findByIdAndUpdate(windowId, {
await ChatWindow.findByIdAndUpdate(windowId, { $pop: { content: 1 },
$pop: { content: 1 }, updateTime: Date.now()
updateTime: Date.now() });
});
})();
} }
// 开启 stream 传输
stream.pipe(res);
} }

View File

@ -148,6 +148,7 @@ const Chat = () => {
); );
}); });
event.addEventListener('done', () => { event.addEventListener('done', () => {
console.log('done');
clearTimeout(timer); clearTimeout(timer);
event.close(); event.close();
setChatList((state) => setChatList((state) =>
@ -324,7 +325,7 @@ const Chat = () => {
height={30} height={30}
/> />
</Box> </Box>
<Box flex={'1 0 0'} w={0} overflowX={'auto'}> <Box flex={'1 0 0'} w={0} overflowX={'hidden'}>
{item.obj === 'AI' ? ( {item.obj === 'AI' ? (
<Markdown <Markdown
source={item.value} source={item.value}

View File

@ -77,7 +77,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
return ( return (
<> <>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}> <Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT FastGPT
</Box> </Box>
<form onSubmit={handleSubmit(onclickFindPassword)}> <form onSubmit={handleSubmit(onclickFindPassword)}>
<FormControl mt={8} isInvalid={!!errors.email}> <FormControl mt={8} isInvalid={!!errors.email}>

View File

@ -58,7 +58,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
return ( return (
<> <>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}> <Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT FastGPT
</Box> </Box>
<form onSubmit={handleSubmit(onclickLogin)}> <form onSubmit={handleSubmit(onclickLogin)}>
<FormControl mt={8} isInvalid={!!errors.email}> <FormControl mt={8} isInvalid={!!errors.email}>

View File

@ -78,7 +78,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
return ( return (
<> <>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}> <Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT FastGPT
</Box> </Box>
<form onSubmit={handleSubmit(onclickRegister)}> <form onSubmit={handleSubmit(onclickRegister)}>
<FormControl mt={8} isInvalid={!!errors.email}> <FormControl mt={8} isInvalid={!!errors.email}>

View File

@ -20,10 +20,13 @@ export const jsonRes = (
let msg = message; let msg = message;
if ((code < 200 || code >= 400) && !message) { if ((code < 200 || code >= 400) && !message) {
msg = msg = error?.message || '请求错误';
typeof error === 'string' if (typeof error === 'string') {
? error msg = error;
: openaiError[error?.response?.data?.message] || error?.message || '请求错误'; } else if (error?.response?.data?.message in openaiError) {
msg = openaiError[error?.response?.data?.message];
}
console.error(error); console.error(error);
console.error(msg); console.error(msg);
} }

View File

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