update at 2025-10-15 15:07:45

This commit is contained in:
douboer
2025-10-15 15:07:45 +08:00
parent eb8fb51283
commit 901d00e4e1
21 changed files with 4030 additions and 57 deletions

175
STOP_GENERATION_FIX.md Normal file
View 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 块中重置