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