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) =>