refactor(stop-button): 拆分停止按钮和后台任务控制
停止按钮现在只停主智能体,与后台任务无关。 - 后端 cancel_task 简化:移除两段式判断、force_stop参数、is_ma_mode分支 - 删除 _force_stop_multi_agent 方法 - 新增 /api/sub_agents/stop_all 接口(mode=terminate/soft_stop) - 前端 stopTask 移除 forceStop 概念,只取消主智能体 - 取消期间不再二次点击终结子智能体 子智能体停止按钮: - InputComposer state bar 加'x个子智能体运行中'按钮 - 独立浮层显示,不依赖 git 栏(参考 askuser 方案) - 点击传统模式弹窗终结;多智能体弹窗软停止 - App.vue 新增 handleStopAllSubAgents 调 ui.confirmDialog 后台指令停止按钮: - LeftPanel 后台指令标签右边加停止按钮 - 仅当 panelMode=backgroundCommands 且存在运行中后台指令时显示 - 调 backgroundCommandStore.stopAllCommands 无二次确认 showStopIcon 只看主对话 streaming 状态: - 后台任务在跑但主对话空闲时显示发送按钮 - 多智能体 / 传统模式统一处理 子智能体 store 加 stopAllAgents(mode) action 调新 API useLegacySocket task_stopped handler 简化: - 移除 multi_agent_reloaded 重载 - 移除'再次按下停止按钮以结束后台任务'提示 - 保留 has_running_multi_agent/sub_agents 用于 taskInProgress 判定 清理调试日志:[TaskCancel][stop_debug], [STOP_DEBUG] 全部去掉 保留 ma_debug 框架用于多智能体调试
This commit is contained in:
parent
30bfb79cbd
commit
ae70e4902e
@ -393,7 +393,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
让子智能体在当前工具完成后进入 idle 状态,保留上下文。
|
让子智能体在当前工具完成后进入 idle 状态,保留上下文。
|
||||||
返回实际发出软停止信号的子智能体数量。
|
返回实际发出软停止信号的子智能体数量。
|
||||||
"""
|
"""
|
||||||
debug_log(f"[TaskCancel][stop_debug] soft_stop_all_agents 进入,conv={conversation_id}")
|
ma_debug("soft_stop_all_agents_enter", conversation_id=conversation_id)
|
||||||
count = 0
|
count = 0
|
||||||
matched = 0
|
matched = 0
|
||||||
skipped_terminal = 0
|
skipped_terminal = 0
|
||||||
@ -403,27 +403,35 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
continue
|
continue
|
||||||
matched += 1
|
matched += 1
|
||||||
status = task_info.get("status")
|
status = task_info.get("status")
|
||||||
debug_log(f"[TaskCancel][stop_debug] soft_stop 遍历: task_id={task_id}, status={status}")
|
ma_debug(
|
||||||
|
"soft_stop_iter",
|
||||||
|
task_id=task_id,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
if status in TERMINAL_STATUSES.union({"terminated", "idle"}):
|
if status in TERMINAL_STATUSES.union({"terminated", "idle"}):
|
||||||
skipped_terminal += 1
|
skipped_terminal += 1
|
||||||
debug_log(f"[TaskCancel][stop_debug] soft_stop 跳过 (terminal/idle): task_id={task_id}, status={status}")
|
|
||||||
continue
|
continue
|
||||||
agent_id = task_info.get("agent_id")
|
agent_id = task_info.get("agent_id")
|
||||||
if agent_id is None:
|
if agent_id is None:
|
||||||
continue
|
continue
|
||||||
# 從 _sub_agent_instances 查找 SubAgentTask 实例
|
# 從 _sub_agent_instances 查找 SubAgentTask 实例
|
||||||
sub_agent_task = self._sub_agent_instances.get(agent_id)
|
sub_agent_task = self._sub_agent_instances.get(agent_id)
|
||||||
debug_log(f"[TaskCancel][stop_debug] soft_stop _sub_agent_instances 查找: agent_id={agent_id}, found={bool(sub_agent_task)}")
|
|
||||||
if sub_agent_task and hasattr(sub_agent_task, "request_soft_stop"):
|
if sub_agent_task and hasattr(sub_agent_task, "request_soft_stop"):
|
||||||
try:
|
try:
|
||||||
sub_agent_task.request_soft_stop()
|
sub_agent_task.request_soft_stop()
|
||||||
count += 1
|
count += 1
|
||||||
debug_log(f"[TaskCancel][stop_debug] soft_stop 已请求: task_id={task_id}, agent_id={agent_id}")
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
debug_log(f"[TaskCancel][stop_debug] soft_stop 请求失败: task_id={task_id}, error={exc}")
|
ma_debug("soft_stop_failed", task_id=task_id, error=str(exc))
|
||||||
else:
|
else:
|
||||||
skipped_no_instance += 1
|
skipped_no_instance += 1
|
||||||
debug_log(f"[TaskCancel][stop_debug] soft_stop_all_agents 结束: matched={matched}, count={count}, skipped_terminal={skipped_terminal}, skipped_no_instance={skipped_no_instance}")
|
ma_debug(
|
||||||
|
"soft_stop_all_agents_done",
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
matched=matched,
|
||||||
|
count=count,
|
||||||
|
skipped_terminal=skipped_terminal,
|
||||||
|
skipped_no_instance=skipped_no_instance,
|
||||||
|
)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def terminate_sub_agent(
|
def terminate_sub_agent(
|
||||||
|
|||||||
@ -1069,32 +1069,32 @@ class SubAgentTask:
|
|||||||
- request_soft_stop(): 设标志 + cancel asyncio.Task → CancelledError
|
- request_soft_stop(): 设标志 + cancel asyncio.Task → CancelledError
|
||||||
→ 捕获后重载对话文件(丢弃半成品)→ mark idle → 保留实例
|
→ 捕获后重载对话文件(丢弃半成品)→ mark idle → 保留实例
|
||||||
"""
|
"""
|
||||||
from server.utils_common import debug_log as _dbg
|
|
||||||
_dbg(f"[TaskCancel][stop_debug] request_soft_stop 进入: task_id={self.task_id}, agent_id={self.agent_id}, _soft_stop 前={self._soft_stop}, _task={self._task}, _task.done()={self._task.done() if self._task else 'n/a'}")
|
|
||||||
self._soft_stop = True
|
self._soft_stop = True
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_request_soft_stop",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
has_task=bool(self._task),
|
||||||
|
task_done=self._task.done() if self._task else None,
|
||||||
|
)
|
||||||
# 取消子智能体正在阻塞等待的 ask_master/ask_other 未来,解除阻塞
|
# 取消子智能体正在阻塞等待的 ask_master/ask_other 未来,解除阻塞
|
||||||
if self.multi_agent_state and self.agent_id is not None:
|
if self.multi_agent_state and self.agent_id is not None:
|
||||||
try:
|
try:
|
||||||
self.multi_agent_state.cancel_pending_question_for_agent(self.agent_id)
|
self.multi_agent_state.cancel_pending_question_for_agent(self.agent_id)
|
||||||
_dbg(f"[TaskCancel][stop_debug] request_soft_stop cancel_pending_question_for_agent 已调 agent_id={self.agent_id}")
|
except Exception:
|
||||||
except Exception as exc:
|
pass
|
||||||
_dbg(f"[TaskCancel][stop_debug] request_soft_stop cancel_pending_question_for_agent 失败: {exc}")
|
|
||||||
# 直接 cancel asyncio.Task 以中断正在执行的工具调用/API 调用
|
# 直接 cancel asyncio.Task 以中断正在执行的工具调用/API 调用
|
||||||
# CancelledError 会在 run() 中被捕获,检测到 _soft_stop 后走恢复路径
|
# CancelledError 会在 run() 中被捕获,检测到 _soft_stop 后走恢复路径
|
||||||
if self._task and not self._task.done():
|
if self._task and not self._task.done():
|
||||||
try:
|
try:
|
||||||
loop = self._task.get_loop()
|
loop = self._task.get_loop()
|
||||||
loop.call_soon_threadsafe(self._task.cancel)
|
loop.call_soon_threadsafe(self._task.cancel)
|
||||||
_dbg(f"[TaskCancel][stop_debug] request_soft_stop call_soon_threadsafe(cancel) 已投递 task_id={self.task_id}")
|
except Exception:
|
||||||
except Exception as exc:
|
|
||||||
_dbg(f"[TaskCancel][stop_debug] request_soft_stop call_soon_threadsafe 失败: {exc}")
|
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
else:
|
|
||||||
_dbg(f"[TaskCancel][stop_debug] request_soft_stop 无可用 _task,无法 cancel (done or None): task_id={self.task_id}")
|
|
||||||
|
|
||||||
# 额外保险:如果 _task 为 None 但子智能体状态是 running,直接设 idle 状态
|
# 额外保险:如果 _task 为 None 但子智能体状态是 running,直接设 idle 状态
|
||||||
if self._task is None and self.multi_agent_state and self.agent_id is not None:
|
if self._task is None and self.multi_agent_state and self.agent_id is not None:
|
||||||
_dbg(f"[TaskCancel][stop_debug] request_soft_stop _task 为 None,直接 mark idle: task_id={self.task_id}")
|
ma_debug("sub_agent_soft_stop_no_task_mark_idle", task_id=self.task_id, agent_id=self.agent_id)
|
||||||
self._soft_stop = False
|
self._soft_stop = False
|
||||||
self._idle = True
|
self._idle = True
|
||||||
self.multi_agent_state.mark_status(self.agent_id, "idle")
|
self.multi_agent_state.mark_status(self.agent_id, "idle")
|
||||||
|
|||||||
@ -1808,6 +1808,55 @@ def get_sub_agent_activity(task_id: str, terminal: WebTerminal, workspace: UserW
|
|||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@conversation_bp.route('/api/sub_agents/stop_all', methods=['POST'])
|
||||||
|
@api_login_required
|
||||||
|
@with_terminal
|
||||||
|
def stop_all_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
|
"""批量停止当前会话的所有非终态子智能体。
|
||||||
|
|
||||||
|
body: {"mode": "terminate" | "soft_stop"}
|
||||||
|
- mode=terminate: 传统模式终结子智能体(重、不保留实例)
|
||||||
|
- mode=soft_stop: 多智能体模式软停止(轻、保留实例可被后续唤醒)
|
||||||
|
"""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
mode = str(data.get('mode') or 'terminate').strip().lower()
|
||||||
|
if mode not in {'terminate', 'soft_stop'}:
|
||||||
|
return jsonify({"success": False, "error": "mode 只支持 terminate/soft_stop"}), 400
|
||||||
|
manager = getattr(terminal, 'sub_agent_manager', None)
|
||||||
|
if not manager:
|
||||||
|
return jsonify({"success": False, "error": "子智能体管理器不可用"}), 404
|
||||||
|
conversation_id = terminal.context_manager.current_conversation_id
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
manager._load_state()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
manager.reconcile_task_states(conversation_id=conversation_id)
|
||||||
|
stopped_count = 0
|
||||||
|
if mode == 'soft_stop':
|
||||||
|
stopped_count = manager.soft_stop_all_agents(conversation_id)
|
||||||
|
debug_log(f"[StopAllSubAgents] soft_stop 会话={conversation_id}, 计数={stopped_count}")
|
||||||
|
else:
|
||||||
|
# terminate: 遍历所有非终态子智能体逐个终结
|
||||||
|
from modules.sub_agent.state import TERMINAL_STATUSES
|
||||||
|
for task_info in list(manager.tasks.values()):
|
||||||
|
if task_info.get('conversation_id') != conversation_id:
|
||||||
|
continue
|
||||||
|
status = task_info.get('status')
|
||||||
|
if status in TERMINAL_STATUSES.union({"terminated"}):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
tid = task_info.get('task_id')
|
||||||
|
manager.terminate_sub_agent(task_id=tid)
|
||||||
|
stopped_count += 1
|
||||||
|
debug_log(f"[StopAllSubAgents] terminated task_id={tid}")
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[StopAllSubAgents] 终止失败 tid={task_info.get('task_id')}, err={exc}")
|
||||||
|
return jsonify({"success": True, "data": {"stopped_count": stopped_count, "mode": mode}})
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
@conversation_bp.route('/api/sub_agents/<task_id>/terminate', methods=['POST'])
|
@conversation_bp.route('/api/sub_agents/<task_id>/terminate', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@with_terminal
|
@with_terminal
|
||||||
|
|||||||
@ -205,9 +205,7 @@ def cancel_task_api(task_id: str):
|
|||||||
rec = task_manager.get_task(username, task_id)
|
rec = task_manager.get_task(username, task_id)
|
||||||
if not rec:
|
if not rec:
|
||||||
return jsonify({"success": False, "error": "任务不存在"}), 404
|
return jsonify({"success": False, "error": "任务不存在"}), 404
|
||||||
force_stop = bool(request.json.get("force_stop", False)) if request.is_json else False
|
ok = task_manager.cancel_task(username, task_id)
|
||||||
debug_log(f"[TaskCancel][stop_debug] cancel_task_api 收到请求: task_id={task_id}, force_stop={force_stop}, body={request.json if request.is_json else None}")
|
|
||||||
ok = task_manager.cancel_task(username, task_id, force_stop=force_stop)
|
|
||||||
# 用户取消任务时,一并停止该工作区的目标模式,避免后续新对话继承旧目标。
|
# 用户取消任务时,一并停止该工作区的目标模式,避免后续新对话继承旧目标。
|
||||||
try:
|
try:
|
||||||
if ok and rec.workspace_id:
|
if ok and rec.workspace_id:
|
||||||
|
|||||||
@ -211,19 +211,21 @@ class TaskManager:
|
|||||||
if rec.username == username and (workspace_id is None or rec.workspace_id == workspace_id)
|
if rec.username == username and (workspace_id is None or rec.workspace_id == workspace_id)
|
||||||
]
|
]
|
||||||
|
|
||||||
def cancel_task(self, username: str, task_id: str, force_stop: bool = False) -> bool:
|
def cancel_task(self, username: str, task_id: str) -> bool:
|
||||||
debug_log(f"[TaskCancel][stop_debug] === cancel_task 被调 === task_id={task_id}, force_stop={force_stop}")
|
"""取消主智能体任务。只停主智能体,不触碰后台任务。
|
||||||
|
|
||||||
|
后台任务(子智能体、后台指令)的终止/暂停由独立的 API 处理,
|
||||||
|
参见 /api/sub_agents/stop_all 和 /api/background_commands/stop_all。
|
||||||
|
"""
|
||||||
rec = self.get_task(username, task_id)
|
rec = self.get_task(username, task_id)
|
||||||
if not rec:
|
if not rec:
|
||||||
debug_log(f"[TaskCancel][stop_debug] cancel_task 找不到任务: task_id={task_id}")
|
debug_log(f"[TaskCancel] cancel_task 找不到任务: task_id={task_id}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 立即快照进入时的状态,后续所有两段式判断都用它。
|
task_id = rec.task_id
|
||||||
# _run_chat_task finally 会在硬取消后并发修改 rec.status,
|
|
||||||
# 如果后面再读会被误判为第二下。
|
|
||||||
status_at_entry = rec.status
|
status_at_entry = rec.status
|
||||||
|
|
||||||
# 先取终端引用,后续多智能体模式与两段式都要用
|
# 取终端引用(仅用于停止目标模式等副作用)
|
||||||
entry = stop_flags.get(task_id)
|
entry = stop_flags.get(task_id)
|
||||||
terminal = None
|
terminal = None
|
||||||
if isinstance(entry, dict):
|
if isinstance(entry, dict):
|
||||||
@ -231,48 +233,32 @@ class TaskManager:
|
|||||||
if not terminal and rec.workspace_id:
|
if not terminal and rec.workspace_id:
|
||||||
try:
|
try:
|
||||||
terminal, _ = get_user_resources(username, rec.workspace_id)
|
terminal, _ = get_user_resources(username, rec.workspace_id)
|
||||||
debug_log(f"[TaskCancel][stop_debug] fallback get_user_resources 获取 terminal OK")
|
except Exception:
|
||||||
except Exception as exc:
|
pass
|
||||||
debug_log(f"[TaskCancel][stop_debug] fallback get_user_resources 失败: {exc}")
|
|
||||||
|
|
||||||
is_ma_mode = bool(getattr(terminal, 'multi_agent_mode', False)) if terminal else False
|
|
||||||
debug_log(
|
debug_log(
|
||||||
f"[TaskCancel][stop_debug] 入口状态: task_id={task_id}, status={status_at_entry}, "
|
f"[TaskCancel] 入口: task_id={task_id}, status={status_at_entry}, "
|
||||||
f"conversation_id={rec.conversation_id}, workspace_id={rec.workspace_id}, "
|
f"conv={rec.conversation_id}"
|
||||||
f"force_stop={force_stop}, multi_agent_mode={is_ma_mode}, terminal_present={bool(terminal)}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 多智能体模式 force_stop:跳过两段式,直接走强制停止路径
|
|
||||||
# 适用场景:第一下已停主智能体,第二下停所有子智能体并重载对话
|
|
||||||
if force_stop:
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] force_stop=true → 走 _force_stop_multi_agent 分支")
|
|
||||||
return self._force_stop_multi_agent(rec, entry, terminal, username)
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] force_stop=false → 走两段式分支(可诱导出终端)")
|
|
||||||
|
|
||||||
# 1. 硬取消主 asyncio task(如果引用还在)
|
# 1. 硬取消主 asyncio task(如果引用还在)
|
||||||
if isinstance(entry, dict):
|
if isinstance(entry, dict):
|
||||||
loop = entry.get('loop')
|
loop = entry.get('loop')
|
||||||
task = entry.get('task')
|
task = entry.get('task')
|
||||||
debug_log(
|
|
||||||
f"[TaskCancel][stop_debug] stop_flags entry: loop_exists={loop is not None}, "
|
|
||||||
f"task_exists={task is not None}, task_done={task.done() if task else 'n/a'}"
|
|
||||||
)
|
|
||||||
if loop and task and not task.done():
|
if loop and task and not task.done():
|
||||||
try:
|
try:
|
||||||
loop.call_soon_threadsafe(task.cancel)
|
loop.call_soon_threadsafe(task.cancel)
|
||||||
debug_log(f"[TaskCancel][stop_debug] 已投递硬取消: task_id={task_id}")
|
debug_log(f"[TaskCancel] 已投递硬取消: task_id={task_id}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
debug_log(f"[TaskCancel][stop_debug] 硬取消失败: task_id={task_id}, error={exc}")
|
debug_log(f"[TaskCancel] 硬取消失败: task_id={task_id}, error={exc}")
|
||||||
entry['stop'] = True
|
entry['stop'] = True
|
||||||
else:
|
else:
|
||||||
debug_log(f"[TaskCancel][stop_debug] stop_flags 无 entry,设软标志: task_id={task_id}")
|
|
||||||
stop_flags[task_id] = {'stop': True, 'task': None, 'terminal': None, 'loop': None}
|
stop_flags[task_id] = {'stop': True, 'task': None, 'terminal': None, 'loop': None}
|
||||||
entry = stop_flags[task_id]
|
entry = stop_flags[task_id]
|
||||||
|
|
||||||
rec.stop_requested = True
|
rec.stop_requested = True
|
||||||
|
|
||||||
# 2. 停止目标模式
|
# 2. 停止目标模式
|
||||||
workspace = None
|
|
||||||
if rec.workspace_id:
|
if rec.workspace_id:
|
||||||
try:
|
try:
|
||||||
_, workspace = get_user_resources(username, rec.workspace_id)
|
_, workspace = get_user_resources(username, rec.workspace_id)
|
||||||
@ -289,204 +275,20 @@ class TaskManager:
|
|||||||
rec.runtime_guidance_queue = []
|
rec.runtime_guidance_queue = []
|
||||||
rec.updated_at = time.time()
|
rec.updated_at = time.time()
|
||||||
|
|
||||||
# 4. 两段式停止:严格按进入时的 rec.status 判断(快照),不依赖 task.done() 的异步时机。
|
# 4. 标记为 cancel_requested。
|
||||||
# 原因:投递硬取消后 _run_chat_task finally 可能并发把 status 改成 cancel_requested,
|
# _run_chat_task finally 会并发把 status 改为 stopped 并发 task_stopped 事件。
|
||||||
# 若此时再读 rec.status 会误判为第二下,导致第一下就误杀后台。
|
# 此处仅做软标记,不强制覆盖 finally 即将设的终态。
|
||||||
# - 第一下(running/pending):只硬取消主智能体并置为 cancel_requested,保留后台任务。
|
|
||||||
# - 第二下(cancel_requested/stopped/canceled):清理后台任务并置为 stopped。
|
|
||||||
# - 额外保护:极短时间内(<500ms)的重复 cancel 请求视为抖动,不执行第二下。
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
is_first_press = status_at_entry in {"running", "pending"}
|
|
||||||
|
|
||||||
if not terminal and rec.workspace_id:
|
|
||||||
try:
|
|
||||||
terminal, _ = get_user_resources(username, rec.workspace_id)
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel] 取消任务时重新获取 terminal 失败: {exc}")
|
|
||||||
|
|
||||||
if is_first_press:
|
|
||||||
# 第一下:主智能体仍在跑,只请求取消,保留后台任务。
|
|
||||||
# 注意 1:不要清空 rec.events,否则 _run_chat_task finally 发送的
|
|
||||||
# task_stopped 事件会被丢弃,前端无法感知并弹提示。
|
|
||||||
# 注意 2:_run_chat_task finally 可能并发先执行并把 status 设为 stopped,
|
|
||||||
# 此时不要再覆盖回 cancel_requested。
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] 第一下分支:ma_mode={is_ma_mode}, 仅取消主智能体")
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if rec.status in {"running", "pending"}:
|
if rec.status in {"running", "pending"}:
|
||||||
rec.status = "cancel_requested"
|
rec.status = "cancel_requested"
|
||||||
rec.updated_at = now
|
rec.updated_at = now
|
||||||
rec.last_cancel_at = now
|
rec.last_cancel_at = now
|
||||||
debug_log(
|
debug_log(
|
||||||
f"[TaskCancel] 第一下:请求取消智能体,保留后台任务: "
|
f"[TaskCancel] 已取消主智能体: task_id={task_id}, "
|
||||||
f"task_id={task_id}, status_at_entry={status_at_entry}, "
|
f"status_at_entry={status_at_entry}, current_status={rec.status}"
|
||||||
f"current_status={rec.status}"
|
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] 第二下分支进入,status_at_entry={status_at_entry}, is_ma_mode={is_ma_mode}")
|
|
||||||
|
|
||||||
# 第二下:清理后台任务(过滤 500ms 内的重复请求)
|
|
||||||
if rec.last_cancel_at is not None and (now - rec.last_cancel_at) < 0.5:
|
|
||||||
debug_log(
|
|
||||||
f"[TaskCancel][stop_debug] 500ms 内重复取消,忽略: task_id={task_id}, "
|
|
||||||
f"elapsed={now - rec.last_cancel_at:.3f}s"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
is_ma_mode = bool(getattr(terminal, "multi_agent_mode", False))
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] 准备清理后台任务,is_ma_mode={is_ma_mode}")
|
|
||||||
|
|
||||||
if is_ma_mode:
|
|
||||||
# 多智能体模式:第二下不应走 terminate 路径!
|
|
||||||
# 应改为软停止所有子智能体,与 force_stop 一致。
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] 多智能体模式第二下分支,转走软停止路径(不是 terminate)")
|
|
||||||
soft_stop_count = 0
|
|
||||||
if terminal:
|
|
||||||
sub_agent_manager = getattr(terminal, 'sub_agent_manager', None)
|
|
||||||
if sub_agent_manager:
|
|
||||||
try:
|
|
||||||
sub_agent_manager.reconcile_task_states(conversation_id=rec.conversation_id)
|
|
||||||
soft_stop_count = sub_agent_manager.soft_stop_all_agents(rec.conversation_id)
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] 软停止 {soft_stop_count} 个子智能体已发出")
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] soft_stop_all_agents 失败: {exc}")
|
|
||||||
# 清空主智能体多智能体运行态(与旧逻辑一致)
|
|
||||||
if terminal and rec.conversation_id:
|
|
||||||
try:
|
|
||||||
ma_state = terminal.sub_agent_manager.get_multi_agent_state(rec.conversation_id)
|
|
||||||
if ma_state:
|
|
||||||
ma_state.clear()
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] 多智能体状态已清空: conv={rec.conversation_id}")
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] 清空多智能体状态失败: {exc}")
|
|
||||||
has_running_background = False
|
|
||||||
else:
|
|
||||||
# 传统模式:第二下继续走 terminate 路径
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] 传统模式第二下分支,继续 _cleanup_background_tasks terminate")
|
|
||||||
has_running_background = self._cleanup_background_tasks(rec, terminal)
|
|
||||||
|
|
||||||
with self._lock:
|
|
||||||
rec.status = "stopped"
|
|
||||||
rec.updated_at = time.time()
|
|
||||||
rec.last_cancel_at = time.time()
|
|
||||||
rec.events.clear()
|
|
||||||
debug_log(
|
|
||||||
f"[TaskCancel] 第二下:清理后台并停止: task_id={task_id}, "
|
|
||||||
f"has_running_background={has_running_background}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 多智能体模式:从已保存对话文件恢复,丢弃运行中的半成品状态
|
|
||||||
# (调用一半的工具、输出一半的内容等不会保存在对话文件里)
|
|
||||||
reload_ok = False
|
|
||||||
messages_count = 0
|
|
||||||
if is_ma_mode and terminal and rec.conversation_id:
|
|
||||||
try:
|
|
||||||
result = terminal.load_conversation(rec.conversation_id, restore_model=False)
|
|
||||||
if result.get("success"):
|
|
||||||
reload_ok = True
|
|
||||||
messages_count = result.get("messages_count", 0)
|
|
||||||
debug_log(f"[TaskCancel] 多智能体对话已从文件恢复: conv={rec.conversation_id}, messages={messages_count}")
|
|
||||||
else:
|
|
||||||
debug_log(f"[TaskCancel] 多智能体对话恢复失败: {result.get('message', '')}")
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel] 多智能体对话恢复异常: {exc}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from server.extensions import socketio
|
|
||||||
socketio.emit('task_stopped', {
|
|
||||||
'message': '任务已停止',
|
|
||||||
'reason': 'user_requested',
|
|
||||||
'task_id': task_id,
|
|
||||||
'conversation_id': rec.conversation_id,
|
|
||||||
'has_running_sub_agents': False,
|
|
||||||
'has_running_background_commands': False,
|
|
||||||
'has_running_multi_agent': False,
|
|
||||||
'multi_agent_reloaded': reload_ok,
|
|
||||||
'messages_count': messages_count,
|
|
||||||
}, room=f"user_{username}")
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel] 发送 task_stopped 事件失败: {exc}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _force_stop_multi_agent(self, rec: TaskRecord, entry, terminal, username: str) -> bool:
|
|
||||||
"""多智能体模式强制停止:软停止所有子智能体 → 重载主对话 → 变空闲。
|
|
||||||
|
|
||||||
与传统模式第二下的区别:
|
|
||||||
- 传统模式第二下:terminate 后台子智能体(会终止进程并留下终止记录)
|
|
||||||
- 多智能体模式:软停止子智能体(完成当前工具后变 idle)
|
|
||||||
+ 从已保存对话文件恢复主对话(丢弃主智能体接收到的半成品输出)
|
|
||||||
"""
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] === _force_stop_multi_agent 被调 === task_id={rec.task_id}")
|
|
||||||
task_id = rec.task_id
|
|
||||||
|
|
||||||
# 1. 先确保主智能体也取消(如果还在跑)
|
|
||||||
if entry:
|
|
||||||
entry['stop'] = True
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] _force_stop_multi_agent entry.stop=True")
|
|
||||||
rec.stop_requested = True
|
|
||||||
|
|
||||||
# 2. 获取 terminal
|
|
||||||
if not terminal and rec.workspace_id:
|
|
||||||
try:
|
|
||||||
terminal, _ = get_user_resources(username, rec.workspace_id)
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] _force_stop_multi_agent fallback get terminal OK")
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] _force_stop_multi_agent fallback get terminal 失败: {exc}")
|
|
||||||
|
|
||||||
# 3. 软停止所有子智能体(不 terminate,让它们在当前工具完成后变 idle)
|
|
||||||
soft_stop_count = 0
|
|
||||||
if terminal:
|
|
||||||
sub_agent_manager = getattr(terminal, 'sub_agent_manager', None)
|
|
||||||
if sub_agent_manager:
|
|
||||||
try:
|
|
||||||
sub_agent_manager.reconcile_task_states(conversation_id=rec.conversation_id)
|
|
||||||
soft_stop_count = sub_agent_manager.soft_stop_all_agents(rec.conversation_id)
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] force_stop 软停止 {soft_stop_count} 个子智能体")
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] force_stop 软停止子智能体异常: {exc}")
|
|
||||||
else:
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] terminal 无 sub_agent_manager,无法软停止")
|
|
||||||
else:
|
|
||||||
debug_log(f"[TaskCancel][stop_debug] terminal 为 None,无法软停止")
|
|
||||||
|
|
||||||
# 4. 标记任务已停止
|
|
||||||
with self._lock:
|
|
||||||
rec.status = "stopped"
|
|
||||||
rec.updated_at = time.time()
|
|
||||||
rec.events.clear()
|
|
||||||
|
|
||||||
# 5. 从已保存对话文件恢复,丢弃主对话半成品状态
|
|
||||||
reload_ok = False
|
|
||||||
messages_count = 0
|
|
||||||
if terminal and rec.conversation_id:
|
|
||||||
try:
|
|
||||||
result = terminal.load_conversation(rec.conversation_id, restore_model=False)
|
|
||||||
if result.get("success"):
|
|
||||||
reload_ok = True
|
|
||||||
messages_count = result.get("messages_count", 0)
|
|
||||||
debug_log(f"[TaskCancel] force_stop 主对话已从文件恢复: conv={rec.conversation_id}, messages={messages_count}")
|
|
||||||
else:
|
|
||||||
debug_log(f"[TaskCancel] force_stop 对话恢复失败: {result.get('message', '')}")
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel] force_stop 对话恢复异常: {exc}")
|
|
||||||
|
|
||||||
# 6. 发送 task_stopped 事件通知前端
|
|
||||||
try:
|
|
||||||
from server.extensions import socketio
|
|
||||||
socketio.emit('task_stopped', {
|
|
||||||
'message': '所有子智能体已停止',
|
|
||||||
'reason': 'force_stop',
|
|
||||||
'task_id': task_id,
|
|
||||||
'conversation_id': rec.conversation_id,
|
|
||||||
'has_running_sub_agents': False,
|
|
||||||
'has_running_background_commands': False,
|
|
||||||
'has_running_multi_agent': False,
|
|
||||||
'multi_agent_reloaded': reload_ok,
|
|
||||||
'messages_count': messages_count,
|
|
||||||
}, room=f"user_{username}")
|
|
||||||
except Exception as exc:
|
|
||||||
debug_log(f"[TaskCancel] force_stop 发送 task_stopped 事件失败: {exc}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_runtime_pending_queue(raw_queue: Any) -> List[Dict[str, Any]]:
|
def _normalize_runtime_pending_queue(raw_queue: Any) -> List[Dict[str, Any]]:
|
||||||
@ -766,10 +568,8 @@ class TaskManager:
|
|||||||
# ---- internal helpers ----
|
# ---- internal helpers ----
|
||||||
def _cleanup_background_tasks(self, rec: TaskRecord, terminal: Optional[Any]) -> bool:
|
def _cleanup_background_tasks(self, rec: TaskRecord, terminal: Optional[Any]) -> bool:
|
||||||
"""清理指定任务/对话下的所有后台子智能体和后台命令,返回是否清理到任何任务。"""
|
"""清理指定任务/对话下的所有后台子智能体和后台命令,返回是否清理到任何任务。"""
|
||||||
debug_log(f"[TaskCancel][stop_debug] _cleanup_background_tasks 被调! conv={rec.conversation_id}")
|
|
||||||
has_running_background = False
|
has_running_background = False
|
||||||
if not terminal or not rec.conversation_id:
|
if not terminal or not rec.conversation_id:
|
||||||
debug_log(f"[TaskCancel][stop_debug] _cleanup_background_tasks 跳过:terminal或conv 不存在")
|
|
||||||
return has_running_background
|
return has_running_background
|
||||||
|
|
||||||
sub_agent_manager = getattr(terminal, 'sub_agent_manager', None)
|
sub_agent_manager = getattr(terminal, 'sub_agent_manager', None)
|
||||||
@ -782,7 +582,6 @@ class TaskManager:
|
|||||||
status = task_info.get('status')
|
status = task_info.get('status')
|
||||||
if status not in SUB_AGENT_TERMINAL_STATUSES.union({"terminated"}):
|
if status not in SUB_AGENT_TERMINAL_STATUSES.union({"terminated"}):
|
||||||
has_running_background = True
|
has_running_background = True
|
||||||
debug_log(f"[TaskCancel][stop_debug] 调用 terminate_sub_agent: task_id={task_info.get('task_id')}, status={status}")
|
|
||||||
try:
|
try:
|
||||||
sub_agent_manager.terminate_sub_agent(task_id=task_info.get('task_id'))
|
sub_agent_manager.terminate_sub_agent(task_id=task_info.get('task_id'))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@ -331,8 +331,10 @@
|
|||||||
:multi-agent-mode="multiAgentMode"
|
:multi-agent-mode="multiAgentMode"
|
||||||
:has-running-task="composerBusy || anyWorkspaceHasRunningTask"
|
:has-running-task="composerBusy || anyWorkspaceHasRunningTask"
|
||||||
:main-chat-idle="mainChatIdle"
|
:main-chat-idle="mainChatIdle"
|
||||||
|
:active-sub-agent-count="activeSubAgentCount"
|
||||||
:avatar-status="showStatusAvatar && messages.length > 0 ? avatarStatus : null"
|
:avatar-status="showStatusAvatar && messages.length > 0 ? avatarStatus : null"
|
||||||
@restore-user-question="restoreUserQuestionDialog"
|
@restore-user-question="restoreUserQuestionDialog"
|
||||||
|
@stop-all-sub-agents="handleStopAllSubAgents"
|
||||||
@update:input-message="inputSetMessage"
|
@update:input-message="inputSetMessage"
|
||||||
@input-change="handleInputChange"
|
@input-change="handleInputChange"
|
||||||
@input-focus="handleInputFocus"
|
@input-focus="handleInputFocus"
|
||||||
|
|||||||
@ -252,6 +252,19 @@ export const computed = {
|
|||||||
this.compressing
|
this.compressing
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 子智能体非终态数(包含 running + idle,不含 failed/completed/terminated/timeout)。
|
||||||
|
* 用于状态栏的小按钮显示 “x 个子智能体运行中”。
|
||||||
|
*/
|
||||||
|
activeSubAgentCount() {
|
||||||
|
const subAgentStore = useSubAgentStore();
|
||||||
|
const list = Array.isArray(subAgentStore.subAgents) ? subAgentStore.subAgents : [];
|
||||||
|
const TERMINAL = new Set(['completed', 'failed', 'timeout', 'terminated']);
|
||||||
|
return list.filter((s: any) => {
|
||||||
|
const status = String(s?.status || '').toLowerCase();
|
||||||
|
return !TERMINAL.has(status);
|
||||||
|
}).length;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 主对话是否空闲。
|
* 主对话是否空闲。
|
||||||
* 与 composerBusy 的区别:composerBusy=true 可能是“主对话空闲但后台子智能体在跑”
|
* 与 composerBusy 的区别:composerBusy=true 可能是“主对话空闲但后台子智能体在跑”
|
||||||
|
|||||||
@ -36,7 +36,6 @@ export const sendMethods = {
|
|||||||
const mainIdle = typeof this.mainChatIdle === 'function' ? this.mainChatIdle : (
|
const mainIdle = typeof this.mainChatIdle === 'function' ? this.mainChatIdle : (
|
||||||
!this.streamingUi && !this.stopRequested && !this.compressionInProgress && !this.compressing
|
!this.streamingUi && !this.stopRequested && !this.compressionInProgress && !this.compressing
|
||||||
);
|
);
|
||||||
console.log('[STOP_DEBUG] handleSendOrStop 状态 → composerBusy:', this.composerBusy, 'mainIdle:', mainIdle, 'hasText:', hasText, 'hasMedia:', hasMedia, 'waitingForSubAgent:', this.waitingForSubAgent);
|
|
||||||
if (this.composerBusy && mainIdle && hasText) {
|
if (this.composerBusy && mainIdle && hasText) {
|
||||||
// 如果有 pending 问题(子智能体询问主智能体),仍走问答路径,不走直接发送
|
// 如果有 pending 问题(子智能体询问主智能体),仍走问答路径,不走直接发送
|
||||||
if (Array.isArray(this.pendingUserQuestions) && this.pendingUserQuestions.length > 0) {
|
if (Array.isArray(this.pendingUserQuestions) && this.pendingUserQuestions.length > 0) {
|
||||||
@ -49,7 +48,7 @@ export const sendMethods = {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[STOP_DEBUG] 主对话空闲但后台子智能体在跑,直接发送新消息触发主智能体下一轮');
|
// 主对话空闲但后台任务在跑:直接发送新消息触发主智能体下一轮
|
||||||
this.sendMessage();
|
this.sendMessage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -426,16 +425,9 @@ export const sendMethods = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 多智能体模式下,stopRequested 不阻止第二次点击——第二次点击是强制停止子智能体
|
// 停止按钮现在只停主智能体,与后台任务无关。
|
||||||
const isMultiAgent = !!this.multiAgentMode;
|
// canStop 判断只看主智能体是否在 streaming。多次点击被 stopRequested 抖动拦截。
|
||||||
const canStop = isMultiAgent
|
const canStop = this.streamingUi && !this.stopRequested;
|
||||||
? (this.composerBusy || this.stopRequested)
|
|
||||||
: (this.composerBusy && !this.stopRequested);
|
|
||||||
|
|
||||||
// 多智能体模式下,如果主智能体已经停了(stopRequested=true),第二次点击是 force stop
|
|
||||||
const isForceStop = isMultiAgent && this.stopRequested;
|
|
||||||
|
|
||||||
console.log('[STOP_DEBUG] stopTask 状态快照 → isMultiAgent:', isMultiAgent, 'composerBusy:', this.composerBusy, 'stopRequested:', this.stopRequested, 'taskInProgress:', this.taskInProgress, 'canStop:', canStop, 'isForceStop:', isForceStop);
|
|
||||||
|
|
||||||
goalModeDebugLog('stopTask:entry', {
|
goalModeDebugLog('stopTask:entry', {
|
||||||
composerBusy: this.composerBusy,
|
composerBusy: this.composerBusy,
|
||||||
@ -443,8 +435,6 @@ export const sendMethods = {
|
|||||||
taskInProgress: this.taskInProgress,
|
taskInProgress: this.taskInProgress,
|
||||||
streamingUi: this.streamingUi,
|
streamingUi: this.streamingUi,
|
||||||
canStop,
|
canStop,
|
||||||
isForceStop,
|
|
||||||
isMultiAgent,
|
|
||||||
currentTaskId: this.currentTaskId,
|
currentTaskId: this.currentTaskId,
|
||||||
});
|
});
|
||||||
if (!canStop) {
|
if (!canStop) {
|
||||||
@ -470,30 +460,12 @@ export const sendMethods = {
|
|||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
|
|
||||||
if (taskStore.currentTaskId) {
|
if (taskStore.currentTaskId) {
|
||||||
await taskStore.cancelTask(isForceStop);
|
await taskStore.cancelTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待后端确认;轮询继续,task_stopped 事件到达后会由 taskStore 自动停止轮询
|
// 等待后端确认;轮询继续,task_stopped 事件到达后会由 taskStore 自动停止轮询
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
// 多智能体模式强制停止后,直接释放所有状态,等待 task_stopped 事件处理重载
|
|
||||||
if (isForceStop) {
|
|
||||||
this.clearPendingTools('user_stop');
|
|
||||||
this.streamingMessage = false;
|
|
||||||
this.taskInProgress = false;
|
|
||||||
this.forceUnlockMonitor('user_stop');
|
|
||||||
if (typeof this.clearProcessedEvents === 'function') {
|
|
||||||
this.clearProcessedEvents();
|
|
||||||
}
|
|
||||||
this.stopMultiAgentTaskProbe();
|
|
||||||
this.stopWaitingTaskProbe();
|
|
||||||
const lastMessage = this.messages[this.messages.length - 1];
|
|
||||||
if (lastMessage && lastMessage.role === 'assistant') {
|
|
||||||
lastMessage.awaitingFirstContent = false;
|
|
||||||
lastMessage.generatingLabel = '';
|
|
||||||
}
|
|
||||||
console.log('[DEBUG_AWAITING] stopTask force stop 清理完成');
|
|
||||||
} else {
|
|
||||||
const shouldKeepBusy =
|
const shouldKeepBusy =
|
||||||
['running', 'pending', 'cancel_requested', 'canceled'].includes(String(taskStore.taskStatus));
|
['running', 'pending', 'cancel_requested', 'canceled'].includes(String(taskStore.taskStatus));
|
||||||
|
|
||||||
@ -516,26 +488,10 @@ export const sendMethods = {
|
|||||||
|
|
||||||
// 清理assistant消息的等待动画状态
|
// 清理assistant消息的等待动画状态
|
||||||
const lastMessage = this.messages[this.messages.length - 1];
|
const lastMessage = this.messages[this.messages.length - 1];
|
||||||
const before = {
|
|
||||||
hasLastMessage: !!lastMessage,
|
|
||||||
role: lastMessage?.role,
|
|
||||||
awaitingFirstContent: lastMessage?.awaitingFirstContent,
|
|
||||||
generatingLabel: lastMessage?.generatingLabel
|
|
||||||
};
|
|
||||||
|
|
||||||
if (lastMessage && lastMessage.role === 'assistant') {
|
if (lastMessage && lastMessage.role === 'assistant') {
|
||||||
lastMessage.awaitingFirstContent = false;
|
lastMessage.awaitingFirstContent = false;
|
||||||
lastMessage.generatingLabel = '';
|
lastMessage.generatingLabel = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[DEBUG_AWAITING] stopTask 清理完成', {
|
|
||||||
before,
|
|
||||||
after: {
|
|
||||||
awaitingFirstContent: lastMessage?.awaitingFirstContent,
|
|
||||||
generatingLabel: lastMessage?.generatingLabel
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Message] 取消任务失败:', error);
|
console.error('[Message] 取消任务失败:', error);
|
||||||
const { useTaskStore } = await import('../../../stores/task');
|
const { useTaskStore } = await import('../../../stores/task');
|
||||||
@ -569,7 +525,7 @@ export const sendMethods = {
|
|||||||
|
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '停止请求已发送',
|
title: '停止请求已发送',
|
||||||
message: '若任务未停止,请再次点击停止按钮',
|
message: '若主对话未停止,请稍候;后台任务可通过状态栏单独停止',
|
||||||
type: 'info'
|
type: 'info'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -180,5 +180,55 @@ export const modeMethods = {
|
|||||||
},
|
},
|
||||||
async toggleThinkingMode() {
|
async toggleThinkingMode() {
|
||||||
await this.handleCycleRunMode();
|
await this.handleCycleRunMode();
|
||||||
|
},
|
||||||
|
async handleStopAllSubAgents() {
|
||||||
|
const isMultiAgent = !!this.multiAgentMode;
|
||||||
|
const mode = isMultiAgent ? 'soft_stop' : 'terminate';
|
||||||
|
const title = isMultiAgent ? '是否暂停所有子智能体?' : '是否终结所有子智能体?';
|
||||||
|
const message = isMultiAgent
|
||||||
|
? '所有正在运行的子智能体将停止工作并变为空闲状态。取消表示不执行操作。'
|
||||||
|
: '所有后台子智能体将被强制终止。取消表示不执行操作。';
|
||||||
|
const confirmText = isMultiAgent ? '暂停' : '终结';
|
||||||
|
let proceed = false;
|
||||||
|
try {
|
||||||
|
const uiStore = (await import('../../../stores/ui')).useUiStore();
|
||||||
|
proceed = await uiStore.requestConfirm({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmText,
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmVariant: 'danger',
|
||||||
|
closeOnBackdrop: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[stopAllSubAgents] 弹窗异常:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!proceed) return;
|
||||||
|
try {
|
||||||
|
const { useSubAgentStore } = await import('../../../stores/subAgent');
|
||||||
|
const store = useSubAgentStore();
|
||||||
|
const result = await store.stopAllAgents(mode);
|
||||||
|
if (!result.success) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '停止子智能体失败',
|
||||||
|
message: result.error || '请重试',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uiPushToast({
|
||||||
|
title: isMultiAgent ? '子智能体已暂停' : '子智能体已终结',
|
||||||
|
message: `已处理 ${result.stoppedCount || 0} 个子智能体`,
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[stopAllSubAgents] 调用失败:', error);
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '停止子智能体失败',
|
||||||
|
message: error?.message || String(error),
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -177,6 +177,14 @@
|
|||||||
>
|
>
|
||||||
等待回答问题
|
等待回答问题
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="(activeSubAgentCount || 0) > 0"
|
||||||
|
type="button"
|
||||||
|
class="floating-project-status__notice floating-project-status__notice--button"
|
||||||
|
@click.stop="$emit('stop-all-sub-agents')"
|
||||||
|
>
|
||||||
|
{{ activeSubAgentCount }} 个子智能体运行中
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="terminalCount > 0"
|
v-if="terminalCount > 0"
|
||||||
type="button"
|
type="button"
|
||||||
@ -549,7 +557,8 @@ const emit = defineEmits([
|
|||||||
'toggle-goal-mode',
|
'toggle-goal-mode',
|
||||||
'open-goal-dialog',
|
'open-goal-dialog',
|
||||||
'open-git-changes-panel',
|
'open-git-changes-panel',
|
||||||
'toggle-agent-mode'
|
'toggle-agent-mode',
|
||||||
|
'stop-all-sub-agents'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -625,6 +634,8 @@ const props = defineProps<{
|
|||||||
* 多智能体模式下,若主对话空闲,输入栏应处于“正常发送”状态,不是“停止”状态。
|
* 多智能体模式下,若主对话空闲,输入栏应处于“正常发送”状态,不是“停止”状态。
|
||||||
*/
|
*/
|
||||||
mainChatIdle?: boolean;
|
mainChatIdle?: boolean;
|
||||||
|
/** 非终态子智能体数量,用于状态栏“x 个子智能体运行中”按钮显示 */
|
||||||
|
activeSubAgentCount?: number;
|
||||||
hasPendingRuntimeGuidance?: boolean;
|
hasPendingRuntimeGuidance?: boolean;
|
||||||
terminalCount?: number;
|
terminalCount?: number;
|
||||||
hostMode?: boolean;
|
hostMode?: boolean;
|
||||||
@ -841,7 +852,8 @@ const floatingStatusVisible = computed(() => {
|
|||||||
!!projectGitSummaryForRender.value ||
|
!!projectGitSummaryForRender.value ||
|
||||||
!!props.goalRunning ||
|
!!props.goalRunning ||
|
||||||
!!props.goalModeArmed ||
|
!!props.goalModeArmed ||
|
||||||
(!!props.userQuestionMinimized && Number(props.pendingUserQuestionCount || 0) > 0)
|
(!!props.userQuestionMinimized && Number(props.pendingUserQuestionCount || 0) > 0) ||
|
||||||
|
(Number(props.activeSubAgentCount || 0) > 0)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2404,34 +2416,20 @@ const hasComposerContent = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showStopIcon = computed(() => {
|
const showStopIcon = computed(() => {
|
||||||
// 多智能体模式下:主对话空闲(后台子智能体在跑)时,输入栏应为“正常发送”状态,不显示停止按钮。
|
// 停止按钮只看主对话是否在 streaming。后台任务(子智能体/后台指令)的停止
|
||||||
// 只有主对话在 streaming 时才显示停止按钮(即 mainChatIdle=false 时)。
|
// 有独立的“x 个子智能体运行中”按钮和 LeftPanel 后台指令停止按钮,不再集成在停止按钮上。
|
||||||
if (props.multiAgentMode) {
|
return !!props.streamingMessage && !hasComposerContent.value;
|
||||||
if (props.mainChatIdle) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !hasComposerContent.value;
|
|
||||||
}
|
|
||||||
return props.streamingMessage && !hasComposerContent.value;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendButtonDisabled = computed(() => {
|
const sendButtonDisabled = computed(() => {
|
||||||
if (!props.isConnected) {
|
if (!props.isConnected) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// 多智能体模式下:主对话空闲但后台子智能体在跑时,输入栏正常可用
|
// 主对话 streaming 时,有内容则可点击(走 stopTask);无内容也显示停止按钮
|
||||||
if (props.multiAgentMode && props.mainChatIdle) {
|
|
||||||
if (props.inputLocked) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (props.mediaUploading) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !hasComposerContent.value;
|
|
||||||
}
|
|
||||||
if (props.streamingMessage) {
|
if (props.streamingMessage) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// 主对话空闲时输入栏正常可用,与是否后台任务在跑无关。
|
||||||
if (props.inputLocked) {
|
if (props.inputLocked) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,16 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></span>
|
></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="panelMode === 'backgroundCommands' && runningBackgroundCommandCount > 0"
|
||||||
|
type="button"
|
||||||
|
class="background-stop-btn"
|
||||||
|
title="终止所有后台指令"
|
||||||
|
@click.stop="handleStopAllCommands"
|
||||||
|
>
|
||||||
|
<span class="icon icon-sm" :style="iconStyle('stop')" aria-hidden="true"></span>
|
||||||
|
<span class="background-stop-label">停止</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@ -415,6 +425,20 @@ const openBackgroundCommand = (command: any) => {
|
|||||||
backgroundCommandStore.openCommand(command);
|
backgroundCommandStore.openCommand(command);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BG_COMMAND_TERMINAL = new Set(['completed', 'failed', 'timeout', 'cancelled']);
|
||||||
|
const runningBackgroundCommandCount = computed(() => {
|
||||||
|
return (backgroundCommands.value || []).filter((c: any) => {
|
||||||
|
const s = String(c?.status || '').toLowerCase();
|
||||||
|
return !BG_COMMAND_TERMINAL.has(s);
|
||||||
|
}).length;
|
||||||
|
});
|
||||||
|
const handleStopAllCommands = async () => {
|
||||||
|
const result = await backgroundCommandStore.stopAllCommands();
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('[LeftPanel] 批量停止后台指令失败:', result.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatBackgroundCommandMeta = (command: any) => {
|
const formatBackgroundCommandMeta = (command: any) => {
|
||||||
const code = command?.return_code;
|
const code = command?.return_code;
|
||||||
if (code === null || code === undefined) {
|
if (code === null || code === undefined) {
|
||||||
|
|||||||
@ -1642,59 +1642,22 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
// 任务停止
|
// 任务停止
|
||||||
ctx.socket.on('task_stopped', (data) => {
|
ctx.socket.on('task_stopped', (data) => {
|
||||||
socketLog('任务已停止:', data.message);
|
socketLog('任务已停止:', data.message);
|
||||||
console.log('[STOP_DEBUG] 收到 task_stopped 事件:', JSON.stringify({
|
|
||||||
message: data.message,
|
|
||||||
reason: data.reason,
|
|
||||||
task_id: data.task_id,
|
|
||||||
has_running_sub_agents: data.has_running_sub_agents,
|
|
||||||
has_running_background_commands: data.has_running_background_commands,
|
|
||||||
has_running_multi_agent: data.has_running_multi_agent,
|
|
||||||
multi_agent_reloaded: data.multi_agent_reloaded,
|
|
||||||
multiAgentMode: ctx.multiAgentMode,
|
|
||||||
stopRequested_before: ctx.stopRequested,
|
|
||||||
taskInProgress_before: ctx.taskInProgress,
|
|
||||||
composerBusy: ctx.composerBusy
|
|
||||||
}));
|
|
||||||
const hasRunningSubAgents = !!data?.has_running_sub_agents;
|
const hasRunningSubAgents = !!data?.has_running_sub_agents;
|
||||||
const hasRunningBackgroundCommands = !!data?.has_running_background_commands;
|
const hasRunningBackgroundCommands = !!data?.has_running_background_commands;
|
||||||
const hasRunningBackground = hasRunningSubAgents || hasRunningBackgroundCommands;
|
const hasRunningBackground = hasRunningSubAgents || hasRunningBackgroundCommands;
|
||||||
const hasRunningMultiAgent = !!data?.has_running_multi_agent;
|
const hasRunningMultiAgent = !!data?.has_running_multi_agent;
|
||||||
const multiAgentReloaded = !!data?.multi_agent_reloaded;
|
// 主对话已被停止。streaming、stopRequested 都重置。
|
||||||
|
// 如果仍有后台任务在跑,保留 taskInProgress=true 让对话在列表里仍显示“运行中”
|
||||||
// 多智能体模式下,第二下停止后后端已从文件恢复对话,前端需要重新加载对话内容
|
// (仅影响 UI 标识,不影响输入栏可用性——输入栏只看 streamingMessage)。
|
||||||
if (multiAgentReloaded && ctx.currentConversationId) {
|
const stillHasBackground = hasRunningBackground || hasRunningMultiAgent;
|
||||||
console.log('[DEBUG] 多智能体对话已从文件恢复,前端重新加载');
|
ctx.streamingMessage = false;
|
||||||
// 触发前端重新加载对话内容(加载已保存的完整消息列表,丢弃半成品状态)
|
ctx.stopRequested = false;
|
||||||
if (typeof ctx.loadConversation === 'function') {
|
|
||||||
ctx.loadConversation(ctx.currentConversationId, { force: true });
|
|
||||||
} else {
|
|
||||||
// 回退:直接调 API 重新加载
|
|
||||||
fetch(`/api/conversations/${ctx.currentConversationId}/load`, { method: 'PUT' })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(result => {
|
|
||||||
if (result.success && result.messages_count !== undefined) {
|
|
||||||
console.log('[DEBUG] 对话重新加载成功,消息数:', result.messages_count);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => console.error('[DEBUG] 对话重新加载失败:', err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 多智能体模式下,主任务停止即可恢复输入区,不因子智能体后台运行保持停止按钮
|
|
||||||
if (ctx.multiAgentMode || !hasRunningBackground) {
|
|
||||||
ctx.taskInProgress = false;
|
|
||||||
} else {
|
|
||||||
ctx.waitingForSubAgent = hasRunningSubAgents;
|
ctx.waitingForSubAgent = hasRunningSubAgents;
|
||||||
ctx.waitingForBackgroundCommand = hasRunningBackgroundCommands;
|
ctx.waitingForBackgroundCommand = hasRunningBackgroundCommands;
|
||||||
if (typeof ctx.uiPushToast === 'function') {
|
ctx.taskInProgress = stillHasBackground;
|
||||||
ctx.uiPushToast({
|
if (typeof ctx.scheduleResetAfterTask === 'function') {
|
||||||
title: '智能体已停止',
|
|
||||||
message: '再次按下停止按钮以结束后台任务',
|
|
||||||
type: 'info'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.scheduleResetAfterTask('socket:task_stopped', { preserveMonitorWindows: true });
|
ctx.scheduleResetAfterTask('socket:task_stopped', { preserveMonitorWindows: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 任务完成(重点:更新Token统计)
|
// 任务完成(重点:更新Token统计)
|
||||||
|
|||||||
@ -184,6 +184,24 @@ export const useSubAgentStore = defineStore('subAgent', {
|
|||||||
this.stoppingTaskIds = next;
|
this.stoppingTaskIds = next;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async stopAllAgents(mode: 'terminate' | 'soft_stop'): Promise<{ success: boolean; stoppedCount?: number; error?: string }> {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/sub_agents/stop_all', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ mode })
|
||||||
|
});
|
||||||
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
if (!resp.ok || !data?.success) {
|
||||||
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||||
|
}
|
||||||
|
await this.fetchSubAgents();
|
||||||
|
return { success: true, stoppedCount: data?.data?.stopped_count || 0 };
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error?.message || String(error);
|
||||||
|
return { success: false, error: message };
|
||||||
|
}
|
||||||
|
},
|
||||||
stripConversationPrefix(conversationId: string) {
|
stripConversationPrefix(conversationId: string) {
|
||||||
if (!conversationId) return '';
|
if (!conversationId) return '';
|
||||||
return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId;
|
return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId;
|
||||||
|
|||||||
@ -538,19 +538,17 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.currentTaskId = null; // 清除任务 ID
|
this.currentTaskId = null; // 清除任务 ID
|
||||||
},
|
},
|
||||||
|
|
||||||
async cancelTask(forceStop: boolean = false) {
|
async cancelTask() {
|
||||||
if (!this.currentTaskId) {
|
if (!this.currentTaskId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debugLog('[Task] 取消任务:', this.currentTaskId, 'forceStop:', forceStop);
|
debugLog('[Task] 取消任务:', this.currentTaskId);
|
||||||
console.log('[STOP_DEBUG] cancelTask 发送请求 → taskId:', this.currentTaskId, 'forceStop:', forceStop);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
|
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' }
|
||||||
body: JSON.stringify({ force_stop: forceStop })
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@ -998,3 +998,28 @@
|
|||||||
.panel-menu button.active {
|
.panel-menu button.active {
|
||||||
background: var(--theme-tab-active);
|
background: var(--theme-tab-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 后台指令停止按钮 */
|
||||||
|
.background-stop-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--claude-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--state-error);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 140ms ease, color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-stop-btn:hover {
|
||||||
|
background: color-mix(in srgb, var(--state-error) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-stop-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user