From 65da4653bcc92c52fb9b3c14f6dad39c3752d74b Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Fri, 10 Mar 2023 02:57:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20stream=E6=B5=81=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.js | 12 +-- package.json | 1 + pnpm-lock.yaml | 9 ++ src/components/Markdown/index.module.scss | 2 +- src/components/Markdown/index.tsx | 4 +- src/pages/api/chat/chatGpt.ts | 120 ++++++++++------------ src/pages/chat/index.tsx | 13 +-- 7 files changed, 73 insertions(+), 88 deletions(-) diff --git a/next.config.js b/next.config.js index e3e375924..04c3c8a67 100644 --- a/next.config.js +++ b/next.config.js @@ -6,17 +6,7 @@ const isDev = process.env.NODE_ENV === 'development'; const nextConfig = { output: 'standalone', reactStrictMode: false, - compress: true, - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'docgpt-1301319986.cos.ap-shanghai.myqcloud.com', - port: '', - pathname: '/**' - } - ] - } + compress: true }; module.exports = nextConfig; diff --git a/package.json b/package.json index 16b84e745..2983bcaf5 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "axios": "^1.3.3", "crypto": "^1.0.1", "dayjs": "^1.11.7", + "eventsource-parser": "^0.1.0", "formidable": "^2.1.1", "framer-motion": "^9.0.6", "hyperdown": "^2.4.29", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e642514b..5e8f4dff4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,7 @@ specifiers: 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 husky: ^8.0.3 @@ -65,6 +66,7 @@ dependencies: axios: registry.npmmirror.com/axios/1.3.3 crypto: registry.npmmirror.com/crypto/1.0.1 dayjs: registry.npmmirror.com/dayjs/1.11.7 + eventsource-parser: registry.npmmirror.com/eventsource-parser/0.1.0 formidable: registry.npmmirror.com/formidable/2.1.1 framer-motion: registry.npmmirror.com/framer-motion/9.0.6_biqbaboplfbrettd7655fr4n2y hyperdown: registry.npmmirror.com/hyperdown/2.4.29 @@ -4553,6 +4555,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + registry.npmmirror.com/eventsource-parser/0.1.0: + resolution: {integrity: sha512-M9QjFtEIkwytUarnx113HGmgtk52LSn3jNAtnWKi3V+b9rqSfQeVdLsaD5AG/O4IrGQwmAAHBIsqbmURPTd2rA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-0.1.0.tgz} + name: eventsource-parser + version: 0.1.0 + engines: {node: '>=14.18'} + dev: false + registry.npmmirror.com/execa/6.1.0: resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/execa/-/execa-6.1.0.tgz} name: execa diff --git a/src/components/Markdown/index.module.scss b/src/components/Markdown/index.module.scss index 0be946be6..3c69ddae0 100644 --- a/src/components/Markdown/index.module.scss +++ b/src/components/Markdown/index.module.scss @@ -369,7 +369,7 @@ } 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'; diff --git a/src/components/Markdown/index.tsx b/src/components/Markdown/index.tsx index e6e4f17f2..299ec0a6e 100644 --- a/src/components/Markdown/index.tsx +++ b/src/components/Markdown/index.tsx @@ -26,8 +26,8 @@ const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean code({ node, inline, className, children, ...props }) { const match = /language-(\w+)/.exec(className || ''); const code = String(children).replace(/\n$/, ''); - return !inline ? ( - + return !inline || match ? ( + { res.end(); - stream.destroy(); }); 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( (item: ChatItemType) => ({ role: map[item.obj], - content: item.value.replace(/(\n| )/g, '') + content: item.value.replace(/\n/g, ' ') }) ); // 第一句话,强调代码类型 formatPrompts.unshift({ role: ChatCompletionRequestMessageRoleEnum.System, - content: - 'If the content is code or code blocks, please mark the code type as accurately as possible!' + content: '如果你想返回代码,请务必声明代码的类型!' }); - // 获取 chatAPI const chatAPI = getOpenAIApi(userApiKey); const chatResponse = await chatAPI.createChatCompletion( @@ -78,48 +68,57 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) messages: formatPrompts, 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 = ''; - // 循环给 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`) + // 解析数据 + const decoder = new TextDecoder(); + new ReadableStream({ + async start(controller) { + // callback + async function onParse(event: ParsedEvent | ReconnectInterval) { + if (event.type === 'event') { + const data = event.data; + if (data === '[DONE]') { + controller.close(); + res.write('event: done\ndata: \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, '
')}\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) { let errorText = err; if (err.code === 'ECONNRESET') { @@ -143,17 +142,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } } 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, { - $pop: { content: 1 }, - updateTime: Date.now() - }); - })(); + await ChatWindow.findByIdAndUpdate(windowId, { + $pop: { content: 1 }, + updateTime: Date.now() + }); } - - // 开启 stream 传输 - stream.pipe(res); } diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 00a7b8cb1..86da56d36 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -148,6 +148,7 @@ const Chat = () => { ); }); event.addEventListener('done', () => { + console.log('done'); clearTimeout(timer); event.close(); setChatList((state) => @@ -324,7 +325,7 @@ const Chat = () => { height={30} />
- + {item.obj === 'AI' ? ( { ))} - {/* 空内容提示 */} - {/* { - chatList.length === 0 && ( - <> - -内容太长 - - - ) - } */}