agent-Specialization/docs/analysis-stop-button.md

22 KiB
Raw Permalink Blame History

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 禁用条件

按钮被禁用的情况:

  1. 未连接服务器 (!isConnected)
  2. 输入被锁定且不在流式输出中 (inputLocked && !streamingMessage)
  3. 媒体上传中且不在流式输出中 (mediaUploading && !streamingMessage)
  4. 没有输入内容且不在流式输出中

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 的时机:

  1. 发送消息时 (/static/src/app/methods/message.ts:200)
this.taskInProgress = true;
  1. AI消息开始时 (/static/src/app/methods/taskPolling.ts:128,138)
this.taskInProgress = true;
  1. 收到用户消息事件时 (/static/src/app/methods/taskPolling.ts:566)
this.taskInProgress = true;
  1. 恢复任务状态时 (/static/src/app/methods/taskPolling.ts:739,882)
this.taskInProgress = true;

设置为 false 的时机:

  1. 任务完成时 (/static/src/app/methods/taskPolling.ts:538)
this.taskInProgress = false;
  1. 任务错误时 (/static/src/app/methods/taskPolling.ts:588)
this.taskInProgress = false;
  1. 停止任务时 (/static/src/app/methods/message.ts:280)
this.taskInProgress = false;
  1. 创建任务失败时 (/static/src/app/methods/message.ts:218)
this.taskInProgress = false;

3.2 streamingMessage 状态管理

设置为 true 的时机:

  1. AI消息开始时 (/static/src/app/methods/taskPolling.ts:130,141)
this.streamingMessage = true;
  1. 恢复任务状态时 (/static/src/app/methods/taskPolling.ts:738,881)
this.streamingMessage = true;

设置为 false 的时机:

  1. 任务完成时 (/static/src/app/methods/taskPolling.ts:532)
this.streamingMessage = false;
  1. 任务错误时 (/static/src/app/methods/taskPolling.ts:587)
this.streamingMessage = false;
  1. 停止任务时 (/static/src/app/methods/message.ts:279)
this.streamingMessage = false;
  1. 收到用户消息事件时 (/static/src/app/methods/taskPolling.ts:567)
this.streamingMessage = false;

3.3 stopRequested 状态管理

设置为 true 的时机:

  1. 停止任务时 (/static/src/app/methods/message.ts:261)
this.stopRequested = true;

设置为 false 的时机:

  1. AI消息开始时 (/static/src/app/methods/taskPolling.ts:129,140)
this.stopRequested = false;
  1. 任务完成时 (/static/src/app/methods/taskPolling.ts:533)
this.stopRequested = false;
  1. 任务错误时 (/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 停止按钮无法点击

问题描述: 在某些情况下,停止按钮显示但无法点击。

可能原因:

  1. composerBusy 计算错误,导致 canStop 条件不满足
  2. stopRequested 已经为 true阻止重复停止
  3. 按钮被其他元素遮挡CSS z-index 问题)

相关代码: /static/src/app/methods/message.ts:255-258

const canStop = this.composerBusy && !this.stopRequested;
if (!canStop) {
    return;
}

7.2 停止后任务仍在运行

问题描述: 点击停止按钮后,前端状态已清除,但后端任务仍在执行。

可能原因:

  1. 后端停止标志检查不够频繁
  2. 某些长时间运行的工具没有检查停止标志
  3. 停止标志设置和检查之间存在竞态条件

相关代码:

  • /server/chat_flow_tool_loop.py:229-236 - 工具执行过程中每 0.1 秒检查一次
  • /server/chat_flow_task_support.py:147-154 - 命令执行过程中检查

7.3 状态不同步

问题描述: 前端和后端的任务状态不一致。

可能原因:

  1. 轮询间隔过长150ms导致状态更新延迟
  2. 事件处理顺序错误
  3. 多个状态变量(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 又依赖 streamingMessagehasPendingToolActions()
  • 多层依赖导致状态更新顺序敏感
  • 难以调试和维护

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 前端状态恢复优化

页面刷新后的状态恢复逻辑复杂,建议:

  1. 简化恢复逻辑,只恢复必要的状态
  2. 添加恢复失败的降级处理
  3. 提供手动刷新按钮,让用户可以重新加载状态

9. 总结

停止按钮和运行状态机制是系统中最复杂的部分之一涉及前端多个状态变量、REST API 轮询、后端停止标志检查等多个环节。主要问题包括:

  1. 状态管理复杂: 多个独立的布尔变量(taskInProgress, streamingMessage, stopRequested)难以维护
  2. 停止响应延迟: 后端停止标志检查不够频繁,导致停止响应慢
  3. 状态同步问题: 前端立即清理状态,但后端可能仍在运行,导致状态不一致
  4. 页面刷新恢复: 状态恢复逻辑复杂,容易出错

建议采用状态机模式统一管理任务状态,增加停止确认机制,改进停止标志检查频率,并添加详细的调试日志。