- Remove direct task.cancel() calls, use stop flag instead - Monitor stop flag every 100ms during tool execution - Cancel tool task immediately when stop flag is detected - Return "命令执行被用户取消" as tool result with role=tool - Save cancellation result to conversation history - Prevent abrupt task termination, allow graceful shutdown Changes: - server/socket_handlers.py: Comment out pending_task.cancel() - server/tasks.py: Comment out entry['task'].cancel() - server/chat_flow_tool_loop.py: Add stop flag monitoring loop Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
192 lines
5.3 KiB
Markdown
192 lines
5.3 KiB
Markdown
# Bug 修复 v2 - 工具块和轮询优化
|
||
|
||
## 修复的问题
|
||
|
||
### 1. 工具块无法显示
|
||
**原因**: 工具事件处理逻辑不完整,缺少关键的工具创建和更新逻辑
|
||
|
||
**解决方案**:
|
||
- 完整实现 `handleToolPreparing()` - 创建工具准备块
|
||
- 完整实现 `handleToolStart()` - 工具开始执行
|
||
- 完整实现 `handleToolUpdateAction()` - 工具状态更新和完成
|
||
- 添加 `update_action` 事件处理(工具完成的关键事件)
|
||
- 在 WebSocket 中跳过 `update_action` 事件
|
||
|
||
### 2. 轮询频率太慢(1秒)
|
||
**原因**: 1秒的轮询间隔无法提供流式输出的体验
|
||
|
||
**解决方案**:
|
||
- 将轮询间隔从 1000ms 改为 **150ms**
|
||
- 接近流式输出的效果(每秒约 6-7 次更新)
|
||
- 在 `stores/task.ts` 中修改 `startPolling()` 方法
|
||
|
||
### 3. 刷新后加载两遍内容
|
||
**原因**:
|
||
- `restoreTaskState()` 没有检查是否已在流式输出中
|
||
- `handleTaskComplete()` 会重新加载历史记录
|
||
|
||
**解决方案**:
|
||
- 在 `restoreTaskState()` 中添加状态检查,避免重复恢复
|
||
- 检查最后一条消息是否是 assistant 消息
|
||
- 在 `handleTaskComplete()` 中移除 `fetchAndDisplayHistory()`,只更新统计
|
||
|
||
## 修改的文件
|
||
|
||
### 1. static/src/app/methods/taskPolling.ts
|
||
**工具处理逻辑**:
|
||
```javascript
|
||
handleToolPreparing(data) {
|
||
// 创建工具准备块
|
||
const action = {
|
||
id: data.id,
|
||
type: 'tool',
|
||
tool: {
|
||
status: 'preparing',
|
||
name: data.name,
|
||
// ... 完整的工具属性
|
||
}
|
||
};
|
||
msg.actions.push(action);
|
||
this.preparingTools.set(data.id, action);
|
||
}
|
||
|
||
handleToolStart(data) {
|
||
// 从 preparing 转为 running
|
||
let action = this.preparingTools.get(data.preparing_id);
|
||
action.tool.status = 'running';
|
||
action.tool.arguments = data.arguments;
|
||
// ... 更新工具状态
|
||
}
|
||
|
||
handleToolUpdateAction(data) {
|
||
// 更新工具状态(包括完成)
|
||
let targetAction = this.toolFindAction(data.id, ...);
|
||
targetAction.tool.status = data.status;
|
||
targetAction.tool.result = data.result;
|
||
// ... 更新工具结果
|
||
}
|
||
```
|
||
|
||
**恢复逻辑优化**:
|
||
```javascript
|
||
async restoreTaskState() {
|
||
// 检查是否已在流式输出中
|
||
if (this.streamingMessage || this.taskInProgress) {
|
||
return;
|
||
}
|
||
|
||
// 检查是否已有 assistant 消息
|
||
const lastMessage = this.messages[this.messages.length - 1];
|
||
const hasAssistantMessage = lastMessage && lastMessage.role === 'assistant';
|
||
|
||
// 只标记状态,不清空消息
|
||
this.streamingMessage = true;
|
||
this.taskInProgress = true;
|
||
}
|
||
```
|
||
|
||
**任务完成优化**:
|
||
```javascript
|
||
handleTaskComplete(data) {
|
||
// 只更新统计,不重新加载历史
|
||
this.fetchConversationTokenStatistics();
|
||
this.updateCurrentContextTokens();
|
||
// 移除了 fetchAndDisplayHistory()
|
||
}
|
||
```
|
||
|
||
### 2. static/src/stores/task.ts
|
||
**轮询频率优化**:
|
||
```javascript
|
||
startPolling(eventHandler) {
|
||
// 150ms 间隔,接近流式输出效果
|
||
this.pollingInterval = window.setInterval(() => {
|
||
this.pollTaskEvents(handler);
|
||
}, 150);
|
||
}
|
||
```
|
||
|
||
### 3. static/src/composables/useLegacySocket.ts
|
||
**跳过 update_action 事件**:
|
||
```javascript
|
||
ctx.socket.on('update_action', (data) => {
|
||
if (ctx.usePollingMode) {
|
||
return; // 跳过 WebSocket 事件
|
||
}
|
||
// ... 原有逻辑
|
||
});
|
||
```
|
||
|
||
## 事件处理流程
|
||
|
||
### 工具执行流程
|
||
1. `tool_preparing` → 创建工具准备块
|
||
2. `tool_start` → 工具开始执行,更新状态为 running
|
||
3. `update_action` → 工具完成,更新状态为 completed,显示结果
|
||
|
||
### 轮询流程
|
||
1. 每 150ms 轮询一次
|
||
2. 获取新事件(通过 `from` 参数)
|
||
3. 处理事件,更新界面
|
||
4. 任务完成后停止轮询
|
||
|
||
### 刷新恢复流程
|
||
1. 页面加载完成
|
||
2. 延迟 1 秒后调用 `restoreTaskState()`
|
||
3. 检查是否已在流式输出中(避免重复)
|
||
4. 检查是否已有 assistant 消息(历史已加载)
|
||
5. 只标记状态,启动轮询
|
||
6. 轮询只处理新事件,不重建历史
|
||
|
||
## 性能优化
|
||
|
||
- **轮询频率**: 150ms(每秒约 6-7 次)
|
||
- **事件增量获取**: 通过 `from` 参数只获取新事件
|
||
- **避免重复加载**: 检查状态和消息,避免重复恢复
|
||
- **避免重复显示**: 任务完成后不重新加载历史
|
||
|
||
## 测试建议
|
||
|
||
1. **工具块显示**
|
||
- 发送需要调用工具的消息
|
||
- 观察工具块是否正常显示
|
||
- 观察工具状态变化(preparing → running → completed)
|
||
- 观察工具结果是否正确显示
|
||
|
||
2. **流式输出效果**
|
||
- 发送消息,观察输出是否流畅
|
||
- 观察思考块、文本块的更新频率
|
||
- 对比之前 1 秒轮询的卡顿感
|
||
|
||
3. **刷新恢复**
|
||
- 任务执行中刷新页面
|
||
- 观察是否只显示一遍内容
|
||
- 观察新内容是否正常追加
|
||
- 观察工具块是否保留
|
||
|
||
4. **任务完成**
|
||
- 等待任务完成
|
||
- 观察是否有重复内容
|
||
- 观察统计是否正确更新
|
||
|
||
## 构建
|
||
|
||
```bash
|
||
cd static
|
||
npm run build
|
||
```
|
||
|
||
构建成功!
|
||
- `static/dist/assets/main.js` (689.89 kB)
|
||
- `static/dist/assets/task.js` (3.68 kB)
|
||
|
||
## 已知改进
|
||
|
||
- ✅ 工具块正常显示
|
||
- ✅ 轮询频率提升到 150ms(接近流式效果)
|
||
- ✅ 刷新后不重复显示内容
|
||
- ✅ 任务完成后不重复加载历史
|
||
- ✅ 状态检查避免重复恢复
|
||
|
||
现在可以启动服务器测试了!
|