update at 2025-10-15 15:07:45
This commit is contained in:
175
STOP_GENERATION_FIX.md
Normal file
175
STOP_GENERATION_FIX.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 停止生成功能修复文档
|
||||
|
||||
## 问题描述
|
||||
1. **按钮点击无效**:确认/停止按钮点击没有响应
|
||||
2. **停止逻辑不生效**:即使调用了 `stopGeneration()`,流式输出仍在继续
|
||||
|
||||
## 参考实现
|
||||
参考了 Cherry Studio 中的 **PAUSED** 状态设计理念。
|
||||
|
||||
## 修复内容
|
||||
|
||||
### 1. 修复按钮点击事件绑定
|
||||
**问题**:原代码使用三元表达式直接绑定函数引用
|
||||
```vue
|
||||
@click="store.state.isSending ? handleStopGeneration : handleSendMessage"
|
||||
```
|
||||
|
||||
**修复**:改为调用统一的处理函数
|
||||
```vue
|
||||
@click="handleButtonClick"
|
||||
```
|
||||
|
||||
```typescript
|
||||
const handleButtonClick = () => {
|
||||
if (store.state.isSending) {
|
||||
handleStopGeneration()
|
||||
} else {
|
||||
handleSendMessage()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 添加 PAUSED 消息状态
|
||||
**文件**:`web/src/types/chat.ts`
|
||||
|
||||
```typescript
|
||||
// 添加 'paused' 状态
|
||||
export type MessageStatus = 'pending' | 'sending' | 'success' | 'error' | 'paused'
|
||||
|
||||
// 添加 'paused' 事件类型
|
||||
export interface StreamEvent {
|
||||
type: 'start' | 'delta' | 'end' | 'error' | 'paused'
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 优化错误处理逻辑
|
||||
**文件**:`web/src/services/chatService.ts`
|
||||
|
||||
```typescript
|
||||
catch (error) {
|
||||
const isAborted = error instanceof Error && error.name === 'AbortError'
|
||||
|
||||
if (isAborted) {
|
||||
// 用户主动停止,保留已生成的内容
|
||||
assistantMessage.status = 'paused'
|
||||
assistantMessage.error = undefined
|
||||
onChunk({ type: 'paused', messageId: assistantMessage.id })
|
||||
} else {
|
||||
// 其他错误
|
||||
assistantMessage.status = 'error'
|
||||
assistantMessage.error = error instanceof Error ? error.message : '发送失败'
|
||||
onChunk({ type: 'error', error: assistantMessage.error, messageId: assistantMessage.id })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 在流式读取中检查中止信号
|
||||
**文件**:`web/src/services/modelServiceManager.ts`
|
||||
|
||||
```typescript
|
||||
while (true) {
|
||||
// 检查是否被中止
|
||||
if (signal?.aborted) {
|
||||
console.log('🛑 [makeChatRequestStream] 检测到中止信号,停止读取流')
|
||||
reader.cancel()
|
||||
throw new DOMException('用户中止操作', 'AbortError')
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
// ... 处理数据
|
||||
}
|
||||
```
|
||||
|
||||
### 5. UI 显示优化
|
||||
**文件**:`web/src/components/Chat/ChatLayout.vue`
|
||||
|
||||
```vue
|
||||
<!-- 显示暂停状态 -->
|
||||
<n-tag v-else-if="msg.status === 'paused'" type="warning" size="small">
|
||||
已停止
|
||||
</n-tag>
|
||||
|
||||
<!-- 允许 paused 状态的消息显示操作按钮 -->
|
||||
<div v-if="msg.role === 'assistant' && (msg.status === 'success' || msg.status === 'paused')"
|
||||
class="message-actions">
|
||||
<!-- 复制、重新生成、删除按钮 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 用户点击停止按钮时:
|
||||
1. `handleButtonClick()` 检测到 `isSending = true`
|
||||
2. 调用 `handleStopGeneration()`
|
||||
3. `store.stopGeneration()` 执行 `abortController.abort()`
|
||||
4. 中止信号传递到 `chatService.sendMessageStream()`
|
||||
5. 信号继续传递到 `modelServiceManager.makeChatRequestStream()`
|
||||
6. 流式读取循环检测到 `signal.aborted`
|
||||
7. 调用 `reader.cancel()` 并抛出 `AbortError`
|
||||
8. 错误向上冒泡,在 `chatService` 中被识别为用户中止
|
||||
9. 消息状态设置为 `'paused'`,保留已生成内容
|
||||
10. UI 更新显示"已停止"标签
|
||||
|
||||
### 正常完成时:
|
||||
1. 流式读取完成,消息状态设置为 `'success'`
|
||||
2. UI 显示完整消息和操作按钮
|
||||
|
||||
## 关键改进点
|
||||
|
||||
### 1. 按钮事件绑定
|
||||
- ✅ 使用函数调用而非三元表达式
|
||||
- ✅ 运行时动态判断状态
|
||||
|
||||
### 2. 状态管理
|
||||
- ✅ 新增 `paused` 状态区分用户中止和错误
|
||||
- ✅ 保留用户中止前的已生成内容
|
||||
|
||||
### 3. 中止信号传递
|
||||
- ✅ 完整的信号链:UI → Store → Service → API
|
||||
- ✅ 在流读取循环中实时检查中止状态
|
||||
|
||||
### 4. 用户体验
|
||||
- ✅ 立即响应停止操作
|
||||
- ✅ 保留部分生成的内容可查看
|
||||
- ✅ 可以对停止的消息进行复制、重新生成等操作
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 手动测试步骤:
|
||||
1. **启动应用**并创建新对话
|
||||
2. **发送消息**并立即点击"停止"按钮
|
||||
3. **验证**:
|
||||
- ✅ 流式输出立即停止
|
||||
- ✅ 消息显示"已停止"标签
|
||||
- ✅ 已生成的内容被保留
|
||||
- ✅ 可以对停止的消息进行操作(复制、重新生成、删除)
|
||||
- ✅ `isSending` 状态恢复为 `false`
|
||||
- ✅ 可以继续发送新消息
|
||||
|
||||
### 预期行为:
|
||||
- **立即响应**:点击停止后 100ms 内停止输出
|
||||
- **状态正确**:消息标记为 "已停止" 而非 "发送失败"
|
||||
- **内容保留**:显示停止前生成的所有文本
|
||||
- **可继续操作**:可以立即发送下一条消息
|
||||
|
||||
## 参考资源
|
||||
- Cherry Studio PAUSED 状态设计
|
||||
- AbortController Web API
|
||||
- Fetch API with abort signals
|
||||
- ReadableStream reader.cancel() method
|
||||
|
||||
## 修改文件清单
|
||||
1. `web/src/components/Chat/ChatLayout.vue` - 按钮事件和UI显示
|
||||
2. `web/src/types/chat.ts` - 类型定义
|
||||
3. `web/src/services/chatService.ts` - 错误处理逻辑
|
||||
4. `web/src/services/modelServiceManager.ts` - 流式读取中止检查
|
||||
5. `web/src/stores/chatStore.ts` - 已有正确的中止逻辑(无需修改)
|
||||
|
||||
## 注意事项
|
||||
- 确保在所有流式读取循环中检查 `signal.aborted`
|
||||
- 区分用户中止 (`AbortError`) 和其他错误
|
||||
- 保持状态一致性:`isSending` 必须在 finally 块中重置
|
||||
Reference in New Issue
Block a user