From 571ed6c81fd2b6bf41133f7ac7701d630856e6f6 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Tue, 14 Jul 2026 20:37:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(sub-agent):=20=E4=BF=AE=E5=A4=8D=E4=BC=A0?= =?UTF-8?q?=E7=BB=9F=E6=A8=A1=E5=BC=8F=20terminate=20=E5=90=8E=E5=AD=90?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E4=BB=8D=E7=BB=A7=E7=BB=AD=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E5=B7=A5=E5=85=B7=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _call_model 中硬取消时直接抛出 CancelledError,避免返回半成品 tool_calls - 软停止时仍优雅 break,后续由 _run_loop 丢弃半成品并进入 idle - 保持 _mark_task_done 不覆盖 terminated 状态 - 侧边栏当前对话运行态标记修复保留 --- modules/sub_agent/state.py | 11 +++++++++++ modules/sub_agent/task.py | 6 +++++- static/src/App.vue | 4 ++++ .../components/sidebar/ConversationSidebar.vue | 18 +++++++++++++++--- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/modules/sub_agent/state.py b/modules/sub_agent/state.py index de168bf..13aafbb 100644 --- a/modules/sub_agent/state.py +++ b/modules/sub_agent/state.py @@ -301,6 +301,17 @@ class SubAgentStateMixin: task = self.tasks.get(task_id) if not task: return + # 如果 terminate_sub_agent 已把任务标为 terminated,下渗异步任务自然结束时 + # 不要被“已完成/fulfilled覆盖为failed”值覆盖,否则 task 被碰成 failed 但实际上 + # 用户手动终止已发生,状态语义错误且“successful=false”。 + existing_status = task.get("status") + if existing_status in TERMINAL_STATUSES.union({"terminated"}): + ma_debug( + "mark_task_done_skip_already_terminal", + task_id=task_id, + existing_status=existing_status, + ) + return status = "completed" if success else "failed" ma_debug( "mark_task_done", diff --git a/modules/sub_agent/task.py b/modules/sub_agent/task.py index 0b4fcdc..be4bf01 100644 --- a/modules/sub_agent/task.py +++ b/modules/sub_agent/task.py @@ -540,8 +540,12 @@ class SubAgentTask: usage = None async for chunk in client.chat(self.messages, tools=tools, stream=True): - if self._cancelled: + if self._soft_stop: + # 软停止:优雅中断当前模型调用,后续由 _run_loop 丢弃半成品并进入 idle break + if self._cancelled: + # 硬取消:立即抛出 CancelledError,确保不会返回半成品的 tool_calls 继续执行 + raise asyncio.CancelledError() if chunk.get("error"): raise RuntimeError(f"API 调用失败: {chunk.get('error')}") choice = (chunk.get("choices") or [{}])[0] diff --git a/static/src/App.vue b/static/src/App.vue index da38da0..9f949fa 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -63,6 +63,8 @@ :icon-style="iconStyle" :format-time="formatTime" :running-tasks="runningWorkspaceTasks" + :current-task-in-progress="taskInProgress" + :current-conversation-id="currentConversationId" :current-workspace-id="currentHostWorkspaceId" :display-mode="chatDisplayMode" :display-mode-disabled="displayModeSwitchDisabled" @@ -765,6 +767,8 @@ :icon-style="iconStyle" :format-time="formatTime" :running-tasks="runningWorkspaceTasks" + :current-task-in-progress="taskInProgress" + :current-conversation-id="currentConversationId" :current-workspace-id="currentHostWorkspaceId" :display-mode="chatDisplayMode" :display-mode-disabled="displayModeSwitchDisabled" diff --git a/static/src/components/sidebar/ConversationSidebar.vue b/static/src/components/sidebar/ConversationSidebar.vue index f4a1949..d36a848 100644 --- a/static/src/components/sidebar/ConversationSidebar.vue +++ b/static/src/components/sidebar/ConversationSidebar.vue @@ -547,6 +547,9 @@ const props = withDefaults( hostWorkspaceEnabled?: boolean; groupByWorkspace?: boolean; versioningHostMode?: boolean; + /** 当前会话是否仍在运行(用于标记当前正在查看的对话也显示运行中转圈) */ + currentTaskInProgress?: boolean; + currentConversationId?: string; }>(), { showCollapseButton: true, @@ -559,7 +562,9 @@ const props = withDefaults( workspaceKind: 'workspace', hostWorkspaceEnabled: false, groupByWorkspace: false, - versioningHostMode: false + versioningHostMode: false, + currentTaskInProgress: false, + currentConversationId: '' } ); @@ -890,8 +895,15 @@ const completedConversationIds = computed( .filter(Boolean) ) ); -const isConversationActive = (conversationId: string) => - activeConversationIds.value.has(String(conversationId || '')); +const isConversationActive = (conversationId: string) => { + if (activeConversationIds.value.has(String(conversationId || ''))) return true; + // 后端 runningTasks 库只跟踪跨工作的任务(当前会话排除了), + // 所以当前会话的运行态要单独用 currentTaskInProgress 标记。 + if (props.currentTaskInProgress && String(conversationId || '') === String(props.currentConversationId || '')) { + return true; + } + return false; +}; const isConversationCompleted = (conversationId: string) => completedConversationIds.value.has(String(conversationId || '')); const isWorkspaceRunning = (workspaceId: string) =>