一个基于 @microsoft/fetch-event-source 的 SSE 请求封装方法

7 阅读2分钟

AI流行的背景下,很多项目也开始接入大模型,而前端与大模型服务对接的核心交互就是 SSE(server-sent events) ,通过 SSE 可以实现大模型的流式输出,@microsoft/fetch-event-source 这个工具为发起 SSE 请求提供了完善的 Fetch API,本文主要是基于这个工具提供一个通用、可复用的 SSE 请求封装方法,涵盖参数配置、流式数据处理、异常捕获、主动中断等核心场景,帮助项目快速落地大模型流式交互功能,避免重复造轮子。

安装依赖

npm install @microsoft/fetch-event-source

封装方法

import { EventStreamContentType, FetchEventSourceInit, fetchEventSource } from '@microsoft/fetch-event-source'

/**
 * 通用的 sse 请求方法
 *
 * @export
 * @param {string} url 接口地址
 * @param {*} body 请求体
 * @param {FetchEventSourceInit} eventInit 事件配置,参考文档:https://www.npmjs.com/package/@microsoft/fetch-event-source#usage
 * @return {Promise<AbortController>} 返回中止控制器,可以在组件销毁或需要中止事件流时调用 abort() 中断事件流
 */
export async function sseFetch(url: string, body: any, eventInit: FetchEventSourceInit): Promise<AbortController> {
  let messageReceived = false
  return new Promise((resolve, reject) => {
    const ctrl = new AbortController()
    const onerror = function (error: any) {
      let errorMsg = typeof error === 'string' ? error : error?.message || error?.title || 'Content Render Failed!'
      if (error?.name) {
        errorMsg = error.name + ':' + errorMsg
      }
      console.error(error)
      if (eventInit.onerror) {
        eventInit.onerror(new Error(errorMsg))
      } else if (eventInit.onmessage) {
        eventInit.onmessage({ event: 'error', data: errorMsg, id: '__error__' })
      }
      if (!messageReceived) {
        reject(error)
      }
      messageReceived = true
      ctrl.abort()
    }
    if (eventInit.onmessage) {
      const onmessage = eventInit.onmessage
      eventInit.onmessage = function (ev) {
        try {
          onmessage(ev)
          if (!messageReceived) {
            resolve(ctrl)
          }
        } catch (e) {
          onerror(e)
        } finally {
          messageReceived = true
        }
      }
    }
    fetchEventSource(url, {
      method: 'POST',
      body: typeof body === 'string' ? body : JSON.stringify(body),
      headers: {
        accept: '*/*',
      },
      credentials: 'include',
      signal: ctrl.signal,
      openWhenHidden: true,
      fetch(input, init) {
        return fetch(input, { ...init, signal: ctrl.signal })
          .then(async res => {
            const contentType = res.headers.get('content-type')
            if (!contentType?.includes(EventStreamContentType)) {
              const json = contentType === 'application/json' ? await res.json() : await res.text()
              onerror(json)
            }
            return res
          })
          .catch(err => {
            onerror(err)
            return Promise.reject(err)
          })
      },
      ...eventInit,
    }).catch(e => reject(e))
  })
}

使用示例

// 调用 sse 请求并获取流式输出文本
this.abortController = await sseFetch(url, reqData, {
    headers: {
      'Content-Type': 'application/json', // 如果接口有要求 contentType 需要添加请求头,否则就不需要
    },
    onopen(res) {
        // 事件流开启并连通后的回调
        console.log('---stream open---', res)
    },
    onmessage(msg) {
        // 接收消息,一般来说实现这个回调就足够了
        console.log('---msg received---', msg)
    },
    onclose() {
        // 事件流被关闭的回调
        console.log('---stream close---')
    },
    onerror(err) {
        // 请求异常的回调
        console.error('---stream error---', err)
    }
})

// 停止 sse 请求
this.abortController.stop()