1.修复AI卡顿问题

This commit is contained in:
2025-08-18 22:10:21 +08:00
parent 73b43d609f
commit a2f6d5573b
3 changed files with 319 additions and 166 deletions

View File

@@ -1,4 +1,4 @@
import request from '@/utils/request' import request from "@/utils/request";
/** /**
* 获取聊天历史记录 * 获取聊天历史记录
@@ -9,29 +9,24 @@ import request from '@/utils/request'
* @param {string} [params.beforeId] 获取此ID之前的记录 * @param {string} [params.beforeId] 获取此ID之前的记录
* @returns {Promise} 包含历史记录的Promise * @returns {Promise} 包含历史记录的Promise
*/ */
export const getHistory = ({ export const getHistory = ({ conversationId, user, limit = 20, beforeId }) => {
const params = {
conversationId, conversationId,
user, user,
limit = 20, limit,
beforeId };
}) => {
const params = {
conversationId,
user,
limit
}
// 如果有beforeId参数添加到请求中后端参数名为firstId // 如果有beforeId参数添加到请求中后端参数名为firstId
if (beforeId) { if (beforeId) {
params.firstId = beforeId params.firstId = beforeId;
} }
return request({ return request({
url: '/aitutor/aichat/getMessagesToUser', url: "/aitutor/aichat/getMessagesToUser",
method: 'get', method: "get",
params params,
}) });
} };
/** /**
* 发送反馈(点赞/点踩) * 发送反馈(点赞/点踩)
@@ -41,21 +36,17 @@ export const getHistory = ({
* @param {string} params.user 用户ID * @param {string} params.user 用户ID
* @returns {Promise} 包含操作结果的Promise * @returns {Promise} 包含操作结果的Promise
*/ */
export const sendFeedback = ({ export const sendFeedback = ({ messageId, action, user }) => {
messageId, return request({
action, url: "/aitutor/aichat/feedback",
user method: "post",
}) => { data: {
return request({ message_id: messageId,
url: '/aitutor/aichat/feedback', rating: action === 1 ? "like" : "dislike",
method: 'post', user,
data: { },
message_id: messageId, });
rating: action === 1 ? 'like' : 'dislike', };
user
}
})
}
/** /**
* 上传文件 * 上传文件
@@ -64,16 +55,16 @@ export const sendFeedback = ({
* @returns {Promise} 包含文件URL的Promise * @returns {Promise} 包含文件URL的Promise
*/ */
export const uploadFile = (formData, user) => { export const uploadFile = (formData, user) => {
formData.append('user', user) formData.append("user", user);
return request({ return request({
url: '/aitutor/aichat/files/upload', url: "/aitutor/aichat/files/upload",
method: 'post', method: "post",
data: formData, data: formData,
headers: { headers: {
'Content-Type': 'multipart/form-data' "Content-Type": "multipart/form-data",
} },
}) });
} };
/** /**
* 创建新会话 * 创建新会话
@@ -82,15 +73,15 @@ export const uploadFile = (formData, user) => {
* @returns {Promise} 包含新会话ID的Promise * @returns {Promise} 包含新会话ID的Promise
*/ */
export const createConversation = (user, title) => { export const createConversation = (user, title) => {
return request({ return request({
url: '/aitutor/aichat/conversation/create', url: "/aitutor/aichat/conversation/create",
method: 'post', method: "post",
data: { data: {
user, user,
title title,
} },
}) });
} };
/** /**
* 删除会话 * 删除会话
@@ -99,12 +90,12 @@ export const createConversation = (user, title) => {
* @returns {Promise} 包含操作结果的Promise * @returns {Promise} 包含操作结果的Promise
*/ */
export const deleteConversation = (conversationId, user) => { export const deleteConversation = (conversationId, user) => {
return request({ return request({
url: '/aitutor/aichat/conversation/delete', url: "/aitutor/aichat/conversation/delete",
method: 'post', method: "post",
data: { data: {
conversation_id: conversationId, conversation_id: conversationId,
user user,
} },
}) });
} };

View File

