本文着重介绍怎么实现在小程序内实现类似文本流传输的效果。不对openAi接口做过多介绍
思路
- 调的通openai的api --> 成功响应 --> 返回stream数据流
- 自己服务器处理stream数据流,转换成小程序可以接受的数据
- 小程序接收数据并进行处理
实现
这里以nodejs为例
- 服务端发出请求,拿到steam流,简单说就是调用api的时候
stream
参数传true
// 简单示例
const params = {
stream: true,
// ...more params
}
const response = await openai.createChatCompletion(params, {
responseType: 'stream',
})
- 拿到了响应体,并设置了
responseType
为stream
,假设我们现在直接将数据流返回
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点这里