Files
map-client-vue/STOP_GENERATION_FIX.md
2025-10-15 15:07:45 +08:00

176 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 停止生成功能修复文档
## 问题描述
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 块中重置