流式接口数据解析(效果如豆包)
代码如下:
javascript
<script lang="ts" setup>
import { ref, nextTick } from 'vue'
import { ElButton } from 'element-plus'
import {
addSessionIdApi
} from '@/api/system/application'
// 流式接口
const chatList = ref<any>([])
const inputValue = ref('')
const answerType = ref(0)
const answering = ref(false)
const sessionId = ref('')
const appIdNew = ref('')
const handleSubmit = async () => {
answerType.value = 1
if (inputValue.value) {
chatList.value.push({
Direction: '1',
answer: inputValue.value,
StartTime: new Date().toLocaleString()
})
await answerAsk(inputValue.value)
inputValue.value = ''
}
}
// 流式问答
const answerAsk = async (title, loop = false) => {
// 获取sessionId
const res = await addSessionIdApi()
if (res.data) {
sessionId.value = res.data
if (title.trim() == '') return
if (answering.value) return
if (answerType.value === 1) {
chatList.value.push({
qaQuestionTitle: title,
Direction: '0',
answer: '',
StartTime: new Date().toLocaleString(),
dataSetInfo: [],
standardQuestion: ''
})
}
appIdNew.value = '1963410044924919808'
answering.value = true
getaskFluxNew(title, loop)
}
setScrollToBottom()
}
// 流式问答
const getaskFluxNew = async (title, loop = false) => {
const res = await fetch('api/core/chat/askFluxNew', {
method: 'POST',
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json' // 请求头
// Authorization:
// 'Bearer gkSxzExAWUFCUSCVIyySaLMULf6MqRtp4Jo0UxIOsc6JT56Xj62iuXdhi1N60SkkXTYk4ws6quXCeDaFQXUtnMV_7iz68t0q2odCFevLpPAcQ2Gvw_LyCRTHCl0bO73N'
},
body: JSON.stringify({
sessionId: sessionId.value,
qaQuestionTitle: title,
appId: appIdNew.value,
history: []
})
})
// 从响应体创建一个读取器 reader,该读取器通过 TextDecoderStream 解码流数据
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader()
// 声明并初始化三个变量:loopAnswer、errMsg 和 isGuiding。
let loopAnswer = '',
errMsg = '',
isGuiding = false
// 开始一个无限循环,用于不断读取和处理来自服务器的流数据。
while (true) {
// 等待并获取读取器的下一个结果。
const result = await reader.read()
// 解构读取结果,提取 value(当前块的数据)和 done(是否完成的标志)。
let value: string = result.value!,
done: boolean = result.done
// 如果 answering.value 为假,则跳出循环
if (!answering.value) {
console.log('stop', done)
break
}
// 如果读取已完成,执行以下操作
if (done) {
console.log('done', done)
break
}
// 将 value 按双换行符分割成多个部分。
let values = value.split('\n\n')
for (let i = 0; i < values.length; i++) {
// 拼接错误消息和当前部分,去掉所有的 data: 前缀。
let _v = (errMsg + values[i]).replace(/data:/g, '')
// 如果去除空白字符后的 _v 为空字符串,跳过当前迭代
if (_v.trim() === '') continue
try {
// 将 _v 解析为 JSON 对象,存储在 _res 中
let _res: { code: number; data; message: string } = JSON.parse(_v)
if (_res.code == 200) {
_res.data?.forEach((item) => {
// 如果存在 item.answer,继续处理
if (item.answer) {
if (answerType.value === 1) {
// 将 item.answer 添加到 chatList 最后一个元素的 answer 属性中 移除所有 <think> 和 </think> 标签 将所有 \\n 替换为实际的换行符 \n 格式化特定的换行模式。
chatList.value[chatList.value.length - 1].answer += (item.answer || '')
.replace(/<think>|<\/think>/g, '')
.replace(/\\n/g, '\n')
.replace(/\n------\n/g, '\n\n------\n\n')
// 设置 Direction 属性为 '0'
chatList.value[chatList.value.length - 1].Direction = '0'
}
// 如果 item.isGuiding 为真,设置 isGuiding 为 tru
if (item.isGuiding) {
isGuiding = true
}
// 将 item.answer 追加到 loopAnswer,并替换 \\n 为实际的换行符 \n
loopAnswer += (item.answer || '').replace(/\\n/g, '\n')
setScrollToBottom()
}
})
}
errMsg = ''
} catch {
errMsg = _v
console.log('errMsg')
}
}
}
if (loopAnswer != '') {
if (answering.value && !isGuiding) {
await getaskFluxNew(title, true)
}
}
if (loop) {
return
}
answering.value = false
}
// 滚动到底部
const aidDataRef = ref()
const aidEndDataRef = ref()
const setScrollToBottom = async () => {
if (answerType.value === 1 || answerType.value === 0) {
nextTick(() => {
if (aidDataRef.value) {
aidDataRef.value.scrollTop = aidDataRef.value.scrollHeight
}
})
}
if (answerType.value === 3) {
nextTick(() => {
if (aidEndDataRef.value) {
aidEndDataRef.value.scrollTop = aidEndDataRef.value.scrollHeight
}
})
}
}
</script>
<template>
<div class="chat-content">
<!-- 消息展示区 -->
<div class="AI-massage" ref="aidDataRef" v-if="chatList.length != 0">
<div class="chat" v-for="(item, index) in chatList" :key="index">
<div :class="item.Direction == '0' ? 'customer' : 'machineMan'">
<div class="photo">
<img
src="@/assets/imgs/photo_customer.png"
v-if="item.Direction == '0'"
/>
<img src="@/assets/imgs/photo_machineMan.png" v-else />
</div>
<div class="word">
<div class="answer">{{ item.answer }}</div>
<div class="time" v-if="item.StartTime">{{ item.StartTime }} </div>
</div>
</div>
</div>
</div>
<!-- 输入框 -->
<div class="AI-input">
<el-input
class="no-resize"
placeholder="从这里开始对话"
v-model="inputValue"
type="textarea"
:style="{
'--el-border-color': 'rgba(255,255,255,0.0)',
'--el-input-focus-border-color': 'transparent',
'--el-input-hover-border-color': 'transparent'
}"
>
</el-input>
<div class="AISend">
<img src="@/assets/imgs/enter.png" alt="" />
<el-button
class="sendBtnStyle"
:class="inputValue.length == 0 ? 'sendBtn' : ''"
:disabled="inputValue.length == 0"
type="primary"
link
@click="handleSubmit"
>
发送
</el-button>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
// 流式接口
.AI-massage {
height: 140px;
padding: 10px;
margin-bottom: 10px;
overflow-y: auto;
.photo {
width: 32px;
margin: 0 8px;
flex-shrink: 0;
img {
width: 32px;
height: 32px;
}
}
.answer {
padding: 10px;
background: #f3f8fb;
}
.time {
font-size: 12px;
color: #6f7275;
}
.machineMan {
display: flex;
flex-direction: row-reverse;
.answer {
border-radius: 12px 0 12px 12px;
}
.time {
display: flex;
justify-content: end;
}
}
.customer {
display: flex;
.answer {
color: #ffffff;
background: #008fe8;
border-radius: 0 12px 12px 12px;
}
.time {
display: flex;
justify-content: start;
}
}
}
</style>