项目中使用 fetch-event-source 实现调用大模型的流式接口,实现类似chatGPT流式输出效果
@microsoft/fetch-event-source 使用说明
github地址: fetch-event-source
MDN解释: event source
fetch-event-source提供了一个更好的 API,用于发出事件源请求( Event Source requests 也称为服务器发送事件),并具有Fetch API中提供的所有功能。
- 基于 Fetch API 提供了一个用于使用服务器发送事件的备用接口,可以使用任何请求方法、头信息和请求体,以及 fetch() 提供的所有其他功能。
- 如果连接中断或发生错误,可以控制重试
安装
npm install @microsoft/fetch-event-source
用法
import { fetchEventSource } from '@microsoft/fetch-event-source'
class RetriableError extends Error { }
class FatalError extends Error { }
const ctrl = new AbortController();
//AbortController 是一个用于控制和取消异步操作的接口。它通常与 AbortSignal 一起使用,后者是由 AbortController 生成的信号对象。AbortController 和 AbortSignal 主要用于取消 fetch 请求或其他需要取消的异步任务。
fetchEventSource('/api/sse', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
foo: 'bar'
}),
signal: ctrl.signal,//用于控制和取消 fetch 请求
async onopen(response) {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
return; // 一切正常
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// 客户端错误通常是不可重试的:
throw new FatalError();
} else {
throw new RetriableError();
}
},
onmessage(msg) {
// 如果服务器发出错误消息,抛出异常
// 以便它由下面的 onerror 回调处理:
if (msg.event === 'FatalError') {
throw new FatalError(msg.data);
}
},
onclose() {
// 如果服务器意外关闭连接,重试:
throw new RetriableError();
},
onerror(err) {
if (err instanceof FatalError) {
throw err;
} else {
// 什么都不做以自动重试。您也可以
// 在这里返回一个特定的重试间隔。
}
}
});
兼容性
ES2017
vue项目示例
- 获取流式数据,拼接流式显示
- 自定义规则拼接并截取句子,调用语音接口播放
import { fetchEventSource } from '@microsoft/fetch-event-source'
const ctrl=new AbortController()
const textBuffer = ref(''); //文字缓冲区
const reportHtml = ref('')
const eventSource =()=>{
reportHtml.value=''
fetchEventSource(`/xxx/openai/dataStream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `bearerToken`
},
body: JSON.stringify({}),//参数
mode:'cors',
signal:ctrl.signal,
onmessage: (event) => {
let res = event.data
let content = res
reportHtml.value = reportHtml.value+(res===null?'':res)
if(content){
textBuffer.value += content;
}
// 如果缓冲区中的文本达到一定长度 并有句号时,进行语音转换和播放 ,最大长度100时转换
const textLen = 5
const maxLen = 10
if (textBuffer.value.length >= textLen
&& textBuffer.value.length <= maxLen
&& (textBuffer.value.slice(textLen).indexOf('。') != -1 || textBuffer.value.slice(textLen).indexOf('!') != -1)
|| (
textBuffer.value.length > maxLen
&& (textBuffer.value.slice(maxLen).indexOf(',') != -1 ||textBuffer.value.slice(maxLen).indexOf(',') != -1 || textBuffer.value.slice(maxLen).indexOf('.') != -1)
)
) {
// 分隔符
let delimiter = ''
let baseText = ''
let extraText = ''
// 超出xxx
if(textBuffer.value.length > maxLen){
// 前 xx 个字符都要播报
baseText = textBuffer.value.slice(0,maxLen)
extraText = textBuffer.value.slice(maxLen)
}else if(textBuffer.value.length > textLen && textBuffer.value.length <= maxLen){
baseText = textBuffer.value.slice(0,textLen)
extraText = textBuffer.value.slice(textLen)
}
if(extraText.indexOf('。')>-1){
delimiter = '。'
}else if(extraText.indexOf('!')>-1){
delimiter = '!'
}else if(extraText.indexOf(',')>-1){
delimiter = ','
}else if(extraText.indexOf(',')>-1){
delimiter = ','
}
else if(extraText.indexOf('.')>-1){
delimiter = '.'
}
let splitText = ''
let leftText = ''
if(delimiter){
splitText = extraText.split(delimiter)[0]
leftText = extraText.split(delimiter)[1]
}
let wholeText = baseText + splitText + delimiter
const currentTime = new Date().toLocaleTimeString();
emit('playAudio', { //调用播报
text:wholeText,
type:0
})
console.log(currentTime,wholeText,wholeText.length,'playAudio方法调用-----')
textBuffer.value = leftText
}
},
onerror: (error:any) => {
props.itemData.isLoading=false
throw error
},
onclose: () => {
props.itemData.isLoading=false
emit('loadingEnd')
if (textBuffer.value) {
console.log(textBuffer.value,'textBuffer.value - playing')
emit('playAudio', {
text:textBuffer.value,
type:1 // 播放完了 需要缓存队列
})
textBuffer.value = '';
}
}
})
}
接口截图
network