7.7 KiB
toolName: view_files
status: success
filePath: d:\Users\19211\Desktop\demo\QAModule.vue
我来查看一下 aiAPI.sendMessage 的实现,这很可能是问题所在。
toolName: file_search
status: success
file_pattern: **/ai.js
toolName: file_search
status: success
file_pattern: /api//*.{js,ts}
toolName: search_by_regex
status: success
query: aiAPI
toolName: search_by_regex
status: success
query: sendMessage
toolName: search_by_regex
status: success
query: import.*aiAPI
toolName: file_search
status: success
file_pattern: /api/
toolName: search_by_regex
status: success
query: export.*sendMessage
toolName: view_folder
status: success
d:\Users\19211\Desktop\demo
toolName: file_search
status: success
file_pattern: **/*.vue
toolName: search_by_regex
status: success
query: @/api
toolName: file_search
status: success
file_pattern: **/*.js
toolName: file_search
status: success
file_pattern: **/*.ts
toolName: view_files
status: success
filePath: d:\Users\19211\Desktop\demo\前端示例.md
我发现问题了。您的 QAModule.vue 代码本身有打字机效果的实现逻辑,但问题很可能是 aiAPI.sendMessage 没有真正实现流式读取。
让我查看一下后端 FileService 的 streamFromRagService 方法,了解它返回的数据格式:
toolName: search_by_regex
status: success
query: streamFromRagService
toolName: search_by_regex
status: success
query: streamFromRagService
toolName: search_by_regex
status: success
query: streamFromRagService
toolName: view_files
status: success
filePath: d:\Users\19211\Desktop\demo\src\main\java\top\tqx\demo_1\service\impl\FileServiceImpl.java
我找到问题根源了!您的代码中 @/api/ai 文件不存在,所以我无法看到 aiAPI.sendMessage 的具体实现。但根据代码分析,问题很可能出在 aiAPI.sendMessage 没有真正实现流式读取。
问题诊断
后端 streamFromRagService 是真正的 SSE 流式输出(第521行 emitter.send(event)),问题在于前端 API 层没有正确解析 SSE 流。
您需要创建一个 @/api/ai.js 文件来实现正确的流式读取。以下是修复后的代码:
// src/api/ai.js 或 @/api/ai.js
import request from '@/utils/request'
export default {
sendMessage(params, onStart, onMessage, onSource, onReference, onError, onComplete) {
const controller = new AbortController()
// 1. 先发送请求
fetch('/ai/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify(params),
signal: controller.signal
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok')
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
// 2. 开始读取流
function read() {
reader.read().then(({ done, value }) => {
if (done) {
// 处理缓冲区中剩余的数据
if (buffer.trim()) {
try {
const data = JSON.parse(buffer.trim())
if (data.type === 'content' && onMessage) {
onMessage(data.content)
}
} catch (e) {}
}
if (onComplete) onComplete({})
return
}
// 解码并添加到缓冲区
buffer += decoder.decode(value, { stream: true })
// 处理每一行(处理不完整的数据行)
const lines = buffer.split('\n')
buffer = lines.pop() // 保留最后未完成的一行
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6).trim()
if (!dataStr || dataStr === '[DONE]') continue
try {
const data = JSON.parse(dataStr)
switch (data.type) {
case 'content':
// 关键:每个字符块都立即调用回调!
if (onMessage) onMessage(data.content)
break
case 'source':
case 'sources':
if (onSource) onSource(data)
break
case 'reference':
if (onReference) onReference(data)
break
case 'session_id':
if (onComplete) onComplete({ sessionId: data.session_id })
break
case 'error':
if (onError) onError(data.message)
break
case 'finish':
case 'result':
// 收集最终结果
if (onComplete) onComplete({
answer: data.answer || '',
sources: data.sources || []
})
break
default:
// 其他事件类型也处理
if (data.answer && onMessage) {
// 有些服务把完整答案放在 answer 字段
onMessage(data.answer)
}
}
} catch (e) {
// JSON 解析失败,忽略该行
}
}
}
// 继续读取
read()
})
}
read()
})
.catch(error => {
if (error.name === 'AbortError') {
// 请求被取消
} else if (onError) {
onError(error.message)
}
})
return controller
},
cancelRequest(controller) {
if (controller) {
controller.abort()
}
},
getSessions(params) {
return request.get('/ai/sessions', { params })
},
getSessionDetail(sessionId, params) {
return request.get(`/ai/session/${sessionId}`, { params })
},
deleteSession(sessionId) {
return request.delete(`/ai/session/${sessionId}`)
}
}
关键点说明
1. 为什么"一下子输出出来"?
问题在于 aiAPI.sendMessage 可能没有按 chunk 逐字调用 onMessage,而是:
- 要么收集完所有数据后一次性传给前端
- 要么每次收到多个字符才调用一次
onMessage
2. 修复方法
确保 onMessage 回调是每个字符块都被调用,而不是批量处理:
// ✅ 正确:每个chunk立即传递
case 'content':
if (onMessage) onMessage(data.content) // 可能是1个字符,也可能是多个
break
// ❌ 错误:收集后一次性传递
case 'content':
buffer += data.content
if (buffer.length >= 10) { // 等积累到一定量才发送
onMessage(buffer)
buffer = ''
}
break
3. 打字机效果的问题
即使 onMessage 被正确调用,您代码中的打字机效果 startTypingEffect 每次只添加 1-3 个字符(第864行),这意味着如果 onMessage 一次收到超过 3 个字符,就会出现跳跃感。
建议检查 onMessage 回调是否真的被逐字调用:
// 在 QAModule.vue 的 handleSend 中添加调试:
onMessage: (contentChunk) => {
currentAIMessage.value += contentChunk
console.log(`📥 chunk长度: ${contentChunk.length}, 内容: "${contentChunk}"`)
// ...
}
如果调试日志显示 chunk长度 经常大于 3,那打字效果就会显得不流畅。
您需要确认 @/api/ai.js 文件的存在和实现,或者如果有其他 API 文件路径,请告诉我它的位置。