j***@qq.com
j***@qq.com
  • 发布:2023-10-25 16:31
  • 更新:2024-11-15 11:43
  • 阅读:8401

uni-app通过SSE支持流式效果

分类:uni-app

uni-app支持SSE

因为uni.request没有办法支持SSE,为此尝试了各种方案,着了大急。
现将尝试的各种方案进行一个整理,若有疏漏请大家帮忙补充。

H5

因为Web端运行在浏览器内核上,SSE的支持是比较完备的,可以使用axios、@microsoft/fetch-event-source 等实现,各种案例也比较完善因此不再赘述。

微信小程序

微信小程序的SSE方案参考的是《微信小程序除了WebSocket其他思路实现流传输文字(打字机)效果》
因为我们是在uniapp中实现,所以在原文方案的基础上使用的uni相关的API来实现,考虑到要实现停止和兼容H5的的接口,最后引入了abort-controller@uni-helper/uni-network来进行封装。

import type { UnCancelTokenListener, UnGenericAbortSignal, UnHeaders } from '@uni-helper/uni-network'  

/**  
 * 二进制解析成文本  
 * @param data 二进制数据  
 * @returns 文本  
 */  
export function decodeArrayBuffer(data: ArrayBuffer | undefined) {  
  if (!data) {  
    return ''  
  }  
  return decodeUsingURIComponent(data)  
}  

/**  
 * URIComponent解码二进制流(不用引入额外包)  
 * @param data 二进制流  
 * @returns 文本  
 */  
function decodeUsingURIComponent(data: ArrayBuffer) {  
  const uint8Array = new Uint8Array(data)  
  let text = String.fromCharCode(...uint8Array)  
  try {  
    text = decodeURIComponent(escape(text))  
  } catch (e) {  
    console.error('decodeUsingURIComponent: Can not decodeURI ', text)  
  }  
  return text  
}  

type onStreamReceivedListener = (text: string) => void  

export function fetchStreamChat(  
  params: { prompt: string; uuid: string },  
  signal?: UnGenericAbortSignal,  
  listener?: onStreamReceivedListener  
) {  
  const onHeadersReceived = (response?: { headers?: UnHeaders }) => {  
    console.log('fetchStreamChat.onHeadersReceived: ', response?.headers)  
  }  
  const onChunkReceived = (response?: { data?: ArrayBuffer }) => {  
    const text = decodeArrayBuffer(response?.data)  
    listener?.(text)  
  }  
  return post<string>({  
    url: '/openai/completions/stream',  
    headers: {  
      Accept: 'text/event-stream',  
      'Content-Type': 'application/json',  
      token: 'your-token'  
    },  
    data: {  
      content: params.prompt,  
      scene: params.uuid,  
      source: 'gpt4',  
    },  
    responseType: 'arraybuffer',  
    enableChunked: true,  
    onHeadersReceived,  
    onChunkReceived,  
    signal: signal  
  })  
}

特殊注意以上代码使用时对abort-controller的引入方式

import AbortController from 'abort-controller/dist/abort-controller'  

let controller = new AbortController()  
const onResponseListener = async (responseText: string) => {  
console.log('==response==\n', responseText)  
}  
await fetchStreamChat({ prompt, uuid }, controller.signal, onResponseListener)

后端NGINX配置

# 注意这里只配置代理发送接口,不然其他接口也会受影响  

location /openai/completions/stream {  
    # ...more config  

    proxy_set_header Transfer-Encoding "";  
    chunked_transfer_encoding on;  
    proxy_buffering off;  
}

APP

App目前看到的方案最多,但是目前为止没有找到很合适的方案。有更好的方案请各位大佬补充Thanks♪(・ω・)ノ

1. plus.net.XMLHttpRequest

参考方案 《XMLHttpRequest模块管理网络请求》,具体代码如下

import type { UnCancelTokenListener, UnGenericAbortSignal, UnHeaders } from '@uni-helper/uni-network'  

type onStreamReceivedListener = (text: string) => void  

export class CanceledError extends Error {  
  constructor(message?: string) {  
    super(message ?? 'canceled')  
  }  
}  

