feat(ai-chat): 实现AI聊天对话与会话消息记录功能

新增AI聊天对话接口和会话消息记录查询接口,支持SSE流式响应处理。
在sendMessage组件中集成真实SE调用逻辑,替换原有模拟实现,
并完善conversationId的获取与保存机制。新增sseHelper工具类用于统一处理SSE连接和数据解析。
This commit is contained in:
陈昱达
2025-09-24 14:26:37 +08:00
parent be411ec72d
commit 483b57f667
4 changed files with 128 additions and 139 deletions

21
src/api/boe/aiChat.js Normal file
View File

@@ -0,0 +1,21 @@
import ajax from '@/api/boe/boeApiAjax.js'
/**
* AI聊天对话接口
* @param {Object} data - 请求参数
* @param {string} data.conversationId - 会话ID如果为空则创建新会话
* @param {string} data.query - 用户提问内容
* @returns {Promise} - 返回SSE流
*/
export function aiChat(data) {
return ajax.postJson('/xboe/m/boe/case/ai/chat', data)
}
/**
* 查询会话消息记录接口
* @param {string} conversationId - 会话ID
* @returns {Promise} - 返回会话历史记录
*/
export function getChatMessages(conversationId) {
return ajax.get('/xboe/m/boe/case/ai/messages?conversationId=' + conversationId)
}

83
src/utils/sseHelper.js Normal file
View File

@@ -0,0 +1,83 @@
/**
* SSE流式数据处理工具
*/
/**
* 处理SSE响应数据
* @param {String} data - SSE响应数据
* @param {Function} onMessage - 处理消息的回调函数
* @param {Function} onComplete - 完成时的回调函数
* @param {Function} onError - 错误处理回调函数
*/
export function processSSEData(data, onMessage, onComplete, onError) {
try {
const lines = data.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonData = JSON.parse(line.substring(6))
onMessage(jsonData)
// 如果状态为4表示对话结束
if (jsonData.data && jsonData.data.status === 4) {
onComplete()
}
}
}
} catch (error) {
console.error('处理SSE数据时出错:', error)
if (onError) {
onError(error)
}
}
}
/**
* 创建SSE连接
* @param {String} url - 请求地址
* @param {Object} data - 请求数据
* @param {Function} onMessage - 消息处理回调
* @param {Function} onComplete - 完成回调
* @param {Function} onError - 错误回调
* @returns {Promise} - 返回fetch Promise
*/
export function createSSEConnection(url, data, onMessage, onComplete, onError) {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
function read() {
reader.read().then(({ done, value }) => {
if (done) {
if (onComplete) onComplete()
return
}
buffer += decoder.decode(value, { stream: true })
processSSEData(buffer, onMessage, onComplete, onError)
// 继续读取
read()
}).catch(error => {
console.error('SSE读取错误:', error)
if (onError) onError(error)
})
}
// 开始读取数据
read()
}).catch(error => {
console.error('SSE连接错误:', error)
if (onError) onError(error)
})
}

View File

