vue2 微信小程序前端对接code数据流并用mardown形式展示

270 阅读1分钟

最近工作上遇到一个需求,首次开发对接一个扣子数据流。

业务场景: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数据 展示。