22 KiB
AI Agent 系统停止按钮和运行状态机制分析
概述
本文档详细分析 AI Agent 系统中停止按钮和运行状态机制的实现,这是系统中 bug 最多的部分。系统采用 REST API + 轮询模式(usePollingMode: true),已禁用 WebSocket 实时推送。
1. 停止按钮的显示/隐藏逻辑
1.1 组件定义
文件位置: /static/src/components/input/InputComposer.vue:48-61
<button
type="button"
class="stadium-btn send-btn"
@click="$emit('send-or-stop')"
:disabled="
!isConnected ||
(inputLocked && !streamingMessage) ||
(mediaUploading && !streamingMessage) ||
((!(inputMessage || '').trim() && (!selectedImages?.length && !selectedVideos?.length)) && !streamingMessage)
"
>
<span v-if="streamingMessage" class="stop-icon"></span>
<span v-else class="send-icon"></span>
</button>
1.2 显示条件
停止按钮的显示由 streamingMessage 状态控制:
- 显示停止按钮:
streamingMessage === true - 显示发送按钮:
streamingMessage === false
1.3 禁用条件
按钮被禁用的情况:
- 未连接服务器 (
!isConnected) - 输入被锁定且不在流式输出中 (
inputLocked && !streamingMessage) - 媒体上传中且不在流式输出中 (
mediaUploading && !streamingMessage) - 没有输入内容且不在流式输出中
1.4 关键状态变量
文件位置: /static/src/app/state.ts:23
taskInProgress: false, // 任务是否在进行中(用于保持输入区的"停止"状态)
文件位置: /static/src/stores/chat.ts:78
streamingMessage: false, // 是否正在流式输出消息
文件位置: /static/src/app/computed.ts:160-166
streamingUi() {
return this.streamingMessage || this.hasPendingToolActions();
},
composerBusy() {
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested;
}
2. 点击停止按钮的完整流程
2.1 前端事件处理
文件位置: /static/src/app/methods/message.ts:104-110
handleSendOrStop() {
if (this.composerBusy) {
this.stopTask();
} else {
this.sendMessage();
}
},
2.2 停止任务方法
文件位置: /static/src/app/methods/message.ts:254-282
async stopTask() {
const canStop = this.composerBusy && !this.stopRequested;
if (!canStop) {
return;
}
const shouldDropToolEvents = this.streamingUi;
this.stopRequested = true;
this.dropToolEvents = shouldDropToolEvents;
// 使用 REST API 取消任务
try {
const { useTaskStore } = await import('../../stores/task');
const taskStore = useTaskStore();
if (taskStore.currentTaskId) {
await taskStore.cancelTask();
debugLog('[Message] 任务已取消');
}
} catch (error) {
console.error('[Message] 取消任务失败:', error);
}
// 立即清理前端状态,避免出现"不可输入也不可停止"的卡死状态
this.clearPendingTools('user_stop');
this.streamingMessage = false;
this.taskInProgress = false;
this.forceUnlockMonitor('user_stop');
},
2.3 REST API 取消请求
文件位置: /static/src/stores/task.ts:185-214
async cancelTask() {
if (!this.currentTaskId) {
return;
}
try {
debugLog('[Task] 取消任务:', this.currentTaskId);
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('取消任务失败');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '取消任务失败');
}
this.taskStatus = 'canceled';
this.stopPolling();
debugLog('[Task] 任务已取消');
} catch (error) {
console.error('[Task] 取消任务失败:', error);
throw error;
}
},
2.4 后端接收停止请求
文件位置: /server/tasks.py:372-379
@tasks_bp.route("/api/tasks/<task_id>/cancel", methods=["POST"])
@api_login_required
def cancel_task_api(task_id: str):
username = get_current_username()
ok = task_manager.cancel_task(username, task_id)
if not ok:
return jsonify({"success": False, "error": "任务不存在"}), 404
return jsonify({"success": True})
2.5 设置停止标志
文件位置: /server/tasks.py:144-164
def cancel_task(self, username: str, task_id: str) -> bool:
rec = self.get_task(username, task_id)
if not rec:
return False
rec.stop_requested = True
# 标记停止标志;chat_flow 会检测 stop_flags
entry = stop_flags.get(task_id)
if not isinstance(entry, dict):
entry = {'stop': False, 'task': None, 'terminal': None}
stop_flags[task_id] = entry
entry['stop'] = True
# 注释掉直接取消任务,改为通过停止标志让任务内部处理
# try:
# if entry.get('task') and hasattr(entry['task'], "cancel"):
# entry['task'].cancel()
# except Exception:
# pass
with self._lock:
rec.status = "cancel_requested"
rec.updated_at = time.time()
return True
2.6 聊天流程检测停止标志
系统在多个关键位置检查停止标志:
位置1: /server/chat_flow_tool_loop.py:23-30 - 工具调用前检查
# 检查停止标志
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
debug_log("在工具调用过程中检测到停止状态")
# ... 取消工具调用
位置2: /server/chat_flow_tool_loop.py:229-236 - 工具执行过程中检查
check_count += 1
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
debug_log(f"[停止检测] 工具执行过程中检测到停止请求(检查次数:{check_count}),立即取消工具")
tool_task.cancel()
tool_cancelled = True
位置3: /server/chat_flow_stream_loop.py:46-53 - 流式输出过程中检查
# 检查停止标志
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
debug_log(f"检测到停止请求,中断流处理")
cancel_pending_tools(tool_calls_list=tool_calls, sender=sender, messages=messages)
sender('task_stopped', {...})
位置4: /server/chat_flow_task_support.py:147-154 - 命令执行过程中检查
while time.time() < deadline:
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
sender('task_stopped', {
'message': '命令执行被用户取消',
'reason': 'user_stop'
})
3. 运行状态的设置和清除
3.1 taskInProgress 状态管理
设置为 true 的时机:
- 发送消息时 (
/static/src/app/methods/message.ts:200)
this.taskInProgress = true;
- AI消息开始时 (
/static/src/app/methods/taskPolling.ts:128,138)
this.taskInProgress = true;
- 收到用户消息事件时 (
/static/src/app/methods/taskPolling.ts:566)
this.taskInProgress = true;
- 恢复任务状态时 (
/static/src/app/methods/taskPolling.ts:739,882)
this.taskInProgress = true;
设置为 false 的时机:
- 任务完成时 (
/static/src/app/methods/taskPolling.ts:538)
this.taskInProgress = false;
- 任务错误时 (
/static/src/app/methods/taskPolling.ts:588)
this.taskInProgress = false;
- 停止任务时 (
/static/src/app/methods/message.ts:280)
this.taskInProgress = false;
- 创建任务失败时 (
/static/src/app/methods/message.ts:218)
this.taskInProgress = false;
3.2 streamingMessage 状态管理
设置为 true 的时机:
- AI消息开始时 (
/static/src/app/methods/taskPolling.ts:130,141)
this.streamingMessage = true;
- 恢复任务状态时 (
/static/src/app/methods/taskPolling.ts:738,881)
this.streamingMessage = true;
设置为 false 的时机:
- 任务完成时 (
/static/src/app/methods/taskPolling.ts:532)
this.streamingMessage = false;
- 任务错误时 (
/static/src/app/methods/taskPolling.ts:587)
this.streamingMessage = false;
- 停止任务时 (
/static/src/app/methods/message.ts:279)
this.streamingMessage = false;
- 收到用户消息事件时 (
/static/src/app/methods/taskPolling.ts:567)
this.streamingMessage = false;
3.3 stopRequested 状态管理
设置为 true 的时机:
- 停止任务时 (
/static/src/app/methods/message.ts:261)
this.stopRequested = true;
设置为 false 的时机:
- AI消息开始时 (
/static/src/app/methods/taskPolling.ts:129,140)
this.stopRequested = false;
- 任务完成时 (
/static/src/app/methods/taskPolling.ts:533)
this.stopRequested = false;
- 任务错误时 (
/static/src/app/methods/taskPolling.ts:589)
this.stopRequested = false;
4. 状态转换图
[用户点击发送]
↓
[taskInProgress = true]
[streamingMessage = false]
↓
[创建任务 POST /api/tasks]
↓
[开始轮询 GET /api/tasks/{id}?from={offset}]
↓
[收到 ai_message_start 事件]
↓
[streamingMessage = true]
↓
[显示停止按钮]
↓
┌─────────────────────────────────┐
│ 用户点击停止按钮 │
│ ↓ │
│ [stopRequested = true] │
│ ↓ │
│ [POST /api/tasks/{id}/cancel] │
│ ↓ │
│ [stop_flags[task_id]['stop'] = True] │
│ ↓ │
│ [立即清理前端状态] │
│ [streamingMessage = false] │
│ [taskInProgress = false] │
│ [stopRequested = false] │
│ ↓ │
│ [显示发送按钮] │
└─────────────────────────────────┘
或
↓
[收到 task_complete 事件]
↓
[streamingMessage = false]
[taskInProgress = false]
[stopRequested = false]
↓
[停止轮询]
↓
[显示发送按钮]
5. 事件流程图
5.1 正常完成流程
前端 后端 任务线程
│ │ │
├─ sendMessage() │ │
├─ taskInProgress = true │ │
├─ POST /api/tasks ─────────>│ │
│ ├─ create_chat_task() │
│ ├─ thread.start() ──────────>│
│ │ ├─ run_chat_task_sync()
│<──── task_id ──────────────┤ │
├─ startPolling() │ │
│ │ │
├─ GET /api/tasks/{id} ─────>│ │
│<──── events ────────────────┤ │
├─ handleTaskEvent() │ │
├─ streamingMessage = true │ │
│ │ │
│ (轮询继续...) │ │
│ │ │
│ │ ├─ 任务完成
│ │<──── task_complete ────────┤
├─ GET /api/tasks/{id} ─────>│ │
│<──── task_complete ─────────┤ │
├─ handleTaskComplete() │ │
├─ streamingMessage = false │ │
├─ taskInProgress = false │ │
├─ stopPolling() │ │
5.2 用户停止流程
前端 后端 任务线程
│ │ │
├─ (任务运行中) │ │
├─ streamingMessage = true │ │
│ │ │
├─ stopTask() │ │
├─ stopRequested = true │ │
├─ POST /api/tasks/{id}/cancel ─>│ │
│ ├─ cancel_task() │
│ ├─ stop_flags[task_id]['stop'] = True
│<──── success ──────────────┤ │
├─ streamingMessage = false │ │
├─ taskInProgress = false │ │
├─ stopRequested = false │ │
│ │ │
│ │ ├─ get_stop_flag()
│ │ ├─ 检测到停止标志
│ │ ├─ 取消工具调用
│ │ ├─ sender('task_stopped')
│ │<──── task_stopped ─────────┤
│ ├─ status = 'canceled' │
│ ├─ stop_flags.pop(task_id) │
6. 停止后的UI状态变化
6.1 输入框状态
文件位置: /static/src/components/input/InputComposer.vue:41
:disabled="!isConnected || streamingMessage || inputLocked"
- 停止前:
streamingMessage = true→ 输入框禁用 - 停止后:
streamingMessage = false→ 输入框启用
6.2 停止按钮变为发送按钮
文件位置: /static/src/components/input/InputComposer.vue:59-60
<span v-if="streamingMessage" class="stop-icon"></span>
<span v-else class="send-icon"></span>
- 停止前: 显示停止图标
- 停止后: 显示发送图标
6.3 工具块状态
停止时会调用 clearPendingTools('user_stop'),清理所有待处理的工具调用。
7. 已知问题和Bug
7.1 停止按钮无法点击
问题描述: 在某些情况下,停止按钮显示但无法点击。
可能原因:
composerBusy计算错误,导致canStop条件不满足stopRequested已经为 true,阻止重复停止- 按钮被其他元素遮挡(CSS z-index 问题)
相关代码: /static/src/app/methods/message.ts:255-258
const canStop = this.composerBusy && !this.stopRequested;
if (!canStop) {
return;
}
7.2 停止后任务仍在运行
问题描述: 点击停止按钮后,前端状态已清除,但后端任务仍在执行。
可能原因:
- 后端停止标志检查不够频繁
- 某些长时间运行的工具没有检查停止标志
- 停止标志设置和检查之间存在竞态条件
相关代码:
/server/chat_flow_tool_loop.py:229-236- 工具执行过程中每 0.1 秒检查一次/server/chat_flow_task_support.py:147-154- 命令执行过程中检查
7.3 状态不同步
问题描述: 前端和后端的任务状态不一致。
可能原因:
- 轮询间隔过长(150ms),导致状态更新延迟
- 事件处理顺序错误
- 多个状态变量(
taskInProgress,streamingMessage,stopRequested)更新不原子
相关代码: /static/src/stores/task.ts:154-157
// 设置定时轮询(150ms 间隔,接近流式输出效果)
this.pollingInterval = window.setInterval(() => {
this.pollTaskEvents(handler);
}, 150);
7.4 页面刷新后状态丢失
问题描述: 页面刷新后,运行中的任务状态无法正确恢复。
解决方案: 已实现 restoreTaskState() 方法,在页面加载时查找运行中的任务并恢复状态。
相关代码: /static/src/app/methods/taskPolling.ts:637-921
async restoreTaskState() {
// 查找运行中的任务
const runningTask = await taskStore.loadRunningTask(this.currentConversationId);
if (!runningTask) {
return;
}
// 恢复状态
this.streamingMessage = true;
this.taskInProgress = true;
// 启动轮询
taskStore.startPolling((event: any) => {
this.handleTaskEvent(event);
});
}
7.5 立即清理导致的问题
问题描述: 停止任务时立即清理前端状态,可能导致后端仍在发送事件,前端无法正确处理。
相关代码: /static/src/app/methods/message.ts:278-281
// 立即清理前端状态,避免出现"不可输入也不可停止"的卡死状态
this.clearPendingTools('user_stop');
this.streamingMessage = false;
this.taskInProgress = false;
this.forceUnlockMonitor('user_stop');
潜在风险:
- 后端可能仍在发送
tool_start,text_chunk等事件 - 前端已清理状态,这些事件会被忽略或导致错误
- 可能出现 UI 不一致的情况
7.6 dropToolEvents 机制
问题描述: dropToolEvents 标志用于在停止后忽略工具事件,但可能导致状态不一致。
相关代码:
/static/src/app/methods/message.ts:262- 设置标志/static/src/app/methods/taskPolling.ts:294-296- 检查标志
if (this.dropToolEvents) {
return;
}
潜在问题:
- 如果后端发送了
task_complete事件,但前端已经dropToolEvents,可能导致状态不清理 dropToolEvents何时清除不明确
7.7 composerBusy 计算复杂
问题描述: composerBusy 依赖多个状态变量,计算逻辑复杂,容易出错。
相关代码: /static/src/app/computed.ts:163-166
composerBusy() {
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested;
}
问题:
streamingUi又依赖streamingMessage和hasPendingToolActions()- 多层依赖导致状态更新顺序敏感
- 难以调试和维护
8. 改进建议
8.1 统一状态管理
建议使用单一的状态机模式,而不是多个独立的布尔变量:
enum TaskState {
IDLE = 'idle',
CREATING = 'creating',
RUNNING = 'running',
STREAMING = 'streaming',
STOPPING = 'stopping',
STOPPED = 'stopped',
COMPLETED = 'completed',
FAILED = 'failed'
}
state: {
taskState: TaskState.IDLE
}
8.2 增加停止确认机制
在前端立即清理状态后,等待后端确认停止成功:
async stopTask() {
this.stopRequested = true;
// 发送停止请求
await taskStore.cancelTask();
// 等待后端确认(轮询直到收到 task_stopped 或 task_complete)
await this.waitForStopConfirmation();
// 清理前端状态
this.streamingMessage = false;
this.taskInProgress = false;
this.stopRequested = false;
}
8.3 增加停止超时机制
如果后端在一定时间内没有响应停止请求,强制清理前端状态:
const STOP_TIMEOUT = 5000; // 5秒
setTimeout(() => {
if (this.stopRequested) {
// 强制清理
this.streamingMessage = false;
this.taskInProgress = false;
this.stopRequested = false;
}
}, STOP_TIMEOUT);
8.4 改进停止标志检查频率
在长时间运行的操作中,增加停止标志检查频率:
# 当前: 每 0.1 秒检查一次
await asyncio.sleep(0.1)
# 建议: 每 0.05 秒检查一次
await asyncio.sleep(0.05)
8.5 添加停止日志
在所有停止相关的代码路径中添加详细日志,便于调试:
debugLog('[Stop] 用户点击停止按钮', {
composerBusy: this.composerBusy,
stopRequested: this.stopRequested,
taskInProgress: this.taskInProgress,
streamingMessage: this.streamingMessage,
currentTaskId: taskStore.currentTaskId
});
8.6 前端状态恢复优化
页面刷新后的状态恢复逻辑复杂,建议:
- 简化恢复逻辑,只恢复必要的状态
- 添加恢复失败的降级处理
- 提供手动刷新按钮,让用户可以重新加载状态
9. 总结
停止按钮和运行状态机制是系统中最复杂的部分之一,涉及前端多个状态变量、REST API 轮询、后端停止标志检查等多个环节。主要问题包括:
- 状态管理复杂: 多个独立的布尔变量(
taskInProgress,streamingMessage,stopRequested)难以维护 - 停止响应延迟: 后端停止标志检查不够频繁,导致停止响应慢
- 状态同步问题: 前端立即清理状态,但后端可能仍在运行,导致状态不一致
- 页面刷新恢复: 状态恢复逻辑复杂,容易出错
建议采用状态机模式统一管理任务状态,增加停止确认机制,改进停止标志检查频率,并添加详细的调试日志。