@@ -24,8 +24,14 @@
<button class="retry-button" @click="retryInit">重试</button> <button class="retry-button" @click="retryInit">重试</button>
</div> </div>
<!-- 消息过多提示 -->
<div v-if="messages.length > maxVisibleMessages" class="message-limit-notice">
<div class="notice-text">为了保证页面性能仅显示最新的 {{ maxVisibleMessages }} 条消息</div>
<div class="notice-subtext"> {{ messages.length }} 条消息</div>
</div>
<!-- 消息项 --> <!-- 消息项 -->
<div v-for="(message, index) in messages" :key="index" class="message-item"> <div v-for="(message, index) in visibleMessages" :key="index" class="message-item">
<!-- 用户消息 --> <!-- 用户消息 -->
<div v-if="message.sender === 'user'" class="user-message"> <div v-if="message.sender === 'user'" class="user-message">
<div class="message-content"> <div class="message-content">
@@ -53,8 +59,9 @@
<!-- 引用来源 --> <!-- 引用来源 -->
<div v-if="message.retrieverResources && message.retrieverResources.length > 0" class="reference-sources"> <div v-if="message.retrieverResources && message.retrieverResources.length > 0" class="reference-sources">
<div class="reference-title">引用来源</div> <div class="reference-title">引用来源</div>
<div v-for="(group, groupIndex) in getGroupedReferences(message.retrieverResources)" :key="groupIndex" <div
class="reference-group"> v-for="(group, groupIndex) in (message.groupedReferences || getGroupedReferences(message.retrieverResources))"
:key="groupIndex" class="reference-group">
<div class="reference-doc" <div class="reference-doc"
:class="{ expanded: showSingleReference[`${message.messageId}-${group.docName}`] }" :class="{ expanded: showSingleReference[`${message.messageId}-${group.docName}`] }"
@click="toggleSingleReference(message.messageId, group.docName)"> @click="toggleSingleReference(message.messageId, group.docName)">
@@ -168,11 +175,23 @@ export default {
performanceMonitor: null, performanceMonitor: null,
memoryCheckInterval: null, memoryCheckInterval: null,
lastMemoryUsage: 0, lastMemoryUsage: 0,
// 请求监控 // 请求监控
activeRequests: new Map(), // 存储活跃请求的信息 activeRequests: new Map(), // 存储活跃请求的信息
requestMonitorInterval: null, requestMonitorInterval: null,
maxRequestDuration: 60000 // 最大请求持续时间60秒 maxRequestDuration: 60000, // 最大请求持续时间60秒
// Markdown渲染缓存
markdownCache: new Map(), // 缓存已渲染的Markdown内容
maxCacheSize: 100, // 最大缓存条目数
// 引用资源分组缓存
referencesCache: new Map(), // 缓存引用资源分组结果
maxReferencesCacheSize: 50, // 最大引用缓存条目数
// 消息显示优化
maxVisibleMessages: 50, // 最大可见消息数量
messageRenderBatch: 20, // 每批渲染的消息数量
} }
}, },
computed: { computed: {
@@ -189,6 +208,14 @@ export default {
// 获取用户名 // 获取用户名
userName() { userName() {
return this.name || localStorage.getItem('userName') || '用户' return this.name || localStorage.getItem('userName') || '用户'
},
// 获取可见消息列表(性能优化)
visibleMessages() {
if (this.messages.length <= this.maxVisibleMessages) {
return this.messages
}
// 只显示最新的消息,保持对话连续性
return this.messages.slice(-this.maxVisibleMessages)
} }
}, },
watch: { watch: {
@@ -200,9 +227,9 @@ export default {
if (!this.isDestroyed) { if (!this.isDestroyed) {
setTimeout(() => { setTimeout(() => {
if (!this.isDestroyed) { if (!this.isDestroyed) {
this.scrollToBottom(false) this.scrollToBottom(false)
} }
}, 100) }, 100)
} }
}) })
} }
@@ -222,7 +249,7 @@ export default {
// 启动性能监控 // 启动性能监控
this.startPerformanceMonitoring() this.startPerformanceMonitoring()
// 启动请求监控 // 启动请求监控
this.startRequestMonitoring() this.startRequestMonitoring()
@@ -240,38 +267,44 @@ export default {
beforeDestroy() { beforeDestroy() {
// 设置销毁标志 // 设置销毁标志
this.isDestroyed = true this.isDestroyed = true
// 移除全局错误处理器 // 移除全局错误处理器
window.removeEventListener('unhandledrejection', this.handleUnhandledRejection) window.removeEventListener('unhandledrejection', this.handleUnhandledRejection)
window.removeEventListener('error', this.handleGlobalError) window.removeEventListener('error', this.handleGlobalError)
// 停止性能监控 // 停止性能监控
this.stopPerformanceMonitoring() this.stopPerformanceMonitoring()
// 停止请求监控 // 停止请求监控
this.stopRequestMonitoring() this.stopRequestMonitoring()
// 清理定时器 // 清理定时器
if (this.loadDebounceTimer) { if (this.loadDebounceTimer) {
clearTimeout(this.loadDebounceTimer) clearTimeout(this.loadDebounceTimer)
this.loadDebounceTimer = null this.loadDebounceTimer = null
} }
// 取消正在进行的请求 // 取消正在进行的请求
if (this.currentCancel) { if (this.currentCancel) {
this.currentCancel() this.currentCancel()
this.currentCancel = null this.currentCancel = null
} }
// 清理请求队列 // 清理请求队列
this.requestQueue = [] this.requestQueue = []
// 清理消息数据 // 清理消息数据
this.messages = [] this.messages = []
// 清理引用展示状态 // 清理引用展示状态
this.showSingleReference = {} this.showSingleReference = {}
// 清理markdown缓存
this.markdownCache.clear()
// 清理引用资源分组缓存
this.referencesCache.clear()
// 重置所有状态 // 重置所有状态
this.conversation_id = '' this.conversation_id = ''
this.earliestMessageId = null this.earliestMessageId = null
@@ -280,7 +313,7 @@ export default {
this.isLoadingHistory = false this.isLoadingHistory = false
this.hasMoreHistory = false this.hasMoreHistory = false
this.initFailed = false this.initFailed = false
// 强制垃圾回收(如果浏览器支持) // 强制垃圾回收(如果浏览器支持)
if (window.gc) { if (window.gc) {
try { try {
@@ -299,25 +332,25 @@ export default {
if (this.isLoadingHistory || this.isDestroyed) { if (this.isLoadingHistory || this.isDestroyed) {
return return
} }
try { try {
this.isLoadingHistory = true this.isLoadingHistory = true
this.retryCount = 0 this.retryCount = 0
// 添加到请求队列 // 添加到请求队列
const requestId = Date.now() const requestId = Date.now()
this.requestQueue.push(requestId) this.requestQueue.push(requestId)
// 如果队列中有太多请求,清理旧请求 // 如果队列中有太多请求,清理旧请求
if (this.requestQueue.length > 3) { if (this.requestQueue.length > 3) {
this.requestQueue = this.requestQueue.slice(-3) this.requestQueue = this.requestQueue.slice(-3)
} }
// 添加超时控制,避免卡死 // 添加超时控制,避免卡死
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 8000) // 减少到8秒 setTimeout(() => reject(new Error('请求超时')), 8000) // 减少到8秒
}) })
// 创建取消Promise // 创建取消Promise
const cancelPromise = new Promise((_, reject) => { const cancelPromise = new Promise((_, reject) => {
const checkCancel = () => { const checkCancel = () => {
@@ -338,22 +371,24 @@ export default {
// 使用Promise.race来实现超时和取消控制 // 使用Promise.race来实现超时和取消控制
const res = await Promise.race([historyPromise, timeoutPromise, cancelPromise]) const res = await Promise.race([historyPromise, timeoutPromise, cancelPromise])
// 从队列中移除当前请求 // 从队列中移除当前请求
this.requestQueue = this.requestQueue.filter(id => id !== requestId) this.requestQueue = this.requestQueue.filter(id => id !== requestId)
// console.log('历史记录响应:', res);
if (this.isDestroyed) { if (this.isDestroyed) {
return return
} }
if (res.code === 200 && res.data && Array.isArray(res.data.data)) { if (res.code === 200 && res.data && Array.isArray(res.data.data)) {
const newMessages = [] const newMessages = []
// console.log('历史记录:', res.data.data)
// 处理历史消息 // 批量处理历史消息减少DOM操作
const messagesBatch = []
res.data.data.forEach(msg => { res.data.data.forEach(msg => {
// 用户消息 // 用户消息
if (msg.query) { if (msg.query) {
newMessages.push({ messagesBatch.push({
sender: 'user', sender: 'user',
avatar: require('@/assets/ai/yonghu.png'), avatar: require('@/assets/ai/yonghu.png'),
content: msg.query, content: msg.query,
@@ -363,9 +398,10 @@ export default {
}) })
} }
// AI消息 // AI消息 - 延迟计算groupedReferences
if (msg.answer) { if (msg.answer) {
newMessages.push({ const retrieverResources = msg.retriever_resources || []
const aiMessage = {
sender: 'ai', sender: 'ai',
avatar: require('@/assets/ai/AI.png'), avatar: require('@/assets/ai/AI.png'),
content: msg.answer, content: msg.answer,
@@ -373,18 +409,32 @@ export default {
conversationId: msg.conversation_id, conversationId: msg.conversation_id,
created_at: msg.created_at, created_at: msg.created_at,
feedback: msg.feedback || null, feedback: msg.feedback || null,
retrieverResources: msg.retriever_resources || [], retrieverResources: retrieverResources,
streamCompleted: true // 历史消息的流输出已完成 streamCompleted: true // 历史消息的流输出已完成
}) }
// 只有在有引用资源时才预计算分组
if (retrieverResources && retrieverResources.length > 0) {
// 使用异步方式计算,避免阻塞主线程
this.$nextTick(() => {
aiMessage.groupedReferences = this.getGroupedReferences(retrieverResources)
})
}
messagesBatch.push(aiMessage)
} }
}) })
// 按时间排序(从旧到新) // 按时间排序(从旧到新)
newMessages.sort((a, b) => { messagesBatch.sort((a, b) => {
return a.created_at - b.created_at return a.created_at - b.created_at
}) })
this.messages = newMessages newMessages.push(...messagesBatch)
// console.log('处理后的历史记录:', newMessages)
// 分批渲染消息,避免一次性渲染过多消息导致卡死
this.renderMessagesInBatches(newMessages)
if (newMessages.length > 0) { if (newMessages.length > 0) {
this.conversation_id = newMessages[0].conversationId this.conversation_id = newMessages[0].conversationId
@@ -398,28 +448,30 @@ export default {
this.earliestMessageId = newMessages[0].messageId.replace('ai-', '') this.earliestMessageId = newMessages[0].messageId.replace('ai-', '')
} }
} }
// console.log('历史记录:', newMessages)
this.hasMoreHistory = res.data.has_more || false this.hasMoreHistory = res.data.has_more || false
} else { } else {
// console.log('没有历史记录')
// 没有历史记录,显示欢迎消息 // 没有历史记录,显示欢迎消息
this.messages = [{ const welcomeMessages = [{
sender: 'ai', sender: 'ai',
avatar: require('@/assets/ai/AI.png'), avatar: require('@/assets/ai/AI.png'),
content: '你好我是智水AI辅导员有什么可以帮助你的吗', content: '你好我是智水AI辅导员有什么可以帮助你的吗',
messageId: 'welcome-' + Date.now() messageId: 'welcome-' + Date.now()
}] }]
this.renderMessagesInBatches(welcomeMessages)
this.hasMoreHistory = false this.hasMoreHistory = false
} }
this.initFailed = false this.initFailed = false
} catch (error) { } catch (error) {
console.error('初始化聊天失败:', error) console.error('初始化聊天失败:', error)
// 如果是取消错误,直接返回 // 如果是取消错误,直接返回
if (error.message === '请求已取消' || this.isDestroyed) { if (error.message === '请求已取消' || this.isDestroyed) {
return return
} }
// 根据错误类型显示不同的提示信息 // 根据错误类型显示不同的提示信息
let errorMessage = '加载历史记录失败' let errorMessage = '加载历史记录失败'
if (error.message === '请求超时') { if (error.message === '请求超时') {
@@ -431,17 +483,18 @@ export default {
} else if (error.response && error.response.status >= 500) { } else if (error.response && error.response.status >= 500) {
errorMessage = '服务器暂时不可用,请稍后重试' errorMessage = '服务器暂时不可用,请稍后重试'
} }
this.showToast(errorMessage) this.showToast(errorMessage)
// 显示欢迎消息作为降级方案 // 显示欢迎消息作为降级方案
this.messages = [{ const fallbackMessages = [{
sender: 'ai', sender: 'ai',
avatar: require('@/assets/ai/AI.png'), avatar: require('@/assets/ai/AI.png'),
content: '你好我是智水AI辅导员有什么可以帮助你的吗\n\n如果遇到网络问题请稍后重试或联系管理员。', content: '你好我是智水AI辅导员有什么可以帮助你的吗\n\n如果遇到网络问题请稍后重试或联系管理员。',
messageId: 'welcome-' + Date.now() messageId: 'welcome-' + Date.now()
}] }]
this.renderMessagesInBatches(fallbackMessages)
// 重置相关状态 // 重置相关状态
this.hasMoreHistory = false this.hasMoreHistory = false
this.conversation_id = '' this.conversation_id = ''
@@ -449,10 +502,10 @@ export default {
this.initFailed = true // 标记初始化失败 this.initFailed = true // 标记初始化失败
} finally { } finally {
this.isLoadingHistory = false this.isLoadingHistory = false
// 清理请求队列 // 清理请求队列
this.requestQueue = [] this.requestQueue = []
// 滚动到底部 // 滚动到底部
if (!this.isDestroyed) { if (!this.isDestroyed) {
this.$nextTick(() => { this.$nextTick(() => {
@@ -516,6 +569,7 @@ export default {
// AI消息 // AI消息
if (msg.answer) { if (msg.answer) {
const retrieverResources = msg.retriever_resources || []
newMessages.push({ newMessages.push({
sender: 'ai', sender: 'ai',
avatar: require('@/assets/ai/AI.png'), avatar: require('@/assets/ai/AI.png'),
@@ -524,7 +578,8 @@ export default {
conversationId: msg.conversation_id, conversationId: msg.conversation_id,
created_at: msg.created_at, created_at: msg.created_at,
feedback: msg.feedback || null, feedback: msg.feedback || null,
retrieverResources: msg.retriever_resources || [] retrieverResources: retrieverResources,
groupedReferences: this.getGroupedReferences(retrieverResources) // 预计算分组引用
}) })
} }
}) })
@@ -578,7 +633,7 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('加载历史记录失败:', error) console.error('加载历史记录失败:', error)
// 根据错误类型显示不同的提示信息 // 根据错误类型显示不同的提示信息
let errorMessage = '加载历史记录失败' let errorMessage = '加载历史记录失败'
if (error.message === '加载历史记录超时') { if (error.message === '加载历史记录超时') {
@@ -588,9 +643,9 @@ export default {
} else if (error.response && error.response.status >= 500) { } else if (error.response && error.response.status >= 500) {
errorMessage = '服务器繁忙,请稍后重试' errorMessage = '服务器繁忙,请稍后重试'
} }
this.showToast(errorMessage) this.showToast(errorMessage)
// 如果是超时或网络错误不改变hasMoreHistory状态允许用户重试 // 如果是超时或网络错误不改变hasMoreHistory状态允许用户重试
if (!error.message || (!error.message.includes('超时') && !error.message.includes('Network'))) { if (!error.message || (!error.message.includes('超时') && !error.message.includes('Network'))) {
this.hasMoreHistory = false this.hasMoreHistory = false
@@ -608,20 +663,20 @@ export default {
if (this.isLoadingHistory || this.isDestroyed) { if (this.isLoadingHistory || this.isDestroyed) {
return return
} }
// 检查重试次数 // 检查重试次数
if (this.retryCount >= this.maxRetries) { if (this.retryCount >= this.maxRetries) {
this.showToast('重试次数过多,请刷新页面') this.showToast('重试次数过多,请刷新页面')
return return
} }
this.initFailed = false this.initFailed = false
this.retryCount++ this.retryCount++
// 清理之前的状态 // 清理之前的状态
this.messages = [] this.messages = []
this.requestQueue = [] this.requestQueue = []
try { try {
await this.initChat() await this.initChat()
} catch (error) { } catch (error) {
@@ -637,7 +692,7 @@ export default {
if (!this.inputMessage.trim() || this.sending || this.isDestroyed) { if (!this.inputMessage.trim() || this.sending || this.isDestroyed) {
return return
} }
// 防止频繁发送 // 防止频繁发送
if (this.requestQueue.length > 2) { if (this.requestQueue.length > 2) {
this.showToast('请等待当前消息处理完成') this.showToast('请等待当前消息处理完成')
@@ -647,7 +702,7 @@ export default {
const userMessage = this.inputMessage.trim() const userMessage = this.inputMessage.trim()
this.inputMessage = '' this.inputMessage = ''
this.sending = true this.sending = true
// 添加到请求队列 // 添加到请求队列
const requestId = Date.now() const requestId = Date.now()
this.requestQueue.push(requestId) this.requestQueue.push(requestId)
@@ -686,7 +741,7 @@ export default {
}) })
this.currentCancel = cancel this.currentCancel = cancel
// 注册请求到监控系统 // 注册请求到监控系统
this.activeRequests.set(requestId, { this.activeRequests.set(requestId, {
startTime: Date.now(), startTime: Date.now(),
@@ -694,7 +749,7 @@ export default {
cancel: cancel, cancel: cancel,
userMessage: userMessage userMessage: userMessage
}) })
// 添加额外的超时保护 // 添加额外的超时保护
const streamTimeout = setTimeout(() => { const streamTimeout = setTimeout(() => {
if (cancel) { if (cancel) {
@@ -704,10 +759,10 @@ export default {
aiMsg.streamCompleted = true aiMsg.streamCompleted = true
this.sending = false this.sending = false
}, 45000) // 45秒超时保护 }, 45000) // 45秒超时保护
const response = await stream const response = await stream
clearTimeout(streamTimeout) // 清除超时保护 clearTimeout(streamTimeout) // 清除超时保护
const { reader, decoder } = response const { reader, decoder } = response
let buffer = '' let buffer = ''
let lastUpdateTime = Date.now() let lastUpdateTime = Date.now()
@@ -725,17 +780,17 @@ export default {
aiMsg.content = aiMsg.content === '正在思考...' ? '服务器响应中断,请重新发送' : aiMsg.content + '\n\n[响应中断]' aiMsg.content = aiMsg.content === '正在思考...' ? '服务器响应中断,请重新发送' : aiMsg.content + '\n\n[响应中断]'
aiMsg.streamCompleted = true aiMsg.streamCompleted = true
}, 15000) // 15秒无数据则认为中断 }, 15000) // 15秒无数据则认为中断
const { done, value } = await reader.read() const { done, value } = await reader.read()
// 清除无数据超时 // 清除无数据超时
if (noDataTimeout) { if (noDataTimeout) {
clearTimeout(noDataTimeout) clearTimeout(noDataTimeout)
noDataTimeout = null noDataTimeout = null
} }
if (done) break if (done) break
// 更新最后接收数据时间 // 更新最后接收数据时间
lastUpdateTime = Date.now() lastUpdateTime = Date.now()
@@ -772,11 +827,12 @@ export default {
} }
if (data.retriever_resources) { if (data.retriever_resources) {
aiMsg.retrieverResources = data.retriever_resources aiMsg.retrieverResources = data.retriever_resources
aiMsg.groupedReferences = this.getGroupedReferences(data.retriever_resources) // 预计算分组引用
} }
// 标记流输出完成 // 标记流输出完成
aiMsg.streamCompleted = true aiMsg.streamCompleted = true
} }
// 处理错误事件 // 处理错误事件
if (data.event === 'error') { if (data.event === 'error') {
aiMsg.content = data.message || '服务器处理出错,请重新发送' aiMsg.content = data.message || '服务器处理出错,请重新发送'
@@ -790,11 +846,11 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('发送消息失败:', error) console.error('发送消息失败:', error)
// 清理所有定时器 // 清理所有定时器
if (streamTimeout) clearTimeout(streamTimeout) if (streamTimeout) clearTimeout(streamTimeout)
if (noDataTimeout) clearTimeout(noDataTimeout) if (noDataTimeout) clearTimeout(noDataTimeout)
// 根据错误类型显示不同的错误信息 // 根据错误类型显示不同的错误信息
let errorMessage = '发送消息失败' let errorMessage = '发送消息失败'
if (error.message.includes('超时')) { if (error.message.includes('超时')) {
@@ -812,27 +868,70 @@ export default {
} else { } else {
aiMsg.content = '抱歉,发送消息时出现错误,请稍后重试' aiMsg.content = '抱歉,发送消息时出现错误,请稍后重试'
} }
aiMsg.streamCompleted = true // 即使出错也标记为完成,显示操作区域 aiMsg.streamCompleted = true // 即使出错也标记为完成,显示操作区域
this.showToast(errorMessage, 'error') this.showToast(errorMessage, 'error')
} finally { } finally {
this.sending = false this.sending = false
this.currentCancel = null this.currentCancel = null
// 清理所有定时器 // 清理所有定时器
if (streamTimeout) clearTimeout(streamTimeout) if (streamTimeout) clearTimeout(streamTimeout)
if (noDataTimeout) clearTimeout(noDataTimeout) if (noDataTimeout) clearTimeout(noDataTimeout)
// 从请求队列中移除当前请求 // 从请求队列中移除当前请求
this.requestQueue = this.requestQueue.filter(id => id !== requestId) this.requestQueue = this.requestQueue.filter(id => id !== requestId)
// 从监控系统中移除请求 // 从监控系统中移除请求
this.activeRequests.delete(requestId) this.activeRequests.delete(requestId)
this.scrollToBottom(true) // 发送完成后使用平滑滚动 this.scrollToBottom(true) // 发送完成后使用平滑滚动
} }
}, },
/**
* 分批渲染消息,避免一次性渲染过多消息导致卡死
*/
renderMessagesInBatches(newMessages) {
if (!newMessages || newMessages.length === 0) {
this.messages = []
return
}
// 如果消息数量较少,直接渲染
if (newMessages.length <= this.messageRenderBatch) {
this.messages = newMessages
return
}
// 分批渲染
this.messages = []
let currentIndex = 0
const renderNextBatch = () => {
if (this.isDestroyed || currentIndex >= newMessages.length) {
return
}
const endIndex = Math.min(currentIndex + this.messageRenderBatch, newMessages.length)
const batch = newMessages.slice(currentIndex, endIndex)
// 添加当前批次的消息
this.messages.push(...batch)
currentIndex = endIndex
// 如果还有更多消息,继续渲染下一批
if (currentIndex < newMessages.length) {
this.$nextTick(() => {
setTimeout(renderNextBatch, 10) // 10ms延迟让浏览器有时间处理当前批次
})
}
}
// 开始渲染第一批
renderNextBatch()
},
/** /**
* 滚动到底部 * 滚动到底部
*/ */
@@ -911,12 +1010,30 @@ export default {
}, },
/** /**
* 渲染Markdown内容 * 渲染Markdown内容(带缓存优化)
*/ */
renderMarkdown(text) { renderMarkdown(text) {
if (!text) return '' if (!text) return ''
// 检查缓存
if (this.markdownCache.has(text)) {
return this.markdownCache.get(text)
}
// 渲染markdown
const html = this.md.render(text) const html = this.md.render(text)
return DOMPurify.sanitize(html) const sanitizedHtml = DOMPurify.sanitize(html)
// 缓存管理:如果缓存超过最大大小,删除最旧的条目
if (this.markdownCache.size >= this.maxCacheSize) {
const firstKey = this.markdownCache.keys().next().value
this.markdownCache.delete(firstKey)
}
// 添加到缓存
this.markdownCache.set(text, sanitizedHtml)
return sanitizedHtml
}, },
/** /**
@@ -1000,13 +1117,25 @@ export default {
}, },
/** /**
* 获取分组的引用来源 * 获取分组的引用来源(带缓存优化)
*/ */
getGroupedReferences(resources) { getGroupedReferences(resources) {
if (!resources || !Array.isArray(resources)) { if (!resources || !Array.isArray(resources)) {
return [] return []
} }
// 生成缓存键
const cacheKey = JSON.stringify(resources.map(r => ({
document_name: r.document_name,
id: r.id || r.chunk_id
})))
// 检查缓存
if (this.referencesCache.has(cacheKey)) {
return this.referencesCache.get(cacheKey)
}
// 计算分组
const grouped = {} const grouped = {}
resources.forEach(resource => { resources.forEach(resource => {
const docName = resource.document_name || '未知文档' const docName = resource.document_name || '未知文档'
@@ -1016,10 +1145,20 @@ export default {
grouped[docName].push(resource) grouped[docName].push(resource)
}) })
return Object.entries(grouped).map(([docName, items]) => ({ const result = Object.entries(grouped).map(([docName, items]) => ({
docName, docName,
items items
})) }))
// 缓存结果
if (this.referencesCache.size >= this.maxReferencesCacheSize) {
// 删除最旧的缓存条目
const firstKey = this.referencesCache.keys().next().value
this.referencesCache.delete(firstKey)
}
this.referencesCache.set(cacheKey, result)
return result
}, },
/** /**
@@ -1075,12 +1214,12 @@ export default {
this.memoryCheckInterval = setInterval(() => { this.memoryCheckInterval = setInterval(() => {
if (performance.memory) { if (performance.memory) {
const memoryUsage = performance.memory.usedJSHeapSize / 1024 / 1024 // MB const memoryUsage = performance.memory.usedJSHeapSize / 1024 / 1024 // MB
// 如果内存使用超过100MB进行清理 // 如果内存使用超过100MB进行清理
if (memoryUsage > 100) { if (memoryUsage > 100) {
this.performMemoryCleanup() this.performMemoryCleanup()
} }
this.lastMemoryUsage = memoryUsage this.lastMemoryUsage = memoryUsage
} }
}, 30000) // 每30秒检查一次 }, 30000) // 每30秒检查一次
@@ -1088,7 +1227,7 @@ export default {
// 监控DOM节点数量 // 监控DOM节点数量
this.performanceMonitor = setInterval(() => { this.performanceMonitor = setInterval(() => {
const messageCount = this.messages.length const messageCount = this.messages.length
// 如果消息数量过多,清理旧消息 // 如果消息数量过多,清理旧消息
if (messageCount > 200) { if (messageCount > 200) {
this.cleanupOldMessages() this.cleanupOldMessages()
@@ -1104,7 +1243,7 @@ export default {
clearInterval(this.memoryCheckInterval) clearInterval(this.memoryCheckInterval)
this.memoryCheckInterval = null this.memoryCheckInterval = null
} }
if (this.performanceMonitor) { if (this.performanceMonitor) {
clearInterval(this.performanceMonitor) clearInterval(this.performanceMonitor)
this.performanceMonitor = null this.performanceMonitor = null
@@ -1118,7 +1257,7 @@ export default {
if (this.requestMonitorInterval) { if (this.requestMonitorInterval) {
clearInterval(this.requestMonitorInterval) clearInterval(this.requestMonitorInterval)
} }
this.requestMonitorInterval = setInterval(() => { this.requestMonitorInterval = setInterval(() => {
this.checkActiveRequests() this.checkActiveRequests()
}, 5000) // 每5秒检查一次 }, 5000) // 每5秒检查一次
@@ -1132,14 +1271,14 @@ export default {
clearInterval(this.requestMonitorInterval) clearInterval(this.requestMonitorInterval)
this.requestMonitorInterval = null this.requestMonitorInterval = null
} }
// 取消所有活跃请求 // 取消所有活跃请求
this.activeRequests.forEach((request, requestId) => { this.activeRequests.forEach((request, requestId) => {
if (request.cancel) { if (request.cancel) {
request.cancel('组件销毁') request.cancel('组件销毁')
} }
}) })
this.activeRequests.clear() this.activeRequests.clear()
}, },
@@ -1149,26 +1288,26 @@ export default {
checkActiveRequests() { checkActiveRequests() {
const now = Date.now() const now = Date.now()
const expiredRequests = [] const expiredRequests = []
this.activeRequests.forEach((request, requestId) => { this.activeRequests.forEach((request, requestId) => {
const duration = now - request.startTime const duration = now - request.startTime
// 如果请求超过最大持续时间,标记为过期 // 如果请求超过最大持续时间,标记为过期
if (duration > this.maxRequestDuration) { if (duration > this.maxRequestDuration) {
expiredRequests.push({ requestId, request }) expiredRequests.push({ requestId, request })
} }
}) })
// 取消过期请求 // 取消过期请求
expiredRequests.forEach(({ requestId, request }) => { expiredRequests.forEach(({ requestId, request }) => {
console.warn(`强制取消超时请求: ${request.type}, 持续时间: ${(Date.now() - request.startTime) / 1000}`) console.warn(`强制取消超时请求: ${request.type}, 持续时间: ${(Date.now() - request.startTime) / 1000}`)
if (request.cancel) { if (request.cancel) {
request.cancel('请求超时被强制取消') request.cancel('请求超时被强制取消')
} }
this.activeRequests.delete(requestId) this.activeRequests.delete(requestId)
// 显示超时提示 // 显示超时提示
this.showToast('请求超时已自动取消,请重试', 'warning') this.showToast('请求超时已自动取消,请重试', 'warning')
}) })
@@ -1181,12 +1320,12 @@ export default {
try { try {
// 清理引用展示状态 // 清理引用展示状态
this.showSingleReference = {} this.showSingleReference = {}
// 强制垃圾回收(如果浏览器支持) // 强制垃圾回收(如果浏览器支持)
if (window.gc) { if (window.gc) {
window.gc() window.gc()
} }
console.log('执行内存清理') console.log('执行内存清理')
} catch (error) { } catch (error) {
console.warn('内存清理失败:', error) console.warn('内存清理失败:', error)
@@ -1202,15 +1341,15 @@ export default {
if (this.messages.length > 100) { if (this.messages.length > 100) {
const keepCount = 100 const keepCount = 100
const removedCount = this.messages.length - keepCount const removedCount = this.messages.length - keepCount
this.messages = this.messages.slice(-keepCount) this.messages = this.messages.slice(-keepCount)
// 更新最早消息ID // 更新最早消息ID
const userMessages = this.messages.filter(msg => msg.sender === 'user') const userMessages = this.messages.filter(msg => msg.sender === 'user')
if (userMessages.length > 0) { if (userMessages.length > 0) {
this.earliestMessageId = userMessages[0].messageId this.earliestMessageId = userMessages[0].messageId
} }
console.log(`清理了 ${removedCount} 条旧消息`) console.log(`清理了 ${removedCount} 条旧消息`)
} }
} catch (error) { } catch (error) {
@@ -1224,10 +1363,10 @@ export default {
setupErrorHandling() { setupErrorHandling() {
// 捕获未处理的Promise错误 // 捕获未处理的Promise错误
window.addEventListener('unhandledrejection', this.handleUnhandledRejection) window.addEventListener('unhandledrejection', this.handleUnhandledRejection)
// 捕获全局JavaScript错误 // 捕获全局JavaScript错误
window.addEventListener('error', this.handleGlobalError) window.addEventListener('error', this.handleGlobalError)
// Vue错误处理 // Vue错误处理
this.$options.errorCaptured = this.handleVueError this.$options.errorCaptured = this.handleVueError
}, },
@@ -1237,10 +1376,10 @@ export default {
*/ */
handleUnhandledRejection(event) { handleUnhandledRejection(event) {
console.error('未处理的Promise错误:', event.reason) console.error('未处理的Promise错误:', event.reason)
// 防止错误冒泡导致页面崩溃 // 防止错误冒泡导致页面崩溃
event.preventDefault() event.preventDefault()
// 如果是网络错误,显示友好提示 // 如果是网络错误,显示友好提示
if (event.reason && (event.reason.message || '').includes('Network')) { if (event.reason && (event.reason.message || '').includes('Network')) {
this.showToast('网络连接异常,请检查网络设置', 'error') this.showToast('网络连接异常,请检查网络设置', 'error')
@@ -1254,13 +1393,13 @@ export default {
*/ */
handleGlobalError(event) { handleGlobalError(event) {
console.error('全局JavaScript错误:', event.error) console.error('全局JavaScript错误:', event.error)
// 防止错误导致页面白屏 // 防止错误导致页面白屏
event.preventDefault() event.preventDefault()
// 如果是内存相关错误,执行清理 // 如果是内存相关错误,执行清理
if (event.error && event.error.message && if (event.error && event.error.message &&
(event.error.message.includes('memory') || event.error.message.includes('Maximum call stack'))) { (event.error.message.includes('memory') || event.error.message.includes('Maximum call stack'))) {
this.performMemoryCleanup() this.performMemoryCleanup()
this.showToast('系统资源不足,已自动清理', 'warning') this.showToast('系统资源不足,已自动清理', 'warning')
} }
@@ -1271,14 +1410,14 @@ export default {
*/ */
handleVueError(err, instance, info) { handleVueError(err, instance, info) {
console.error('Vue组件错误:', err, info) console.error('Vue组件错误:', err, info)
// 如果是渲染错误,尝试重置状态 // 如果是渲染错误,尝试重置状态
if (info && info.includes('render')) { if (info && info.includes('render')) {
this.$nextTick(() => { this.$nextTick(() => {
this.$forceUpdate() this.$forceUpdate()
}) })
} }
return false // 阻止错误继续传播 return false // 阻止错误继续传播
} }
} }
@@ -1972,6 +2111,28 @@ export default {
color: #722ed1 !important; color: #722ed1 !important;
} }
/* 消息限制提示样式 */
.message-limit-notice {
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 8px;
padding: 12px 16px;
margin: 16px 0;
text-align: center;
}
.notice-text {
color: #586069;
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
.notice-subtext {
color: #959da5;
font-size: 12px;
}
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.chat-popup { .chat-popup {

View File

@@ -5,7 +5,8 @@ import { showToast } from "@/utils/toast"; // 请替换为你的Toast组件
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8088', // baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8088',
baseURL: process.env.VUE_APP_BASE_API,
timeout: 15000, timeout: 15000,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",