1.获取流式的接口根据格式拆解出想要的数据
// 首先一个完整的data格式是
data: {"msg_id": "4365c87e-c0db-11ef-b910-0242ac12000c", "created": 1734923343, "choices": [{"delta": {"content": "xxx"}, "index": 16, "finish_reason": null, "error_info": null, "node_name": "xxx", "meta": {}}], "type": "str", "text_print": true, "params": []}
- 要将后端不断返回的内容按照上面的格式先拆解,一开始我是用data :来分割,无法解析的就留在下一次合并后再分割,但是因为接口返回速度有时候很快,短时间很多数据都无法拆分先存到数组中,导致存储的文本太多数组超出长度报错,所以只能按照']}' 结束语来分割。
// 获取数据分段处理的核心代码
async publicChatRoom(param) {
const token = await this.getToken()
const response = await fetch('url', {
method: 'POST',
headers: {
'content-type': 'application/json;charset=utf-8',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
param,
}),
})
let generateData = ''
const decoder = new TextDecoder('utf-8')
const reader = response.body.getReader() // 获取流的读取器
while (true) {
const processTextStream = ({ done, value }) => {
if (done) {
// 此处可以写流式接口返回完毕后的逻辑
return
}
// 当done为false时,表示还有数据需要处理
// 将读取到的二进制数据块value使用decoder解码为文本数据chunk
const chunk = decoder.decode(value, { stream: true })
// 将文本数据chunk按照换行符进行分割,得到一个包含每行文本的数组lines
let lines = (generateData + chunk).split(']}\n\n')
lines = lines.map((item, index) => {
return index === lines.length - 1 ? item : item + ']}'
})
let repeatList = lines[0].split('data: {"msg_id"').filter(item => {
return item
})
if (repeatList.length > 1) {
lines[0] = '{"msg_id"' + repeatList[repeatList.length - 1]
}
for (const line of lines) {
// 如果该行文本不为空(通过line.trim()判断)
if (line) {
const adopt = this.isValidJsonObject(line.replace('data: ', '')) // 尝试解析字符串
if (adopt) {
// curdata为完整的一个mag格式
const curdata = JSON.parse(line.replace('data: ', ''))
// 下面可以继续写获取到数据后的逻辑....
generateData = ''
} else {
generateData += line
// console.log('解析失败的', generateData)
}
}
}
return reader.read().then(processTextStream)
}
return reader.read().then(processTextStream)
}
}
2.页面输出要有打字机效果
- 相关变量
displayText: '', // 打字机输出的有效回答
typingInterval: null,
currentCharIndex: 0, // 记录页面输出到哪个字的位置
typerCompleted: true, // 打印机效果是否完成
answerCompleted // 后端返回的数据是否回答完毕
- 开始调用打印方法typeLine
startTyping() {
// 清空显示文本,重新开始
this.displayText = '' // 存放页面输出文字的变量
this.typerCompleted = false // 是否完成打印输出
if (this.typingInterval) {
clearInterval(this.typingInterval)
}
this.typingInterval = setInterval(this.typeLine, 20) // 这里调整打字速度
}
- typeLine 核心代码
typeLine() {
let text = content // content是解析出来的回答,是不断在增加的
if (text !== undefined && text.length > this.currentCharIndex) {
this.displayText += text[this.currentCharIndex]
this.currentCharIndex = this.currentCharIndex + 1
setTimeout(() => {
this.scrollToBottom() // 每次输出了内容,都要滚动到回答的最下面
}, 500)
if (text.length === this.currentCharIndex && this.answerCompleted) {
clearInterval(this.typingInterval) // 清除定时器
this.typerCompleted = true
this.scrollToBottom()
this.typingInterval = null
this.currentCharIndex = 0
}
}
},
3.要将输出的回答展示为markdown格式需要用到的组件(两个都可以)
- vue-markdown
<vue-markdown :source="displayText"></vue-markdown>
- marked.js
<span v-html="compiledMarkdown" id="answerMarkdown" ></span>
// 写在计算属性中
compiledMarkdown() {
return marked.parse(this.displayText)
}