feat: 实现聊天取消功能,提升用户交互体验
- 在教练页面中添加用户取消发送或终止回复的能力 - 更新发送按钮状态,支持发送和取消状态切换 - 在流式回复中显示取消按钮,允许用户中断助手的生成 - 增强请求管理,添加请求序列号和有效性验证,防止延迟响应影响用户体验 - 优化错误处理,区分用户主动取消和网络错误,提升系统稳定性 - 更新相关文档,详细描述取消功能的实现和用户体验设计
This commit is contained in:
234
docs/complete-cancel-solution.md
Normal file
234
docs/complete-cancel-solution.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# 完整取消功能解决方案
|
||||
|
||||
## 问题诊断
|
||||
|
||||
您遇到的问题确实需要前后端协作解决:
|
||||
|
||||
1. **客户端问题**: XMLHttpRequest.abort() 只能终止网络传输,无法停止服务端处理
|
||||
2. **会话持久化问题**: 服务端可能已经开始处理并保存会话记录
|
||||
3. **延迟响应问题**: 服务端处理完成后,响应仍会到达客户端
|
||||
|
||||
## 已实现的客户端增强方案
|
||||
|
||||
### 1. 请求序列号机制
|
||||
```typescript
|
||||
// 防止延迟响应影响当前状态
|
||||
const requestSequenceRef = useRef<number>(0);
|
||||
const activeRequestIdRef = useRef<string | null>(null);
|
||||
|
||||
function cancelCurrentRequest() {
|
||||
// 增加序列号,使后续响应失效
|
||||
requestSequenceRef.current += 1;
|
||||
activeRequestIdRef.current = null;
|
||||
|
||||
// 中断网络请求
|
||||
streamAbortRef.current?.abort();
|
||||
|
||||
// 清理状态和UI
|
||||
setIsSending(false);
|
||||
setIsStreaming(false);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 请求有效性验证
|
||||
```typescript
|
||||
const isRequestValid = () => {
|
||||
return activeRequestIdRef.current === requestId &&
|
||||
requestSequenceRef.current === currentSequence;
|
||||
};
|
||||
|
||||
// 在所有回调中验证
|
||||
const onChunk = (chunk: string) => {
|
||||
if (!isRequestValid()) {
|
||||
console.log('Ignoring chunk from invalidated request');
|
||||
return;
|
||||
}
|
||||
// 处理chunk...
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 延迟会话ID生成
|
||||
```typescript
|
||||
// 修改前:立即生成会话ID
|
||||
function ensureConversationId(): string {
|
||||
const cid = `mobile-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
setConversationId(cid); // 立即设置
|
||||
return cid;
|
||||
}
|
||||
|
||||
// 修改后:延迟生成,只在请求成功时设置
|
||||
function ensureConversationId(): string {
|
||||
if (conversationId && conversationId.trim()) return conversationId;
|
||||
return ''; // 返回空,让服务端生成
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 请求追踪机制
|
||||
```typescript
|
||||
// 在 api.ts 中添加请求ID
|
||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const requestHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-Id': requestId, // 用于服务端追踪
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## 需要服务端配合的方案
|
||||
|
||||
### 方案1: 连接断开检测(推荐)
|
||||
|
||||
**原理**: 服务端检测客户端连接状态,断开时停止处理
|
||||
|
||||
```python
|
||||
# 服务端伪代码
|
||||
async def ai_chat_stream(request):
|
||||
conversation_id = request.json.get('conversationId')
|
||||
is_new_conversation = not conversation_id
|
||||
|
||||
try:
|
||||
# 检查客户端连接
|
||||
if await request.is_disconnected():
|
||||
return
|
||||
|
||||
# 延迟创建会话记录
|
||||
temp_conv_id = None
|
||||
if is_new_conversation:
|
||||
temp_conv_id = create_temp_conversation()
|
||||
|
||||
async for chunk in ai_generate_stream():
|
||||
# 每次生成前检查连接
|
||||
if await request.is_disconnected():
|
||||
if temp_conv_id:
|
||||
delete_temp_conversation(temp_conv_id)
|
||||
return
|
||||
|
||||
yield chunk
|
||||
|
||||
# 只有成功完成才保存会话
|
||||
if temp_conv_id:
|
||||
save_permanent_conversation(temp_conv_id)
|
||||
|
||||
except ClientDisconnectedError:
|
||||
if temp_conv_id:
|
||||
delete_temp_conversation(temp_conv_id)
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 自动检测,无需额外API
|
||||
- 资源清理及时
|
||||
- 实现相对简单
|
||||
|
||||
### 方案2: 主动取消端点(备选)
|
||||
|
||||
**原理**: 提供专门的取消API,客户端主动通知
|
||||
|
||||
```python
|
||||
@app.post("/api/ai-coach/cancel")
|
||||
async def cancel_request(request):
|
||||
request_id = request.json.get('requestId')
|
||||
conversation_id = request.json.get('conversationId')
|
||||
|
||||
# 标记请求取消
|
||||
cancel_request_processing(request_id)
|
||||
|
||||
# 清理临时会话
|
||||
if is_temp_conversation(conversation_id):
|
||||
delete_conversation(conversation_id)
|
||||
|
||||
return {"status": "cancelled"}
|
||||
```
|
||||
|
||||
**客户端调用**:
|
||||
```typescript
|
||||
function cancelCurrentRequest() {
|
||||
// 现有逻辑...
|
||||
|
||||
// 主动通知服务端
|
||||
if (activeRequestIdRef.current) {
|
||||
notifyServerCancel(conversationId, activeRequestIdRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
async function notifyServerCancel(conversationId?: string, requestId?: string) {
|
||||
try {
|
||||
await api.post('/api/ai-coach/cancel', {
|
||||
requestId,
|
||||
conversationId
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to notify server:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库设计建议
|
||||
|
||||
```sql
|
||||
-- 会话状态管理
|
||||
CREATE TABLE conversations (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
user_id INT,
|
||||
status ENUM('temp', 'active', 'cancelled') DEFAULT 'temp',
|
||||
created_at TIMESTAMP,
|
||||
confirmed_at TIMESTAMP NULL,
|
||||
messages JSON,
|
||||
INDEX idx_status_created (status, created_at)
|
||||
);
|
||||
|
||||
-- 定期清理临时会话(防止内存泄漏)
|
||||
-- 可以用定时任务或在新请求时清理
|
||||
DELETE FROM conversations
|
||||
WHERE status = 'temp'
|
||||
AND created_at < NOW() - INTERVAL 10 MINUTE;
|
||||
```
|
||||
|
||||
## 立即可用的解决方案
|
||||
|
||||
### 当前已实现(无需服务端修改)
|
||||
|
||||
客户端增强已经可以解决大部分问题:
|
||||
|
||||
1. ✅ **防止延迟响应**: 请求序列号机制
|
||||
2. ✅ **严格状态管理**: 请求有效性验证
|
||||
3. ✅ **延迟会话创建**: 避免空会话记录
|
||||
4. ✅ **即时UI反馈**: 取消后立即清理界面
|
||||
|
||||
### 效果评估
|
||||
|
||||
**改进前**:
|
||||
- 点击取消 → 网络中断 → 但服务端继续处理 → 稍后响应仍然到达
|
||||
- 会话记录已创建且保留
|
||||
|
||||
**改进后**:
|
||||
- 点击取消 → 序列号增加 → 界面立即清理 → 延迟响应被忽略
|
||||
- 会话ID延迟生成,减少空记录
|
||||
|
||||
### 推荐实施步骤
|
||||
|
||||
**第一阶段(立即可用)**:
|
||||
1. ✅ 使用当前客户端增强方案
|
||||
2. 测试取消功能的改善效果
|
||||
|
||||
**第二阶段(服务端配合)**:
|
||||
1. 实现连接断开检测
|
||||
2. 临时会话管理机制
|
||||
3. 完善资源清理
|
||||
|
||||
**第三阶段(可选优化)**:
|
||||
1. 主动取消通知端点
|
||||
2. 数据库优化和定期清理
|
||||
3. 监控和日志完善
|
||||
|
||||
## 测试验证
|
||||
|
||||
建议测试以下场景:
|
||||
|
||||
1. **快速取消**: 发送后立即点击取消
|
||||
2. **生成中取消**: 在AI回复过程中取消
|
||||
3. **网络异常**: 弱网环境下的取消行为
|
||||
4. **连续操作**: 快速发送多条消息并取消
|
||||
5. **状态恢复**: 取消后是否能正常发送新消息
|
||||
|
||||
通过这个完整方案,可以显著改善取消功能的用户体验,即使在服务端暂未配合的情况下,客户端增强也能解决大部分问题。
|
||||
Reference in New Issue
Block a user