export function fetchStreamChatForApp(  
  params: { prompt: string; uuid: string },  
  signal?: UnGenericAbortSignal,  
  listener?: onStreamReceivedListener  
) {  
  return new Promise((resolve, reject) => {  
    // 梳理好请求数据  
    const token =  'your-token'  
    const data = JSON.stringify({  
      content: params.prompt,  
      scene: params.uuid,  
      source: 'gpt3.5',  
    })  
    // 处理资源释放  
    let onCanceled: UnCancelTokenListener  
    const done = () => {  
      signal?.removeEventListener?.('abort', onCanceled)  
    }  

    // 封装请求  
    // @ts-ignore  
    let xhr: plus.net.XMLHttpRequest | undefined  
    // @ts-ignore  
    xhr = new plus.net.XMLHttpRequest()  
    xhr.withCredentials = true  
    // 配置终止逻辑  
    if (signal) {  
      signal.addEventListener?.('abort', () => {  
        console.log('fetchStreamChatForApp signal abort')  
        xhr.abort()  
      })  
    }  
    let nLastIndex = 0  
    xhr.onreadystatechange = function () {  
      console.log(`onreadystatechange(${xhr.readyState}) → `)  
      if (xhr.readyState === 4) {  
        if (nLastIndex < xhr.responseText.length) {  
          const responseText = xhr.responseText as string  
          // 处理 HTTP 数据块  
          if (responseText) {  
            const textLen = responseText.length  
            const chunk = responseText.substring(nLastIndex)  
            nLastIndex = textLen  
            listener?.(chunk)  
          }  
        }  
        if (xhr.status === 200) {  
          resolve({ code: ResultCode.SUCCESS, msg: 'end' })  
          done()  
        } else {  
          reject(new Error(xhr.statusText))  
          done()  
        }  
      }  
    }  
    xhr.onprogress = function (event: any) {  
      const responseText = xhr.responseText  
      if (responseText) {  
        const textLen = responseText.length  
        const chunk = responseText.substring(nLastIndex)  
        nLastIndex = textLen  
        listener?.(chunk)  
        console.log('onprogress ', chunk)  
      }  
    }  

    xhr.onerror = function (error: any) {  
      console.error('Network Error:', error)  
      reject(error)  
      done()  
    }  
    // 配置请求  
    xhr.open('POST', 'https://your-site/api/openai/completions/stream')  
    xhr.setRequestHeader('Accept', 'text/event-stream')  
    xhr.setRequestHeader('token', token)  
    xhr.setRequestHeader('User-Agent', 'Mobile')  
    xhr.setRequestHeader('Content-Type', 'application/json')  
    xhr.setRequestHeader('Host', 'mapi.lawvector.cn')  
    xhr.setRequestHeader('Connection', 'keep-alive')  

    // 处理终止逻辑  
    if (signal) {  
      onCanceled = cancel => {  
        console.log('fetchStreamChatForApp onCanceled ', cancel)  
        if (!xhr) {  
          return  
        }  
        reject(new CanceledError('canceled'))  
        xhr.abort()  
        xhr = undefined  
      }  
      // @ts-expect-error no types  
      signal?.aborted ? onCanceled() : signal?.addEventListener('abort', onCanceled)  
    }  
    xhr.send(data)  
  })  
}

当前方案经验证,可以从流式接口获取到数据,但是流式效果不太好,而且从网上汇总来的信息来看,plus.net存在较多问题,比如《plus.net.XMLHttpRequest()在苹果端移动网络环境下不能使用》等。因此 不推荐 plus.net方案

2. event-source-polyfill

参考方案《OpenAI流式请求实现方案》《react + ts + event-source-polyfill 实现方案》《Vue中使用eventSource处理ChatGPT聊天SSE长连接获取数据》

实际App上运行发现报错 TypeError: XMLHttpRequest is not a constructor

哪位大佬可以解决上述问题请补充,不胜感激!

3. fetch-event-source

参考方案《js调用SSE客户端》《fetch-event-source源码解析》ChatGPT-SSE流式响应
经过验证发现单独引入@microsoft/fetch-event-source 会抛出异常
ChatGPT-SSE流式响应分析应该是需要结合renderjs进行使用。
目前推荐使用该方案~

4. App原生语言插件

参考《EventSource (sse)等自定义网络请求 》
因为该插件目前仅支持Android,不推荐。
理论上,原生插件是一定能够解决这个问题,期待大佬们开发更完善的原生插件。

3 关注 分享
苍山暮色烟雨迟 大表哥9312 w***@163.com

要回复文章请先登录注册

操作起来

操作起来

这个插件可以,作者很棒->https://ext.dcloud.net.cn/plugin?id=20971
2024-11-15 11:43
9***@qq.com

9***@qq.com

小程序解析出来是乱码 有碰见的吗
2024-11-09 17:07
3***@qq.com

3***@qq.com

app用这个fetch-event-source有成功的吗?
2024-08-03 17:19
2***@qq.com

2***@qq.com

确实,uniapp自己的ai支持流式传输,然后不给用户开发流式响应的接口,真的恶心
2024-07-09 10:39
英曼畅学

英曼畅学

https://ext.dcloud.net.cn/plugin?id=19103
2024-07-08 11:17
w***@qq.com

w***@qq.com

event-source-polyfill我也不行呀 web端打印的出来 但是在app端一点log都没有 报错也没有 很奇怪
2024-06-01 15:41
c***@haier.com

c***@haier.com

抖音小程序如何实现流式请求?
2024-05-29 09:57
3***@qq.com

3***@qq.com

这种内容过长 正在渲染得时候 ios 点击事件失效
2024-04-29 10:34
苏公子

苏公子

回复 苏公子 :
IOS收不到数据这个也解决了,需要接口允许跨域,接口响应头要设置 【Access-Control-Allow-Origin: '*'】才可以。
2024-03-22 17:26
苏公子

苏公子

IOS一直收不到数据,一直报 【TypeError: Load failed】,是什么原因?
2024-03-22 16:33