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

239 lines
6.0 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. **停止逻辑不生效** - 即使调用了停止方法AI 回复仍在继续生成
## ✅ 解决方案
参考 **Cherry Studio****PAUSED** 状态设计,实现完整的停止生成逻辑。
## 📝 修改清单
### 1. 修复按钮事件绑定 (`ChatLayout.vue`)
**原问题代码:**
```vue
@click="store.state.isSending ? handleStopGeneration : handleSendMessage"
```
**问题分析:**
- 这个三元表达式在编译时求值,而不是运行时
- 导致点击时总是执行同一个函数引用
**修复代码:**
```vue
@click="handleButtonClick"
```
```typescript
const handleButtonClick = () => {
if (store.state.isSending) {
handleStopGeneration()
} else {
handleSendMessage()
}
}
```
### 2. 添加 PAUSED 状态 (`types/chat.ts`)
```typescript
// 新增 paused 状态
export type MessageStatus = 'pending' | 'sending' | 'success' | 'error' | 'paused'
// 新增 paused 事件
export interface StreamEvent {
type: 'start' | 'delta' | 'end' | 'error' | 'paused'
// ...
}
```
### 3. 优化停止时的错误处理 (`chatService.ts`)
```typescript
catch (error) {
const isAborted = error instanceof Error && error.name === 'AbortError'
if (isAborted) {
// 用户主动停止 - 标记为 paused保留内容
assistantMessage.status = 'paused'
assistantMessage.error = undefined
onChunk({ type: 'paused', messageId: assistantMessage.id })
} else {
// 真实错误 - 标记为 error
assistantMessage.status = 'error'
assistantMessage.error = error instanceof Error ? error.message : '发送失败'
onChunk({ type: 'error', error: assistantMessage.error })
}
}
```
### 4. 在流读取中检查中止信号 (`modelServiceManager.ts`)
**关键修复:**
```typescript
while (true) {
// ⚠️ 关键:每次读取前检查中止信号
if (signal?.aborted) {
console.log('🛑 检测到中止信号,停止读取流')
reader.cancel() // 取消流读取
throw new DOMException('用户中止操作', 'AbortError')
}
const { done, value } = await reader.read()
if (done) break
// 处理数据...
}
```
**优化 catch 块:**
```typescript
catch (error) {
if (timeoutId) clearTimeout(timeoutId)
// 正确处理 AbortError不改写为"超时"
if (error instanceof Error && error.name === 'AbortError') {
throw error // 直接抛出,保留原始错误
}
if (error instanceof DOMException && error.name === 'AbortError') {
throw error
}
throw error
}
```
### 5. UI 显示优化 (`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">
<n-button text size="tiny" @click="handleCopyMessage(msg.content)">
复制
</n-button>
<n-button text size="tiny" @click="handleRegenerateMessage(msg.id)">
重新生成
</n-button>
<n-button text size="tiny" @click="handleDeleteMessage(msg.id)">
删除
</n-button>
</div>
```
## 🔄 工作流程
```
用户点击停止
handleButtonClick() → 检测 isSending
handleStopGeneration()
store.stopGeneration() → abortController.abort()
信号传递到 chatService.sendMessageStream()
信号传递到 modelServiceManager.makeChatRequestStream()
流读取循环检测 signal.aborted
reader.cancel() + 抛出 AbortError
chatService catch 块识别为用户中止
设置消息状态为 'paused',保留已生成内容
UI 更新:显示"已停止"标签 + 操作按钮
```
## 🎨 关键改进
### 1. 事件绑定
- ❌ 错误:使用三元表达式绑定函数引用
- ✅ 正确:运行时动态判断并调用对应函数
### 2. 状态区分
- ❌ 之前:用户停止被标记为 `error`
- ✅ 现在:用户停止标记为 `paused`,保留内容
### 3. 信号传递
- ❌ 之前:只在 fetch 中使用 signal
- ✅ 现在:在流读取循环中实时检查 `signal.aborted`
### 4. 用户体验
- ✅ 点击立即响应(< 100ms
- 已生成内容完整保留
- 可对停止的消息进行操作
- 停止后立即可发送新消息
## 📁 修改的文件
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` - (已有正确逻辑无需修改)
## 🧪 测试验证
### 手动测试
```bash
cd web
npm run dev
```
1. 发送一个问题
2. AI 回复时点击"停止"
3. 验证
- 输出立即停止
- 显示"已停止"标签黄色
- 已生成内容保留
- 显示操作按钮
- 可以继续对话
### 控制台日志
停止时应该看到
```
🛑 [handleStopGeneration] 用户请求停止生成
🛑 [makeChatRequestStream] 检测到中止信号,停止读取流
⚠️ [makeChatRequestStream] 请求被中止: 用户中止操作
⏸️ [sendMessageStream] 用户主动停止生成,保留已生成内容
```
## 📚 参考文档
- `STOP_GENERATION_FIX.md` - 详细的技术实现文档
- `STOP_GENERATION_TEST.md` - 完整的测试指南
## ✨ 成功标准
- [x] 按钮点击有明显反应
- [x] 流输出在 100ms 内停止
- [x] 显示"已停止"而非"失败"
- [x] 保留已生成内容
- [x] 停止后可立即继续对话
- [x] 可对停止的消息进行操作
- [x] 无意外错误日志
## 🔍 参考实现
Cherry Studio 的相关设计理念
- 区分用户主动操作和系统错误
- 保留部分生成的内容供用户查看
- 提供完整的消息操作能力
- 确保状态一致性和可恢复性
---
**修复完成!** 🎉
现在停止按钮应该能正常工作点击后会立即停止 AI 生成并保留已生成的内容