Files
digital-pilates/docs/complete-cancel-solution.md
richarjiang d52981ab29 feat: 实现聊天取消功能,提升用户交互体验
- 在教练页面中添加用户取消发送或终止回复的能力
- 更新发送按钮状态,支持发送和取消状态切换
- 在流式回复中显示取消按钮,允许用户中断助手的生成
- 增强请求管理,添加请求序列号和有效性验证,防止延迟响应影响用户体验
- 优化错误处理,区分用户主动取消和网络错误,提升系统稳定性
- 更新相关文档,详细描述取消功能的实现和用户体验设计
2025-08-18 18:59:23 +08:00

6.2 KiB
Raw Permalink Blame History

完整取消功能解决方案

问题诊断

您遇到的问题确实需要前后端协作解决:

  1. 客户端问题: XMLHttpRequest.abort() 只能终止网络传输,无法停止服务端处理
  2. 会话持久化问题: 服务端可能已经开始处理并保存会话记录
  3. 延迟响应问题: 服务端处理完成后,响应仍会到达客户端

已实现的客户端增强方案

1. 请求序列号机制

// 防止延迟响应影响当前状态
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. 请求有效性验证

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生成

// 修改前立即生成会话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. 请求追踪机制

// 在 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: 连接断开检测(推荐)

原理: 服务端检测客户端连接状态,断开时停止处理

# 服务端伪代码
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客户端主动通知

@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"}

客户端调用:

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);
  }
}

数据库设计建议

-- 会话状态管理
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. 状态恢复: 取消后是否能正常发送新消息

通过这个完整方案,可以显著改善取消功能的用户体验,即使在服务端暂未配合的情况下,客户端增强也能解决大部分问题。