添加 Accept : EventStream 请求头

This commit is contained in:
joshen
2025-10-14 09:25:05 +08:00
parent 5ebee96ce4
commit 9580dd9735

View File

@@ -0,0 +1,388 @@
<template>
<div class="input-area">
<el-input
v-model="inputContent"
class="input-placeholder"
placeholder="有问题,尽管问"
@keyup.enter.native="handleSend"
:disabled="disabled"
></el-input>
<div class="action-buttons">
<el-button type="primary" size="small" class="start-btn" @click="handleNewConversation">
+ 开启新对话
</el-button>
<el-button type="text" class="send-btn" @click="handleSend" :disabled="disabled">
<i class="el-icon-s-promotion"></i>
</el-button>
</div>
</div>
</template>
<script>
import { aiChat } from '@/api/boe/aiChat.js'
export default {
name: 'SendMessage',
props: {
value: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
messageList: {
type: Array,
default: () => []
},
suggestions: {
type: Array,
default: () => []
}
},
data() {
return {
inputContent: this.value,
conversationId: '' // 会话ID
}
},
watch: {
value(newVal) {
this.inputContent = newVal
}
},
methods: {
handleSend() {
if (!this.inputContent.trim() || this.disabled) return
// 添加用户消息到列表
const userMessage = {
isBot: false,
text: this.inputContent
};
this.messageList.push(userMessage);
// 显示加载状态
this.$emit('loading', true);
// 调用AI聊天接口 (暂时注释掉SSE使用模拟数据)
this.callAIChat(this.inputContent);
// 清空输入框
this.inputContent = ''
},
// 真实的SSE实现暂时注释掉
callAIChat(question) {
// 创建新的AI消息对象
const aiMessage = {
isBot: true,
text: '',
status:null,
thinkText: '',
caseRefers: [], // 添加caseRefers字段
textCompleted: false // 添加文字处理完成状态默认为false
};
this.messageList.push(aiMessage);
// 构造请求参数
const requestData = {
conversationId: this.conversationId,
query: question
};
// 创建POST请求
fetch('/systemapi/xboe/m/boe/case/ai/chat',{
method: 'POST',
headers: {
'Content-Type': 'application/json',
"accept": "text/event-stream",
},
body: JSON.stringify(requestData)
}).then(r=>{
return r
}).then(response => {
// 处理流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let accumulatedContent = ''; // 累积的内容用于打字机效果
let accumulatedThinkContent = ''; // 累积的思考内容
let inThinkSection = false; // 是否在思考部分
let typingTimer = null; // 打字机定时器
let thinkTypingTimer = null; // 思考内容打字机定时器
// 逐字显示文本的函数
const typeText = (message, fullContent) => {
// 如果已有定时器在运行,先清除它
if (typingTimer) {
clearInterval(typingTimer);
}
// 获取当前已显示的文本长度
const currentLength = message.text.length;
// 获取完整文本
const targetLength = fullContent.length;
// 如果已经显示完整文本,不需要继续
if (currentLength >= targetLength) {
return;
}
const typingSpeed = 50; // 每个字符的间隔时间(毫秒)
typingTimer = setInterval(() => {
// 计算下一个要显示的字符索引
const nextIndex = message.text.length + 1;
if (nextIndex <= targetLength) {
message.text = fullContent.substring(0, nextIndex);
this.$emit('update-message', message);
} else {
clearInterval(typingTimer);
typingTimer = null;
// 当打字机效果完成时检查是否应该设置textCompleted为true
// 这应该在status 4交互完成时才设置
if (message.status === 4) {
if (nextIndex >= targetLength) {
message.textCompleted = true;
}
}
}
}, typingSpeed);
};
// 逐字显示思考内容的函数
const typeThinkText = (message, fullThinkContent) => {
// 如果已有定时器在运行,先清除它
if (thinkTypingTimer) {
clearInterval(thinkTypingTimer);
}
// 获取当前已显示的文本长度
const currentLength = message.thinkText.length;
// 获取完整文本
const targetLength = fullThinkContent.length;
// 如果已经显示完整文本,不需要继续
if (currentLength >= targetLength) {
return;
}
// 从当前显示位置开始继续显示(避免清空重显)
const startIndex = currentLength;
const typingSpeed = 20; // 每个字符的间隔时间(毫秒)
thinkTypingTimer = setInterval(() => {
// 计算下一个要显示的字符索引
const nextIndex = message.thinkText.length + 1;
if (nextIndex <= targetLength) {
message.thinkText = fullThinkContent.substring(0, nextIndex);
this.$emit('update-message', message);
} else {
clearInterval(thinkTypingTimer);
thinkTypingTimer = null;
}
}, typingSpeed);
};
// 添加一个检查是否所有文本都已完成显示的函数
const isTextDisplayCompleted = (message, fullContent) => {
return message.text.length >= fullContent.length;
};
// 读取流数据
const read = () => {
reader.read().then(({ done, value }) => {
if (done) {
// 当流结束时,等待打字机效果完成
const waitForTyping = () => {
if (!typingTimer) {
this.$emit('loading', false);
} else {
setTimeout(waitForTyping, 100);
}
};
waitForTyping();
return;
}
// 解码数据
buffer += decoder.decode(value, { stream: true });
// 按行分割数据
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
if (line.startsWith('data:')) {
try {
const jsonData = JSON.parse(line.substring(5));
// 根据status处理不同类型的数据
switch (jsonData.status) {
case 0: // 引用文件
// 处理引用文件信息
if (jsonData.fileRefer && jsonData.fileRefer.caseRefers) {
aiMessage.caseRefers = jsonData.fileRefer.caseRefers;
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
}
// 从响应中获取并保存conversationId
if (jsonData.conversationId) {
this.conversationId = jsonData.conversationId;
sessionStorage.setItem('conversationId', jsonData.conversationId);
}
break;
case 1: // 流式对话内容
// 处理
const content = jsonData.content;
aiMessage.hasThink = false;
if (content.startsWith('<think>')) {
aiMessage.hasThink = true
inThinkSection = true;
accumulatedThinkContent = content.replace('<think>', '');
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else if (content.startsWith('</think>')) {
inThinkSection = false;
accumulatedThinkContent += content.replace('</think>', '');
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else if (inThinkSection) {
accumulatedThinkContent += content;
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else {
// 累积内容并使用打字机效果更新显示
accumulatedContent += content;
// 如果thinkText已经显示完整则继续使用打字机效果显示内容
if( aiMessage.hasThink){
if(aiMessage.thinkText.length >=accumulatedThinkContent.length){
typeText(aiMessage, accumulatedContent);
}
} else {
typeText(aiMessage, accumulatedContent);
}
}
// 不在这里直接更新,让打字机效果处理更新
break;
case 2: // 回答完成
// 不再在这里设置textCompleted状态
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
// 从响应中获取并保存conversationId
break;
case 3: // 返回建议
// 这里可以处理建议问题
this.$emit('update-suggestions', jsonData.suggestions);
break;
case 4: // 交互完成
aiMessage.status = 4
// 从响应中获取并保存conversationId
this.$emit('loading', false);
// 检查文本是否已经完全显示如果是则设置textCompleted为true
if (isTextDisplayCompleted(aiMessage, accumulatedContent)) {
// aiMessage.textCompleted = true;
this.$emit('update-message', aiMessage);
}
break;
}
} catch (error) {
console.error('解析SSE数据错误:', error);
}
}
}
// 继续读取
read();
}).catch(error => {
console.error('SSE连接错误:', error);
// 出错时也设置文字处理完成状态
if (typingTimer) {
clearInterval(typingTimer);
typingTimer = null;
}
aiMessage.textCompleted = true;
this.$emit('loading', false);
aiMessage.text = '当前无法获取回答,请稍后重试';
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
});
};
// 开始读取数据
read();
}).catch(error => {
console.error('请求失败:', error);
// 出错时也设置文字处理完成状态
aiMessage.textCompleted = true;
this.$emit('loading', false);
aiMessage.text = '当前无法获取回答,请稍后重试';
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
});
},
handleNewConversation() {
this.conversationId = ''
this.$emit('new-conversation')
}
}
}
</script>
<style scoped lang="scss">
.input-area {
background-color: white;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 5px 16px 10px 16px;
display: flex;
flex-direction: column;
.input-placeholder {
color: #999;
font-size: 14px;
margin: 0;
border: none;
::v-deep .el-input__inner {
border: none;
padding: 0;
height: 30px;
}
}
.action-buttons {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 5px;
.start-btn {
padding: 6px 10px;
font-size: 12px;
border-radius: 4px;
color: #409eff;
background-color: #f5f7fa;
border: 1px solid #dcdfe6;
}
.send-btn {
font-size: 18px;
color: #409eff;
padding: 6px;
&[disabled] {
color: #c0c4cc;
}
}
}
}
</style>