5.2 KiB
5.2 KiB
停止生成功能修复文档
问题描述
- 按钮点击无效:确认/停止按钮点击没有响应
- 停止逻辑不生效:即使调用了
stopGeneration(),流式输出仍在继续
参考实现
参考了 Cherry Studio 中的 PAUSED 状态设计理念。
修复内容
1. 修复按钮点击事件绑定
问题:原代码使用三元表达式直接绑定函数引用
@click="store.state.isSending ? handleStopGeneration : handleSendMessage"
修复:改为调用统一的处理函数
@click="handleButtonClick"
const handleButtonClick = () => {
if (store.state.isSending) {
handleStopGeneration()
} else {
handleSendMessage()
}
}
2. 添加 PAUSED 消息状态
文件:web/src/types/chat.ts
// 添加 '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
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
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
<!-- 显示暂停状态 -->
<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>
工作流程
用户点击停止按钮时:
handleButtonClick()检测到isSending = true- 调用
handleStopGeneration() store.stopGeneration()执行abortController.abort()- 中止信号传递到
chatService.sendMessageStream() - 信号继续传递到
modelServiceManager.makeChatRequestStream() - 流式读取循环检测到
signal.aborted - 调用
reader.cancel()并抛出AbortError - 错误向上冒泡,在
chatService中被识别为用户中止 - 消息状态设置为
'paused',保留已生成内容 - UI 更新显示"已停止"标签
正常完成时:
- 流式读取完成,消息状态设置为
'success' - UI 显示完整消息和操作按钮
关键改进点
1. 按钮事件绑定
- ✅ 使用函数调用而非三元表达式
- ✅ 运行时动态判断状态
2. 状态管理
- ✅ 新增
paused状态区分用户中止和错误 - ✅ 保留用户中止前的已生成内容
3. 中止信号传递
- ✅ 完整的信号链:UI → Store → Service → API
- ✅ 在流读取循环中实时检查中止状态
4. 用户体验
- ✅ 立即响应停止操作
- ✅ 保留部分生成的内容可查看
- ✅ 可以对停止的消息进行复制、重新生成等操作
测试验证
手动测试步骤:
- 启动应用并创建新对话
- 发送消息并立即点击"停止"按钮
- 验证:
- ✅ 流式输出立即停止
- ✅ 消息显示"已停止"标签
- ✅ 已生成的内容被保留
- ✅ 可以对停止的消息进行操作(复制、重新生成、删除)
- ✅
isSending状态恢复为false - ✅ 可以继续发送新消息
预期行为:
- 立即响应:点击停止后 100ms 内停止输出
- 状态正确:消息标记为 "已停止" 而非 "发送失败"
- 内容保留:显示停止前生成的所有文本
- 可继续操作:可以立即发送下一条消息
参考资源
- Cherry Studio PAUSED 状态设计
- AbortController Web API
- Fetch API with abort signals
- ReadableStream reader.cancel() method
修改文件清单
web/src/components/Chat/ChatLayout.vue- 按钮事件和UI显示web/src/types/chat.ts- 类型定义web/src/services/chatService.ts- 错误处理逻辑web/src/services/modelServiceManager.ts- 流式读取中止检查web/src/stores/chatStore.ts- 已有正确的中止逻辑(无需修改)
注意事项
- 确保在所有流式读取循环中检查
signal.aborted - 区分用户中止 (
AbortError) 和其他错误 - 保持状态一致性:
isSending必须在 finally 块中重置