从流中获取的数据格式如下

小程序调用SSE接口
const requestTask = wx.request({
url: `xxx`,
enableChunked: true,
method: "GET",
timeout: '120000',
success(res) {
console.log(res.data)
},
fail: function (error) {
console.error(error);
},
complete: function () {
console.log('请求完成', str);
}
})
requestTask.onChunkReceived(res => {
console.log( res, res.data);
})
我这边接收到的数据类型为Uint8Array,需要处理成text文本(如上图)

requestTask.onChunkReceived(res => {
console.log( res, res.data);
let arrayBuffer = res.data;
let decoder = new TextDecoder('utf-8');
let text = decoder.decode(arrayBuffer);
const eventRegex = /event:data\ndata:"data:(.*?)"/g;
const eventRegexErr = /event:600\ndata:"(.*?)"/g;
let matches = [];
let match;
if (text.indexOf('600') != -1) {
while ((match = eventRegexErr.exec(text)) !== null) {
wx.showToast({
title: match[1],
})
matches.push(match[1]);
}
str = str + matches.join('')
} else {
while ((match = eventRegex.exec(text)) !== null) {
matches.push(match[1]);
}
str = str + matches.join('')
console.log(text, str);
}
})
TextDecoder在真机上没法使用,真机上需要使用另一种
arrayBufferToString(arr) {
if (typeof arr === 'string') {
return arr;
}
var dataview = new DataView(arr);
var ints = new Uint8Array(arr.byteLength);
for (var i = 0; i < ints.length; i++) {
ints[i] = dataview.getUint8(i);
}
var str = '',
_arr = ints;
for (var i = 0; i < _arr.length; i++) {
if (_arr[i]) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for (var st = 1; st < bytesLength; st++) {
if (_arr[st + i]) {
store += _arr[st + i].toString(2).slice(2);
}
}
str += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
str += String.fromCharCode(_arr[i]);
}
}
}
return str;
},
使对话有打字机效果
参考自:小程序实现 ChatGPT 聊天打字兼自动滚动效果
handleRequestResolve(result) {
this.setData({
currentContent: ''
})
const contentCharArr = result.trim().split("")
this.showText(0, contentCharArr);
},
showText(key = 0, value) {
if (key >= value.length) {
this.setData({
isShowFinish: true
})
return;
}
this.setData({
currentContent: this.data.currentContent + value[key],
})
setTimeout(() => {
this.showText(key + 1, value);
}, 50);
},
对话滚动到可视区域内
handleScollTop() {
return new Promise((resolve) => {
const query = wx.createSelectorQuery()
query.select('.page-content').boundingClientRect()
query.select('.scroll-view-content').boundingClientRect()
query.exec((res) => {
const scrollViewHeight = res[0].height
const scrollContentHeight = res[1].height
if (scrollContentHeight > (scrollViewHeight - 200)) {
const scrollTop = scrollContentHeight - scrollViewHeight + 200
this.setData({
scrollTop
}, () => {
resolve()
})
} else {
resolve()
}
})
})
},
showText(key = 0, value) {
if (key >= value.length) {
this.setData({
isShowFinish: true
})
return;
}
this.setData({
currentContent: this.data.currentContent + value[key],
}, () => {
this.handleScollTop().then(() => {
setTimeout(() => {
this.showText(key + 1, value);
}, 20);
})
})
},
完整代码
.wxml
<scroll-view scroll-y scroll-top="{{scrollTop}}" wx:else class="page-content {{isFirst ? '' : 'page-content-bg'}}">
<view class="scroll-view-content">
<view wx:for="{{talkArr}}" wx:key="index" class="talk-box1">
<view class="talk-box-question" wx:if="{{item.isAnswer=='0'}}">
<view class="left">
<text class="left-content">{{item.content}}</text>
</view>
<image class="right" src="../images/user-icon.png" mode="aspectFill" />
</view>
<view class="talk-box-reply" wx:else>
<image class="left" src="../images/ai-icon.png" mode="aspectFill" />
<view class="right">
<view class="right-content">
<view wx:if="{{(index!=talkArr.length-1)}}">{{item.content}}</view>
<view wx:else>
<view wx:if="{{loading}}">
<image class="loading" src="../images/loading-1.png" mode="aspectFill" />
</view>
<view wx:else>
{{currentContent}}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
.wxss
.page-content {
width: 100%;
margin-top: 48rpx;
padding-top: 150rpx;
}
.page-content-bg {
background: #F5F6F7;
height: 75%;
padding-bottom: 280rpx;
overflow: scroll;
padding-top: 0;
}
.scroll-view-content {
padding-top: 50rpx;
}
.talk-box {
display: flex;
}
.talk-box1 {
width: 90%;
margin: 0 auto;
}
.talk-box .left {
width: 80rpx;
height: 80rpx;
}
.talk-box .right {
margin-left: 30rpx;
flex: 1;
}
.talk-item {
height: 92rpx;
background: #F6FFF9;
border-radius: 0rpx 20rpx 20rpx 20rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: rgba(51, 51, 51, 0.9);
text-align: left;
display: flex;
align-items: center;
padding: 0 38rpx;
}
.talk-box-question,
.talk-box-reply {
width: 100%;
display: flex;
margin-bottom: 32rpx;
}
.talk-box-question .left {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
}
.left-content {
background: linear-gradient(273deg, #44BE35 0%, #6ECB63 100%);
box-shadow: 0rpx 2rpx 8rpx 0rpx rgba(0, 0, 0, 0.05);
border-radius: 24rpx 0rpx 24rpx 24rpx;
padding: 24rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
color: #FFFFFF;
line-height: 44rpx;
text-align: left;
}
.talk-box-question .right {
margin-left: 30rpx;
width: 80rpx;
height: 80rpx;
}
.talk-box-reply .left {
width: 80rpx;
height: 80rpx;
}
.talk-box-reply .right {
margin-left: 30rpx;
flex: 1;
display: flex;
align-items: center;
justify-content: flex-start;
}
.right-content {
background: #FFFFFF;
box-shadow: 0rpx 2rpx 8rpx 0rpx rgba(0, 0, 0, 0.05);
border-radius: 0rpx 24rpx 24rpx 24rpx;
border: 2rpx solid #6ECB63;
padding: 24rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
color: rgba(0, 0, 0, 0.9);
line-height: 46rpx;
text-align: left;
}
.js
data: {
isShowFinish: false,
scrollTop: '',
currentContent: '',
loading: false,
talkArr: []
},
submit() {
if (stringUtils.isNull(this.data.content)) {
wx.showToast({
title: '请输入提问内容',
icon: 'none'
})
this.setData({
content: ''
})
return
}
if (!this.data.isShowFinish) {
return
}
if (audioContext) {
audioContext.destroy()
audioContext = ''
}
let talkItem = {
dialogueId: uuid.guid(),
themeId: this.data.themeId,
content: this.data.content,
isAnswer: '0',
}
let answerItem = {
dialogueId: uuid.guid(),
themeId: this.data.themeId,
content: '',
isAnswer: '1',
parentId: talkItem.dialogueId,
}
this.setData({
isFirst: false,
isCopying: false,
isSharing: false,
isShowFinish: false
})
this.setData({
talkArr: [...this.data.talkArr, talkItem, answerItem],
scrollTop: this.data.scrollTop + 400
})
this.getDataStream(this.data.content)
this.setData({
content: ''
})
},
getDataStream(data) {
let str = ''
let that = this
this.setData({
loading: true,
})
const requestTask = wx.request({
enableChunked: true,
url: `xxx`,
enableChunked: true,
method: "GET",
responseType: "arraybuffer",
timeout: '120000',
success(res) {},
fail: function (error) {
console.error(error);
},
complete: function () {
that.handleRequestResolve(str)
let index = that.data.talkArr.length - 1
let answerContent = `talkArr[${index}].content`
that.setData({
[answerContent]: str,
loading: false
})
}
})
requestTask.onChunkReceived(res => {
let arrayBuffer = res.data;
let decoder = new TextDecoder('utf-8');
let text = decoder.decode(arrayBuffer);
const eventRegex = /event:data\ndata:"data:(.*?)"/g;
const eventRegexErr = /event:600\ndata:"(.*?)"/g;
let matches = [];
let match;
if (text.indexOf('600') != -1) {
while ((match = eventRegexErr.exec(text)) !== null) {
wx.showToast({
title: match[1],
icon: 'none'
})
matches.push(match[1]);
}
str = str + matches.join('')
} else {
while ((match = eventRegex.exec(text)) !== null) {
matches.push(match[1]);
}
str = str + matches.join('')
}
})
requestTask.offChunkReceived(res => {})
},
handleScollTop() {
return new Promise((resolve) => {
const query = wx.createSelectorQuery()
query.select('.page-content').boundingClientRect()
query.select('.scroll-view-content').boundingClientRect()
query.exec((res) => {
const scrollViewHeight = res[0].height
const scrollContentHeight = res[1].height
if (scrollContentHeight > (scrollViewHeight - 200)) {
const scrollTop = scrollContentHeight - scrollViewHeight + 200
this.setData({
scrollTop
}, () => {
resolve()
})
} else {
resolve()
}
})
})
},
arrayBufferToString(arr) {
if (typeof arr === 'string') {
return arr;
}
var dataview = new DataView(arr);
var ints = new Uint8Array(arr.byteLength);
for (var i = 0; i < ints.length; i++) {
ints[i] = dataview.getUint8(i);
}
var str = '',
_arr = ints;
for (var i = 0; i < _arr.length; i++) {
if (_arr[i]) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for (var st = 1; st < bytesLength; st++) {
if (_arr[st + i]) {
store += _arr[st + i].toString(2).slice(2);
}
}
str += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
str += String.fromCharCode(_arr[i]);
}
}
}
return str;
},
handleRequestResolve(result) {
this.setData({
currentContent: ''
})
const contentCharArr = result.trim().split("")
this.setData({
isShowFinish: false
})
this.showText(0, contentCharArr);
},
showText(key = 0, value) {
if (key >= value.length) {
this.setData({
isShowFinish: true
})
return;
}
this.setData({
currentContent: this.data.currentContent + value[key],
}, () => {
this.handleScollTop().then(() => {
setTimeout(() => {
this.showText(key + 1, value);
}, 20);
})
})
},