微信小程序除了WebSocket其他思路实现流传输文字(打字机)效果

7,248 阅读3分钟

本文着重介绍怎么实现在小程序内实现类似文本流传输的效果。不对openAi接口做过多介绍

思路

  • 调的通openai的api --> 成功响应 --> 返回stream数据流
  • 自己服务器处理stream数据流,转换成小程序可以接受的数据
  • 小程序接收数据并进行处理

实现

这里以nodejs为例

  • 服务端发出请求,拿到steam流,简单说就是调用api的时候stream参数传true
// 简单示例
const params = {
    stream: true,
    // ...more params
}
const response = await openai.createChatCompletion(params, {
    responseType: 'stream',
})

  • 拿到了响应体,并设置了responseTypestream,假设我们现在直接将数据流返回
const stream = response.data;

res.set({
    'Content-Type': 'text/event-stream',
});

stream.pipe(res);
  • 然后在小程序端调用 wx.request方法,你会发现在 success 回调中,并不是实时返回的,而是最后的完整结果。显然这种方法不行~

微信小程序请求不支持接受stream流

替代方案 onChunkReceived

文档介绍:监听 Transfer-Encoding Chunk Received 事件。当接收到新的chunk时触发。

服务端改造

  • 设置响应头 Transfer-Encoding: Chunk,然后监听 stream 的 on/end时间,实时返回数据
// 设置响应头
res.set({
   'Transfer-Encoding': 'chunked',
   'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
});

const stream = response.data;
// 实时返回
stream.on('data', chunk => {
    res.write(chunk)
})
// 结束响应
stream.on('end', () => {
    res.end();
});

小程序处理

在小程序中发起请求时,开启 enableChunked,并监听onChunkReceived的回调,进行数据处理

  • 坑点一: 因为返回的是arrayBuffer数据类型,怎么解码成了问题。这里推荐使用 text-encoding-shim 这个库来解决,因为小程序没有浏览器环境的TextDecoder,虽然IDE中可以使用,但是真机会挂掉的。
import * as TextEncoding from 'text-encoding-shim';

const requestTask = wx.request({
    url: '/v1/chat/completions', // 服务端接口地址
    data: {},
    method: 'POST',
    enableChunked: true,
    success: response => {
        // 开启enableChunked后,成功的回调一般用不到,因为响应数据不在这里返回
        console.log(response)
    }
})

requestTask.onChunkReceived(chunk => {
    const arrayBuffer = response.data;
    const uint8Array = new Uint8Array(arrayBuffer);
    const str = new TextEncoding.TextDecoder('utf-8').decode(uint8Array);
    // 看一下 打印出来的结果
    console.log(str)
})

以上 我们就解决了小程序接受的问题啦。IDE可能看不出来实时的效果,用真机调试就可以啦

  • 坑点二:你以为这就结束了?还有很关键的一步,那就是nginx的配置

在nginx中开启transfer_encoding, 同时关闭缓存 proxy_buffering

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

location /v1/chat/completions {
    # ...more config
    
    proxy_set_header Transfer-Encoding "";
    chunked_transfer_encoding on;
    proxy_buffering off;
}

保存重启,再来看请求结果,到这里就完美解决啦~

补充一个微信原生 请求示例代码,可直接请求 openAi 接口

// 这里我使用 npm 安装了 text-encoding-shim 这个库,然后将 dist内的产物copy了出来
import * as TextEncoding from './text-encoding-shim';

const domain = 'https://api.openai.com'
const Authorization = "Bearer THERE IS KEY"

// 聊天生成
function ApiChat(params: any) {
  return wx.request({
    url: `${domain}/v1/chat/completions`,
    header: {
      "Authorization": Authorization
    },
    method: 'POST',
    enableChunked: true,
    data: {
      ...params,
      stream: true,
      stream_options: { include_usage: true },
    }
  })
}

type Listener = (arrayBuffer: ArrayBuffer) => void
function decodeStream(fn: (messages: any) => void): Listener {
  let str = ''
  let p = 0

  function parseStr(str: string) {
    const _str = str.substring(6)
    try {
      const message = JSON.parse(_str)
      fn?.(message)
    } catch (err) {
      console.log(str);
      console.log(err);
    }
  }

  function decodeFn() {
    while (str.indexOf('data: ', p) !== -1) {
      let line = ''
      const nextIndex = str.indexOf('data: ', p)
      line = str.substring(p - 1, nextIndex);
      p = nextIndex + 1; // 移动到下一个字符
      if (line.trim()) {
        parseStr(line)
      }
    }
  }
  return function (arraybuffer: ArrayBuffer) {
    const uint8Array = new Uint8Array(arraybuffer);
    const decodeBase64Str = new TextEncoding.TextDecoder('utf-8').decode(uint8Array);
    str += decodeBase64Str
    decodeFn()
  }
}

export function useChat(
    chatParams: { model: string, messages: any[] },
    fn: (options: any /* openAi响应的结果类型 */) => void
) {

  const requestTask = ApiChat(chatParams)

  const listener = decodeStream((message: any  /* openAi响应的结果类型 */ ) => {
    fn(message)
    // if (message?.usage) return
    // const text = message.choices[0]?.delta?.content || ''
    // console.log(text)
  })

  requestTask.onChunkReceived((res) => listener(res.data))
}

最后附上我自己的小程序码,感兴趣的小伙伴可以体验一下啦。(因为是我个人的openai的账号,所以每个人只有10条免费的用量哦) 顺便宣传一下自己的PC网站吧QAQ点这里

zhi-you.jpg