@@ -26,7 +26,7 @@
</div>
<div class="message-suggestions" v-if="messageList[messageList.length-1].textCompleted">
<div class="suggestion-item" v-for="(item, index) in suggestions" :key="index">
<a @click="sendSuggestions"> {{ item }} </a>
<a @click="sendSuggestions(item)"> {{ item }} </a>
</div>
</div>
<div v-if="isLoading" class="loading-message">
@@ -49,6 +49,7 @@
@new-conversation="startNewConversation"
:disabled="isLoading"
class="input-area-wrapper"
ref="sendMessage"
/>
</div>
@@ -121,8 +122,13 @@ export default {
this.suggestions = arr
},
// 处理建议
sendSuggestions(){
this.suggestions = []
sendSuggestions(item){
// this.suggestions = []
this.AIContent = item
setTimeout(()=>{
this.$refs.sendMessage.handleSend()
this.AIContent = ''
},500)
},
startNewConversation() {
this.messageList = [

View File

@@ -19,7 +19,7 @@
</template>
<script>
// import { aiChat } from '@/api/boe/aiChat.js'
import { aiChat } from '@/api/boe/aiChat.js'
export default {
name: 'SendMessage',
@@ -55,7 +55,7 @@ export default {
methods: {
handleSend() {
if (!this.inputContent.trim() || this.disabled) return
console.log(1);
// 添加用户消息到列表
const userMessage = {
isBot: false,
@@ -73,128 +73,7 @@ export default {
this.inputContent = ''
},
// 调用AI聊天接口
callAIChat(question) {
// 创建新的AI消息对象
const aiMessage = {
isBot: true,
text: '',
thinkText: '',
caseRefers: [], // 添加caseRefers字段
textCompleted: false // 添加文字处理完成状态默认为false
};
this.messageList.push(aiMessage);
// 模拟SSE流式响应
this.simulateSSE(question, aiMessage);
},
// 模拟SSE流式响应
simulateSSE(question, aiMessage) {
// 模拟AI思考过程包含think标签内容
const fullResponse =
"<think>" +
"让我思考一下如何更好地回答用户的问题。首先需要分析问题的关键点,然后查找相关的知识库内容。\n" +
"用户询问的是关于" + question + "的问题,我需要提供准确且有用的信息。\n" +
"</think>" +
"正在分析问题:" + question + "\n" +
"检索相关案例...\n" +
"匹配最佳解决方案...\n" +
"您好,我已经收到您的问题:\"" + question + "\"。\n" +
"正在分析相关内容...\n" +
"根据我的知识库,我为您提供以下解答:\n" +
"首先,我们需要了解问题的背景和关键点。\n" +
"在这个案例中,我们可以采用以下步骤来解决:\n" +
"1. 确认问题的具体表现和影响范围\n" +
"2. 分析可能的原因和影响因素\n" +
"3. 制定针对性的解决方案\n" +
"4. 实施并验证解决方案的有效性\n" +
"如果您还有其他相关问题,欢迎继续提问!";
let index = 0;
let inThinkTag = false;
let thinkContent = '';
let mainContent = '';
this.suggestions = [1,2,3]
this.$emit('update-suggestions', [1,2,3]);
// 模拟打字机效果
const typeNextCharacter = () => {
if (index < fullResponse.length) {
const char = fullResponse.charAt(index);
// 处理<think>标签
if (fullResponse.substring(index, index + 7) === '<think>') {
inThinkTag = true;
index += 7; // 跳过<think>标签
setTimeout(typeNextCharacter, 10);
return;
}
// 处理</think>标签
if (fullResponse.substring(index, index + 8) === '</think>') {
inThinkTag = false;
aiMessage.thinkText = thinkContent;
index += 8; // 跳过</think>标签
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
setTimeout(typeNextCharacter, 10);
return;
}
// 根据当前状态添加字符
if (inThinkTag) {
thinkContent += char;
aiMessage.thinkText = thinkContent;
} else {
mainContent += char;
aiMessage.text = mainContent;
}
index++;
setTimeout(typeNextCharacter, 30);
// 实时更新父组件的messageList
this.$emit('update-message', aiMessage);
} else {
// 设置文字处理完成状态为true
aiMessage.textCompleted = true;
// 模拟status 0的caseRefers数据
// 在消息完成后添加引用案例
setTimeout(() => {
aiMessage.caseRefers = [
{
caseId: "case_001",
title: "案例1标题",
authorName: "张三",
keywords: ["关键词1", "关键词2"],
content: "这是案例1的内容摘要..."
},
{
caseId: "case_002",
title: "案例2标题",
authorName: "李四",
keywords: ["关键词3", "关键词4"],
content: "这是案例2的内容摘要..."
}
];
this.$emit('update-message', aiMessage);
}, 500);
// 所有内容完成
this.$emit('loading', false);
}
};
// 开始打字机效果
setTimeout(typeNextCharacter, 100);
},
/*
// 真实的SSE实现暂时注释掉
callAIChat(question) {
// 创建新的AI消息对象
@@ -213,19 +92,8 @@ export default {
query: question
};
// 使用fetch API处理SSE
const url = process.env.VUE_APP_BOE_BASE_API + '/xboe/m/boe/case/ai/chat';
const token = this.$store.getters.token;
// 创建POST请求
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[this.$ajax.tokenName]: token
},
body: JSON.stringify(requestData)
}).then(response => {
aiChat(requestData).then(response => {
// 处理流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
@@ -266,6 +134,10 @@ export default {
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
}
// 从响应中获取并保存conversationId
if (jsonData.data.conversationId) {
this.conversationId = jsonData.data.conversationId;
}
break;
case 1: // 流式对话内容
@@ -296,6 +168,10 @@ export default {
aiMessage.textCompleted = true;
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
// 从响应中获取并保存conversationId
if (jsonData.data.conversationId) {
this.conversationId = jsonData.data.conversationId;
}
break;
case 3: // 返回建议
@@ -305,6 +181,10 @@ export default {
break;
case 4: // 交互完成
// 从响应中获取并保存conversationId
if (jsonData.data.conversationId) {
this.conversationId = jsonData.data.conversationId;
}
this.$emit('loading', false);
break;
}
@@ -339,7 +219,6 @@ export default {
this.$emit('update-message', aiMessage);
});
},
*/
handleNewConversation() {
this.$emit('new-conversation')
}