feat: sse流式对话

master
LCJ-MinYa 10 months ago
parent 86ef51c165
commit f63ae9226c

@ -21,6 +21,7 @@
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"dependencies": { "dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"@pureadmin/descriptions": "^1.2.1", "@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.2.0", "@pureadmin/table": "^3.2.0",
"@pureadmin/utils": "^2.4.8", "@pureadmin/utils": "^2.4.8",

@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
'@pureadmin/descriptions': '@pureadmin/descriptions':
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1(echarts@5.5.1)(element-plus@2.8.0(vue@3.4.38(typescript@5.5.4)))(typescript@5.5.4) version: 1.2.1(echarts@5.5.1)(element-plus@2.8.0(vue@3.4.38(typescript@5.5.4)))(typescript@5.5.4)
@ -806,6 +809,9 @@ packages:
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@microsoft/fetch-event-source@2.0.1':
resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -4093,6 +4099,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@microsoft/fetch-event-source@2.0.1': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5

@ -69,8 +69,12 @@ const titleArr = [
title: 'item固定高度的虚拟列表实现', title: 'item固定高度的虚拟列表实现',
}, },
{ {
key: 'sse', key: 'sseNative',
title: 'ai对话流式显示文本', title: 'ai对话流式显示文本(浏览器原生)',
},
{
key: 'sseFetch',
title: 'ai对话流式显示文本(使用fetch-event-source)',
}, },
]; ];

@ -0,0 +1,97 @@
<template>
<base-container>
<h2 class="pt-5 pb-5">流式显示文本(需先启动node-local-server项目作为本地服务端)</h2>
<div>
<el-button
type="primary"
@click="handleSend"
>问个问题</el-button
>
<el-button
type="primary"
@click="handleClose"
>停止</el-button
>
</div>
<div
class="wordbox"
ref="containerRef"
>
<span
id="content"
v-html="content"
></span>
</div>
</base-container>
</template>
<script setup>
import { ref, nextTick } from 'vue';
import { fetchEventSource } from '@microsoft/fetch-event-source';
const content = ref('');
const controller = ref(null);
const updateContent = (event) => {
console.log(event);
console.log('sse收到消息');
content.value += event.data + '<br/>';
console.log(content.value, 'content内容');
if (event.data === 'End') {
/**
* 注释部分为前端手动关闭sse连接的实现
* 这里不受冻关闭原因是@microsoft/fetch-event-source这个框架可以正确检测后段服务端是否关闭了连接所以不需要手动关闭
* 浏览器原生new EventSource()方法无法检测后端服务端是否关闭连接所以需要手动关闭(每次后段关闭都会进onerror事件)
* 具体请看另外一个demo sseNative.vue
*/
// handleClose();
}
nextTick(() => {
scrollToBottom();
});
};
const handleError = (event) => {
console.log(event);
console.log('sse连接出错');
};
const handleClose = () => {
console.log('sse关闭');
// controller.value.abort();
};
const containerRef = ref(null);
const scrollToBottom = () => {
containerRef.value.scrollTop = containerRef.value.scrollHeight;
};
const handleSend = () => {
controller.value = new AbortController();
const signal = controller.value.signal;
/** @microsoft/fetch-event-source该框架支持POST方法并且支持传参 */
fetchEventSource('http://localhost:3000/events', {
method: 'POST',
body: JSON.stringify({
question: '你好,我是小雅,请问有什么可以帮助您?',
token: 'xxxx',
}),
onmessage: updateContent,
onerror: handleError,
onclose: handleClose,
signal,
});
};
</script>
<style lang="scss" scoped>
.wordbox {
width: 100%;
height: 100%;
overflow: auto;
padding: 20px;
border: 1px solid #ccc;
margin: 20px 0;
border-radius: 5px;
}
</style>

@ -37,10 +37,11 @@ const updateContent = (event) => {
content.value += event.data + '<br/>'; content.value += event.data + '<br/>';
console.log(content.value, 'content内容'); console.log(content.value, 'content内容');
if (event.data === 'End') { if (event.data === 'End') {
/** 浏览器原生EventSource必须服务器主动告诉浏览器关闭连接否则会进onerror事件并一直重连 */
handleClose(); handleClose();
} }
nextTick(() => { nextTick(() => {
// scrollToBottom(); scrollToBottom();
}); });
}; };
@ -50,6 +51,7 @@ const handleError = (event) => {
}; };
const handleClose = () => { const handleClose = () => {
console.log('sse关闭');
eventSource.value.close(); eventSource.value.close();
}; };
@ -63,8 +65,6 @@ const handleSend = () => {
eventSource.value.addEventListener('message', updateContent); eventSource.value.addEventListener('message', updateContent);
eventSource.value.addEventListener('error', handleError); eventSource.value.addEventListener('error', handleError);
}; };
onMounted(() => {});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
Loading…
Cancel
Save