最近工作上遇到一个需求,首次开发对接一个扣子数据流。
业务场景:vue2写的微信小程序,后台接入扣子数据后通过接口形式返回前端,需要前端展示在页面上。 后端返回的是markdown格式数据
1.如何请求数据
const requestTask = wx.request({
url: `${baseUrl}/api/digical/wx/coze/chat/create`,
enableChunked: true,
header: {
'Content-Type': 'application/json',
Accept: 'text/event-stream',
OPENID: getStorageData('openId'),
'TENANT-ID': getStorageData('tenantId'),
},
data: {
content: inputValue,
...params,
},
responseType: 'arraybuffer', // 指定响应类型为流
method: 'POST',
})
requestTask.onChunkReceived(async (response) => {
const text = this.arrayBufferToString(response.data)
rawData += text // 追加数据
// 检测是否收到 `[DONE]`
if (rawData.includes('data:[DONE]')) {
rawData = rawData.replace('data:[DONE]', '') // 去除 `[DONE]`
processRawData() // 处理剩余数据
requestTask.abort()
return
}
// 处理数据块
processRawData()
})
2.接收到的数据response.data 是数据流形式需要转换成真机能识别的字符串形式
// ✅ **手写 `ArrayBuffer` 转字符串(兼容微信小程序)**
arrayBufferToString(arrayBuffer) {
const uint8Arr = new Uint8Array(arrayBuffer)
let text = ''
try {
// ✅ 先尝试使用 TextDecoder(新设备支持)
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8').decode(uint8Arr)
}
} catch (e) {
console.error('TextDecoder 解析失败,回退到手动解析:', e)
}
// 🔥 手动 UTF-8 解码,确保不会乱码(适用于微信小程序 iOS)
let out = ''
let i = 0
while (i < uint8Arr.length) {
let charCode = uint8Arr[i++]
if (charCode < 0x80) {
out += String.fromCharCode(charCode)
} else if (charCode >= 0xc0 && charCode < 0xe0) {
let char2 = uint8Arr[i++]
out += String.fromCharCode(((charCode & 0x1f) << 6) | (char2 & 0x3f))
} else if (charCode >= 0xe0 && charCode < 0xf0) {
let char2 = uint8Arr[i++]
let char3 = uint8Arr[i++]
out += String.fromCharCode(
((charCode & 0x0f) << 12) | ((char2 & 0x3f) << 6) | (char3 & 0x3f)
)
} else if (charCode >= 0xf0) {
let char2 = uint8Arr[i++]
let char3 = uint8Arr[i++]
let char4 = uint8Arr[i++]
let codePoint =
((charCode & 0x07) << 18) |
((char2 & 0x3f) << 12) |
((char3 & 0x3f) << 6) |
(char4 & 0x3f)
codePoint -= 0x10000
out += String.fromCharCode(
0xd800 + (codePoint >> 10),
0xdc00 + (codePoint & 0x3ff)
)
}
}
text = out // 手动 UTF-8 解码后的文本
// 🛠 兼容服务器返回 `data:{"id":...}`
console.log('转换后的字符串:', text)
if (text.startsWith('data:')) {
try {
return JSON.parse(text.slice(5)) // 去掉 "data:" 并解析 JSON
} catch (e) {
console.error('JSON 解析失败:', e)
return text.slice(5)
}
}
return text
},
},
3.解析json数据 拼接成字符串
/**
* 处理 `rawData` 解析 JSON
*/
const processRawData = () => {
let lastIndex = 0
let match
const jsonRegex = /data:\s*(\{.*?\})/gs
while ((match = jsonRegex.exec(rawData)) !== null) {
const jsonString = match[1] // 提取 JSON
lastIndex = match.index + match[0].length
try {
let jsonData = JSON.parse(jsonString)
// **完整文本拦截**
if (
jsonData.type === 'answer' &&
jsonData.content_type === 'text' &&
jsonData.content.trim().length > 20
) {
console.log('❌ 忽略完整回答:', jsonData.content)
continue
}
// **正常数据拼接**
if (
jsonData.content &&
jsonData.content_type === 'text' &&
jsonData.type === 'answer'
) {
this.joinStr(jsonData)
// const newContent = jsonData.content.trim()
// fullContent += newContent !== '↵' ? newContent + ' ' : '\n'
}
// // **触发 UI 更新**
// this.joinStr(jsonData)
} catch (err) {
// **等待下个数据块**
console.warn('⚠️ JSON 解析失败,等待完整数据拼接...')
break
}
}
// **清除已解析的数据**
rawData = rawData.slice(lastIndex)
}
4.拼接json字符串
// 拼接json字符串
joinStr(v) {
if (!v.content) return
// 🚀 逐字拆分并加入消息队列
this.messageQueue.push(...v.content.split(''))
if (!this.isTyping) {
this.processQueue() // 🚀 只有在空闲时才启动队列处理
}
},
5.插入消息队列并以打字机效果实现
// 插入消息队列
processQueue() {
if (this.messageQueue.length === 0) {
this.isTyping = false
this.disabledSendBtn = false
return
}
this.isTyping = true // 🚀 标记为正在打字
const newList = this.messageQueue.splice(0, 10)
console.log(`output->newList,514`, newList, 514)
console.log(`output->this.messageQueue,516`, this.messageQueue, 516)
// console.log(`output->ths.this.messageQueue`,this.messageQueue)
// const nextChar = this.messageQueue.shift() // 取出下一个字符
const nextChar = newList.join('') // 取出下一个字符
console.log(`output->nextChar,513`, nextChar, 513)
// **追加到最后一个 AI 消息**
this.$set(
this.aiMsgList,
this.aiMsgList.length - 1,
(this.aiMsgList[this.aiMsgList.length - 1] || '') + nextChar
)
this.scrollViewKey += 1 // 触发 Vue 重新渲染
this.lastScrollTop += 10
this.scrollToBottom() // **保证滚动到底部**
setTimeout(() => {
this.processQueue() // **继续处理下一个字符**
}, 80) // **50ms/字符**
},
6.页面展示
<mp-html
:tag-style="customStyles"
:content="changeTextMarkdoun(aiMsgList[index])"
></mp-html>
// import MarkdownIt from 'markdown-it'
// const md = new MarkdownIt()
changeTextMarkdoun(t) {
return md.render(t) // 🚀 解析为 rich-text 可用格式
},
总结:第一次写可能表述不是很全 大概思路就是 接收流数据转换成json格式 然后取出conten的字符串数据 拼接起来 控制取数据的字符频率 最后通过markdown-it 转换markdown数据 展示。