mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-15 07:31:19 +00:00
perf: 流响应
This commit is contained in:
@@ -36,28 +36,10 @@ export const postGPT3SendPrompt = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预发 prompt 进行存储
|
* 存储一轮对话
|
||||||
*/
|
*/
|
||||||
export const postChatGptPrompt = ({
|
export const postSaveChat = (data: { windowId: string; prompts: ChatItemType[] }) =>
|
||||||
prompt,
|
POST('/chat/saveChat', data);
|
||||||
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}&date=${Date.now()}`);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除最后一句
|
* 删除最后一句
|
||||||
|
47
src/api/fetch.ts
Normal file
47
src/api/fetch.ts
Normal 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 || '请求异常');
|
||||||
|
}
|
||||||
|
});
|
@@ -6,26 +6,19 @@ import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
|||||||
import { httpsAgent } from '@/service/utils/tools';
|
import { httpsAgent } from '@/service/utils/tools';
|
||||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
import { openaiError } from '@/service/errorCode';
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { PassThrough } from 'stream';
|
||||||
|
|
||||||
/* 发送提示词 */
|
/* 发送提示词 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
const { chatId, windowId, prompt } = req.body as {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
prompt: ChatItemType;
|
||||||
res.setHeader('X-Accel-Buffering', 'no');
|
windowId: string;
|
||||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
chatId: string;
|
||||||
|
};
|
||||||
res.on('close', () => {
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
req.on('error', () => {
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
const { chatId, windowId } = req.query as { chatId: string; windowId: string };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!windowId || !chatId) {
|
if (!windowId || !chatId || !prompt) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,15 +28,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const model: ModelType = chat.modelId;
|
const model: ModelType = chat.modelId;
|
||||||
|
|
||||||
const map = {
|
|
||||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
|
||||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
|
||||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
|
||||||
};
|
|
||||||
// 读取对话内容
|
// 读取对话内容
|
||||||
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content;
|
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content;
|
||||||
|
prompts.push(prompt);
|
||||||
|
|
||||||
// 长度过滤
|
// 上下文长度过滤
|
||||||
const maxContext = model.security.contextMaxLen;
|
const maxContext = model.security.contextMaxLen;
|
||||||
const filterPrompts =
|
const filterPrompts =
|
||||||
prompts.length > maxContext + 2
|
prompts.length > maxContext + 2
|
||||||
@@ -51,6 +40,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
: prompts.slice(0, prompts.length);
|
: prompts.slice(0, prompts.length);
|
||||||
|
|
||||||
// 格式化文本内容
|
// 格式化文本内容
|
||||||
|
const map = {
|
||||||
|
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||||
|
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||||
|
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||||
|
};
|
||||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||||
(item: ChatItemType) => ({
|
(item: ChatItemType) => ({
|
||||||
role: map[item.obj],
|
role: map[item.obj],
|
||||||
@@ -62,9 +56,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||||
content: '如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
|
content: '如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取 chatAPI
|
// 获取 chatAPI
|
||||||
const chatAPI = getOpenAIApi(userApiKey);
|
const chatAPI = getOpenAIApi(userApiKey);
|
||||||
|
// 发出请求
|
||||||
const chatResponse = await chatAPI.createChatCompletion(
|
const chatResponse = await chatAPI.createChatCompletion(
|
||||||
{
|
{
|
||||||
model: model.service.chatModel,
|
model: model.service.chatModel,
|
||||||
@@ -84,58 +79,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
'response success'
|
'response success'
|
||||||
);
|
);
|
||||||
|
|
||||||
let AIResponse = '';
|
// 创建响应流
|
||||||
|
const pass = new PassThrough();
|
||||||
|
pass.pipe(res);
|
||||||
|
|
||||||
// 解析数据
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||||
if (event.type === 'event') {
|
if (event.type !== 'event') return;
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
if (data === '[DONE]') {
|
if (data === '[DONE]') return;
|
||||||
// 存入库
|
try {
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
const json = JSON.parse(data);
|
||||||
$push: {
|
const content: string = json?.choices?.[0].delta.content || '';
|
||||||
content: {
|
if (!content) return;
|
||||||
obj: 'AI',
|
// console.log('content:', content)
|
||||||
value: AIResponse
|
pass.push(content.replace(/\n/g, '<br/>'));
|
||||||
}
|
} catch (error) {
|
||||||
},
|
error;
|
||||||
updateTime: Date.now()
|
|
||||||
});
|
|
||||||
res.write('event: done\ndata: \n\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(data);
|
|
||||||
const content: string = json?.choices?.[0].delta.content || '\n';
|
|
||||||
// console.log('content:', content)
|
|
||||||
res.write(`event: responseData\ndata: ${content.replace(/\n/g, '<br/>')}\n\n`);
|
|
||||||
AIResponse += content;
|
|
||||||
} catch (error) {
|
|
||||||
error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for await (const chunk of chatResponse.data as any) {
|
for await (const chunk of chatResponse.data as any) {
|
||||||
const parser = createParser(onParse);
|
const parser = createParser(onParse);
|
||||||
parser.feed(decoder.decode(chunk));
|
parser.feed(decodeURIComponent(chunk));
|
||||||
}
|
}
|
||||||
|
pass.push(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log('error->', err?.response, '===');
|
res.status(500);
|
||||||
let errorText = 'OpenAI 服务器访问超时';
|
jsonRes(res, {
|
||||||
if (err.code === 'ECONNRESET' || err?.response?.status === 502) {
|
code: 500,
|
||||||
errorText = '服务器代理出错';
|
error: err
|
||||||
} else if (err?.response?.statusText && openaiError[err.response.statusText]) {
|
|
||||||
errorText = openaiError[err.response.statusText];
|
|
||||||
}
|
|
||||||
console.log('error->', errorText);
|
|
||||||
res.write(`event: serviceError\ndata: ${errorText}\n\n`);
|
|
||||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
|
||||||
$pop: { content: 1 },
|
|
||||||
updateTime: Date.now()
|
|
||||||
});
|
});
|
||||||
res.end();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,34 +2,31 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
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) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const { windowId, prompt, chatId } = req.body as {
|
const { windowId, prompts } = req.body as {
|
||||||
windowId: string;
|
windowId: string;
|
||||||
prompt: ChatItemType;
|
prompts: ChatItemType[];
|
||||||
chatId: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!windowId || !prompt || !chatId) {
|
if (!windowId || !prompts) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
await connectToDatabase();
|
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, {
|
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||||
$push: { content: prompt },
|
$push: {
|
||||||
|
content: {
|
||||||
|
$each: prompts.map((item) => ({
|
||||||
|
obj: item.obj,
|
||||||
|
value: item.value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
updateTime: Date.now()
|
updateTime: Date.now()
|
||||||
});
|
});
|
||||||
|
|
@@ -1,13 +1,7 @@
|
|||||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import {
|
import { getInitChatSiteInfo, postGPT3SendPrompt, delLastMessage, postSaveChat } from '@/api/chat';
|
||||||
getInitChatSiteInfo,
|
|
||||||
postGPT3SendPrompt,
|
|
||||||
getChatGPTSendEvent,
|
|
||||||
postChatGptPrompt,
|
|
||||||
delLastMessage
|
|
||||||
} from '@/api/chat';
|
|
||||||
import { ChatSiteItemType, ChatSiteType } from '@/types/chat';
|
import { ChatSiteItemType, ChatSiteType } from '@/types/chat';
|
||||||
import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
|
import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
@@ -17,6 +11,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
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';
|
||||||
|
import { streamFetch } from '@/api/fetch';
|
||||||
|
|
||||||
const Markdown = dynamic(() => import('@/components/Markdown'));
|
const Markdown = dynamic(() => import('@/components/Markdown'));
|
||||||
|
|
||||||
@@ -128,69 +123,64 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
|
|||||||
const chatGPTPrompt = useCallback(
|
const chatGPTPrompt = useCallback(
|
||||||
async (newChatList: ChatSiteItemType[]) => {
|
async (newChatList: ChatSiteItemType[]) => {
|
||||||
if (!windowId) return;
|
if (!windowId) return;
|
||||||
/* 预请求,把消息存入库 */
|
const prompt = {
|
||||||
await postChatGptPrompt({
|
obj: newChatList[newChatList.length - 1].obj,
|
||||||
windowId,
|
value: newChatList[newChatList.length - 1].value
|
||||||
prompt: newChatList[newChatList.length - 1],
|
};
|
||||||
chatId
|
// 流请求,获取数据
|
||||||
});
|
const res = await streamFetch({
|
||||||
|
url: '/api/chat/chatGpt',
|
||||||
return new Promise((resolve, reject) => {
|
data: {
|
||||||
const event = getChatGPTSendEvent(chatId, windowId);
|
windowId,
|
||||||
// 30s 收不到消息就报错
|
prompt,
|
||||||
let timer = setTimeout(() => {
|
chatId
|
||||||
event.close();
|
},
|
||||||
reject('服务器超时');
|
onMessage: (text: string) => {
|
||||||
}, 30000);
|
|
||||||
event.addEventListener('responseData', ({ data }) => {
|
|
||||||
/* 重置定时器 */
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
event.close();
|
|
||||||
reject('服务器超时');
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
const msg = data.replace(/<br\/>/g, '\n');
|
|
||||||
setChatList((state) =>
|
setChatList((state) =>
|
||||||
state.map((item, index) => {
|
state.map((item, index) => {
|
||||||
if (index !== state.length - 1) return item;
|
if (index !== state.length - 1) return item;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
value: item.value + msg
|
value: item.value + text
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
event.addEventListener('done', () => {
|
|
||||||
console.log('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.log('error->', err, '===');
|
|
||||||
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
|
|
||||||
});
|
|
||||||
event.onerror = (err) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
event.close();
|
|
||||||
console.log('error->', err);
|
|
||||||
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 保存对话信息
|
||||||
|
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]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,3 +4,7 @@ export const openaiError: Record<string, string> = {
|
|||||||
rate_limit_reached: '同时访问用户过多,请稍后再试',
|
rate_limit_reached: '同时访问用户过多,请稍后再试',
|
||||||
'Bad Request': '上下文太多了,请重开对话~'
|
'Bad Request': '上下文太多了,请重开对话~'
|
||||||
};
|
};
|
||||||
|
export const proxyError: Record<string, boolean> = {
|
||||||
|
ECONNABORTED: true,
|
||||||
|
ECONNRESET: true
|
||||||
|
};
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { openaiError } from './errorCode';
|
import { openaiError, proxyError } from './errorCode';
|
||||||
|
|
||||||
export interface ResponseType<T = any> {
|
export interface ResponseType<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
@@ -23,12 +23,13 @@ export const jsonRes = (
|
|||||||
msg = error?.message || '请求错误';
|
msg = error?.message || '请求错误';
|
||||||
if (typeof error === 'string') {
|
if (typeof error === 'string') {
|
||||||
msg = error;
|
msg = error;
|
||||||
} else if (error?.response?.data?.message in openaiError) {
|
} else if (proxyError[error?.code]) {
|
||||||
msg = openaiError[error?.response?.data?.message];
|
msg = '服务器代理出错';
|
||||||
|
} else if (openaiError[error?.response?.statusText]) {
|
||||||
|
msg = openaiError[error.response.statusText];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('error->', error);
|
console.log('error->', error.code, error?.response?.statusText, msg);
|
||||||
console.log('error->', msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
Reference in New Issue
Block a user