前端项目初始化提交
This commit is contained in:
305
前端示例.md
Normal file
305
前端示例.md
Normal file
@@ -0,0 +1,305 @@
|
||||
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` 文件来实现正确的流式读取。以下是修复后的代码:
|
||||
|
||||
```javascript
|
||||
// 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`** **回调是每个字符块都被调用**,而不是批量处理:
|
||||
|
||||
```javascript
|
||||
// ✅ 正确:每个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` 回调是否真的被**逐字调用**:
|
||||
|
||||
```javascript
|
||||
// 在 QAModule.vue 的 handleSend 中添加调试:
|
||||
onMessage: (contentChunk) => {
|
||||
currentAIMessage.value += contentChunk
|
||||
console.log(`📥 chunk长度: ${contentChunk.length}, 内容: "${contentChunk}"`)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
如果调试日志显示 `chunk长度` 经常大于 3,那打字效果就会显得不流畅。
|
||||
|
||||
您需要确认 `@/api/ai.js` 文件的存在和实现,或者如果有其他 API 文件路径,请告诉我它的位置。
|
||||
Reference in New Issue
Block a user