feat: 实现聊天取消功能,提升用户交互体验
- 在教练页面中添加用户取消发送或终止回复的能力 - 更新发送按钮状态,支持发送和取消状态切换 - 在流式回复中显示取消按钮,允许用户中断助手的生成 - 增强请求管理,添加请求序列号和有效性验证,防止延迟响应影响用户体验 - 优化错误处理,区分用户主动取消和网络错误,提升系统稳定性 - 更新相关文档,详细描述取消功能的实现和用户体验设计
This commit is contained in:
266
docs/server-side-cancel-support.md
Normal file
266
docs/server-side-cancel-support.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 服务端取消支持方案
|
||||
|
||||
## 问题分析
|
||||
|
||||
当前取消功能的问题:
|
||||
1. 客户端 `XMLHttpRequest.abort()` 只能终止网络传输,无法停止服务端处理
|
||||
2. 服务端可能已经生成并保存了会话记录
|
||||
3. 延迟响应导致"取消"的内容稍后出现
|
||||
|
||||
## 服务端需要支持的功能
|
||||
|
||||
### 1. 请求中断检测
|
||||
|
||||
**方案A:连接断开检测(推荐)**
|
||||
```python
|
||||
# 伪代码示例
|
||||
async def ai_chat_stream(request):
|
||||
conversation_id = request.json.get('conversationId')
|
||||
messages = request.json.get('messages', [])
|
||||
|
||||
# 如果是新会话且客户端断开,不创建会话记录
|
||||
is_new_conversation = not conversation_id
|
||||
temp_conversation_id = None
|
||||
|
||||
try:
|
||||
# 检查客户端连接状态
|
||||
if await request.is_disconnected():
|
||||
print("Client disconnected before processing")
|
||||
return
|
||||
|
||||
# 只有在开始生成内容时才创建会话
|
||||
if is_new_conversation:
|
||||
temp_conversation_id = create_conversation()
|
||||
|
||||
async for chunk in ai_generate_stream(messages):
|
||||
# 每次生成前检查连接状态
|
||||
if await request.is_disconnected():
|
||||
print("Client disconnected during generation")
|
||||
# 如果是新会话,删除临时创建的会话
|
||||
if temp_conversation_id:
|
||||
delete_conversation(temp_conversation_id)
|
||||
return
|
||||
|
||||
yield chunk
|
||||
|
||||
# 成功完成,保存会话
|
||||
if temp_conversation_id:
|
||||
save_conversation(temp_conversation_id, messages + [ai_response])
|
||||
|
||||
except ClientDisconnectedError:
|
||||
# 清理未完成的会话
|
||||
if temp_conversation_id:
|
||||
delete_conversation(temp_conversation_id)
|
||||
```
|
||||
|
||||
**方案B:取消端点(备选)**
|
||||
```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_new_conversation(conversation_id):
|
||||
delete_conversation(conversation_id)
|
||||
|
||||
return {"status": "cancelled"}
|
||||
|
||||
# 在主处理函数中检查取消状态
|
||||
async def ai_chat_stream(request):
|
||||
request_id = request.headers.get('X-Request-Id')
|
||||
|
||||
while generating:
|
||||
if is_request_cancelled(request_id):
|
||||
cleanup_and_exit()
|
||||
return
|
||||
# 继续处理...
|
||||
```
|
||||
|
||||
### 2. 会话管理优化
|
||||
|
||||
**延迟会话创建:**
|
||||
```python
|
||||
class ConversationManager:
|
||||
def __init__(self):
|
||||
self.temp_conversations = {}
|
||||
|
||||
async def start_conversation(self, client_id):
|
||||
"""开始新会话,但不立即持久化"""
|
||||
temp_id = f"temp_{uuid4()}"
|
||||
self.temp_conversations[temp_id] = {
|
||||
'client_id': client_id,
|
||||
'messages': [],
|
||||
'created_at': datetime.now(),
|
||||
'confirmed': False
|
||||
}
|
||||
return temp_id
|
||||
|
||||
async def confirm_conversation(self, temp_id, final_messages):
|
||||
"""确认会话并持久化"""
|
||||
if temp_id in self.temp_conversations:
|
||||
# 保存到数据库
|
||||
real_id = self.save_to_database(final_messages)
|
||||
del self.temp_conversations[temp_id]
|
||||
return real_id
|
||||
|
||||
async def cancel_conversation(self, temp_id):
|
||||
"""取消临时会话"""
|
||||
if temp_id in self.temp_conversations:
|
||||
del self.temp_conversations[temp_id]
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
### 3. 流式响应优化
|
||||
|
||||
**分段提交策略:**
|
||||
```python
|
||||
async def ai_chat_stream(request):
|
||||
try:
|
||||
# 第一阶段:验证请求
|
||||
if await request.is_disconnected():
|
||||
return
|
||||
|
||||
# 第二阶段:开始生成(创建临时会话)
|
||||
temp_conv_id = await start_temp_conversation()
|
||||
yield json.dumps({"temp_conversation_id": temp_conv_id})
|
||||
|
||||
# 第三阶段:流式生成内容
|
||||
full_response = ""
|
||||
for chunk in ai_generate():
|
||||
if await request.is_disconnected():
|
||||
await cancel_temp_conversation(temp_conv_id)
|
||||
return
|
||||
|
||||
full_response += chunk
|
||||
yield chunk
|
||||
|
||||
# 第四阶段:确认并保存会话
|
||||
final_conv_id = await confirm_conversation(temp_conv_id, full_response)
|
||||
yield json.dumps({"final_conversation_id": final_conv_id})
|
||||
|
||||
except ClientDisconnectedError:
|
||||
await cancel_temp_conversation(temp_conv_id)
|
||||
```
|
||||
|
||||
## 具体实现建议
|
||||
|
||||
### 1. 服务端 API 修改
|
||||
|
||||
**请求头支持:**
|
||||
```http
|
||||
POST /api/ai-coach/chat
|
||||
X-Request-Id: req_1234567890_abcdef
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"conversationId": "existing_conv_id", // 可选,新会话时为空
|
||||
"messages": [...],
|
||||
"stream": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应头:**
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
X-Conversation-Id: mobile-1701234567-abcdef123
|
||||
X-Request-Id: req_1234567890_abcdef
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
```
|
||||
|
||||
### 2. 数据库设计优化
|
||||
|
||||
**会话状态管理:**
|
||||
```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;
|
||||
```
|
||||
|
||||
### 3. 客户端配合改进
|
||||
|
||||
**添加请求ID头:**
|
||||
```typescript
|
||||
// 在 api.ts 中添加
|
||||
export function postTextStream(path: string, body: any, callbacks: TextStreamCallbacks, options: TextStreamOptions = {}) {
|
||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const requestHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...(options.headers || {}),
|
||||
};
|
||||
|
||||
// ... 其余代码
|
||||
}
|
||||
```
|
||||
|
||||
**主动取消通知(可选):**
|
||||
```typescript
|
||||
async function notifyServerCancel(conversationId?: string, requestId?: string) {
|
||||
try {
|
||||
if (requestId) {
|
||||
await api.post('/api/ai-coach/cancel', {
|
||||
requestId,
|
||||
conversationId
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to notify server of cancellation:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整取消流程
|
||||
|
||||
### 客户端取消时:
|
||||
1. 增加请求序列号(防止延迟响应)
|
||||
2. 调用 `XMLHttpRequest.abort()`
|
||||
3. 清理本地状态
|
||||
4. (可选)通知服务端取消
|
||||
|
||||
### 服务端检测到断开:
|
||||
1. 立即停止 AI 内容生成
|
||||
2. 检查会话状态(临时/已确认)
|
||||
3. 如果是临时会话,删除记录
|
||||
4. 清理相关资源
|
||||
|
||||
### 防止延迟响应:
|
||||
1. 客户端使用请求序列号验证
|
||||
2. 服务端检查连接状态
|
||||
3. 分段式会话确认机制
|
||||
|
||||
## 优先级建议
|
||||
|
||||
**立即实现(高优先级):**
|
||||
1. 客户端请求序列号验证 ✅ 已实现
|
||||
2. 延迟会话ID生成 ✅ 已实现
|
||||
3. 客户端状态严格管理 ✅ 已实现
|
||||
|
||||
**服务端配合(中优先级):**
|
||||
1. 连接断开检测
|
||||
2. 临时会话管理
|
||||
3. 请求ID追踪
|
||||
|
||||
**可选优化(低优先级):**
|
||||
1. 主动取消通知端点
|
||||
2. 会话状态数据库优化
|
||||
3. 定期清理机制
|
||||
|
||||
通过以上客户端和服务端的协作改进,可以彻底解决取消功能的问题,确保用户取消操作的即时性和有效性。
|
||||
Reference in New Issue
Block a user