From f2bfa3f377cae4393cedad9bfa8fff362525c6b1 Mon Sep 17 00:00:00 2001
From: JOJO <1498581755@qq.com>
Date: Sat, 15 Nov 2025 17:10:58 +0800
Subject: [PATCH] feat: add sub agent termination control
---
core/main_terminal.py | 2 +
core/tool_config.py | 2 +-
modules/sub_agent_manager.py | 3 +
sub_agent/static/app.js | 35 ++++-
sub_agent/static/index.html | 40 +-----
sub_agent/static/style.css | 247 +++--------------------------------
sub_agent/web_server.py | 15 ++-
7 files changed, 76 insertions(+), 268 deletions(-)
diff --git a/core/main_terminal.py b/core/main_terminal.py
index 2e30875..4a3759c 100644
--- a/core/main_terminal.py
+++ b/core/main_terminal.py
@@ -2010,6 +2010,8 @@ class MainTerminal:
task_id=arguments.get("task_id"),
agent_id=arguments.get("agent_id")
)
+ message = result.get("message") or result.get("error")
+ self._record_sub_agent_message(message, result.get("task_id"), inline=False)
else:
result = {"success": False, "error": f"未知工具: {tool_name}"}
diff --git a/core/tool_config.py b/core/tool_config.py
index 48a9d7b..1d2c114 100644
--- a/core/tool_config.py
+++ b/core/tool_config.py
@@ -58,6 +58,6 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
),
"sub_agent": ToolCategory(
label="子智能体",
- tools=["create_sub_agent", "wait_sub_agent"],
+ tools=["create_sub_agent", "wait_sub_agent", "close_sub_agent"],
),
}
diff --git a/modules/sub_agent_manager.py b/modules/sub_agent_manager.py
index 00a2083..6b01cf7 100644
--- a/modules/sub_agent_manager.py
+++ b/modules/sub_agent_manager.py
@@ -219,6 +219,7 @@ class SubAgentManager:
task_id = task["task_id"]
response = self._call_service("POST", f"/tasks/{task_id}/terminate", timeout=10)
+ response["task_id"] = task_id
if response.get("success"):
task["status"] = "terminated"
task["final_result"] = {
@@ -229,6 +230,8 @@ class SubAgentManager:
"message": response.get("message") or "子智能体已被强制关闭。",
}
self._save_state()
+ if "system_message" not in response:
+ response["system_message"] = response.get("message")
return response
# ------------------------------------------------------------------
diff --git a/sub_agent/static/app.js b/sub_agent/static/app.js
index 8779d61..f3d48ad 100644
--- a/sub_agent/static/app.js
+++ b/sub_agent/static/app.js
@@ -175,6 +175,7 @@ async function bootstrapApp() {
// 停止功能状态
stopRequested: false,
+ terminating: false,
// 路由相关
initialRouteResolved: false,
@@ -633,6 +634,37 @@ async function bootstrapApp() {
}
},
+ async terminateSubAgentTask() {
+ if (!this.subAgentTaskId || this.terminating) {
+ return;
+ }
+ const shouldTerminate = window.confirm('确定要立即关闭子智能体吗?此操作无法撤销。');
+ if (!shouldTerminate) {
+ return;
+ }
+ this.terminating = true;
+ try {
+ const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/terminate`, { method: 'POST' });
+ let data = null;
+ try {
+ data = await resp.json();
+ } catch (err) {
+ data = { success: false, error: await resp.text() };
+ }
+ if (!resp.ok || !data.success) {
+ const message = (data && (data.error || data.message)) || '关闭失败';
+ alert(`关闭子智能体失败:${message}`);
+ return;
+ }
+ this.addSystemMessage('🛑 子智能体已被手动关闭。');
+ } catch (error) {
+ console.warn('关闭子智能体失败:', error);
+ alert(`关闭子智能体失败:${error.message || error}`);
+ } finally {
+ this.terminating = false;
+ }
+ },
+
formatPathDisplay(path) {
if (!path) {
return '—';
@@ -647,7 +679,8 @@ async function bootstrapApp() {
pending: '等待中',
completed: '已完成',
failed: '失败',
- timeout: '超时'
+ timeout: '超时',
+ terminated: '已关闭'
};
return map[status] || status;
},
diff --git a/sub_agent/static/index.html b/sub_agent/static/index.html
index e7f0557..92aef06 100644
--- a/sub_agent/static/index.html
+++ b/sub_agent/static/index.html
@@ -59,6 +59,13 @@
状态: {{ formatSubAgentStatus(subAgentTaskInfo.status) }}
+
{{ isConnected ? '已连接' : '未连接' }}
@@ -228,39 +235,6 @@
-
-
diff --git a/sub_agent/static/style.css b/sub_agent/static/style.css
index baca62f..7271659 100644
--- a/sub_agent/static/style.css
+++ b/sub_agent/static/style.css
@@ -24,6 +24,7 @@
--claude-shadow: 0 14px 36px rgba(61, 57, 41, 0.12);
--claude-success: #76b086;
--claude-warning: #d99845;
+ --claude-danger: #d65a5a;
}
html, body {
@@ -86,6 +87,21 @@ body {
letter-spacing: 0.04em;
}
+.terminate-btn {
+ background: var(--claude-danger);
+ color: #fff6f6;
+ border: none;
+ padding: 6px 14px;
+ border-radius: 6px;
+ font-size: 13px;
+ cursor: pointer;
+}
+
+.terminate-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
.connection-status {
font-size: 13px;
color: var(--claude-text-secondary);
@@ -1373,11 +1389,6 @@ body {
margin-left: 8px;
}
-/* 输入区域 */
-.token-wrapper {
- flex-shrink: 0;
-}
-
.input-area {
background: rgba(255, 255, 255, 0.82);
border-top: 1px solid var(--claude-border);
@@ -1530,9 +1541,6 @@ body {
.messages-area {
padding: 16px 18px;
}
- .token-wrapper {
- margin-bottom: 8px;
- }
.input-area {
padding: 14px;
}
@@ -1862,230 +1870,7 @@ body {
color: var(--claude-text-secondary);
}
-.token-count {
- color: var(--claude-accent);
- font-weight: 500;
-}
-/* ========================================= */
-/* Token 统计面板样式(无缝一体版)*/
-/* ========================================= */
-/* Token区域包装器 */
-.token-wrapper {
- position: relative;
- z-index: 5;
- margin-bottom: 0;
-}
-
-/* 当前对话信息栏 - 移除底部边框 */
-.current-conversation-info {
- position: relative;
- z-index: 10;
- background: var(--claude-panel);
- backdrop-filter: blur(18px);
- border-bottom: none; /* 移除边框,让它和下面的面板融为一体 */
- padding: 12px 20px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 14px;
- color: var(--claude-text-secondary);
- border-radius: 0; /* 顶部保持直角 */
- box-shadow: 0 12px 28px rgba(61, 57, 41, 0.06);
-}
-
-/* Token面板 - 与标题栏完全一体,底部圆角 */
-.token-display-panel {
- background: var(--claude-panel);
- backdrop-filter: blur(18px);
- border: none;
- border-radius: 0 0 16px 16px;
- box-shadow: 0 8px 18px rgba(189, 93, 58, 0.12);
- overflow: hidden;
- transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
- width: 100%;
- margin: 0;
- padding: 0;
-}
-
-/* 展开状态 */
-.token-display-panel:not(.collapsed) {
- height: 80px;
- opacity: 1;
-}
-
-/* 收起状态 */
-.token-display-panel.collapsed {
- height: 0;
- opacity: 0;
- border: none;
- box-shadow: none;
-}
-
-.token-panel-content {
- padding: 16px 24px;
- height: 100%;
- transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.token-display-panel.collapsed .token-panel-content {
- opacity: 0;
- pointer-events: none;
-}
-
-.token-stats {
- display: flex;
- gap: 32px;
- align-items: center;
- justify-content: center;
- font-size: 13px;
- height: 100%;
-}
-
-.token-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 4px;
- min-width: 80px;
-}
-
-.token-label {
- color: var(--claude-text-secondary);
- font-size: 11px;
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.token-value {
- color: var(--claude-text);
- font-weight: 600;
- font-size: 18px;
- font-variant-numeric: tabular-nums;
-}
-
-.token-value.current {
- color: var(--claude-accent);
- font-size: 20px;
-}
-.token-value.input { color: var(--claude-success); }
-.token-value.output { color: var(--claude-warning); }
-
-.token-separator {
- width: 1px;
- height: 35px;
- background: linear-gradient(to bottom,
- transparent,
- rgba(218, 119, 86, 0.25) 20%,
- rgba(218, 119, 86, 0.25) 80%,
- transparent
- );
- margin: 0 8px;
-}
-
-/* 切换按钮 - 独立定位 */
-.token-toggle-btn {
- position: absolute;
- right: 24px;
- bottom: -18px; /* 相对于wrapper底部 */
- width: 36px;
- height: 36px;
- border-radius: 50%;
- border: 2px solid rgba(218, 119, 86, 0.3);
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
- z-index: 15;
- font-size: 14px;
- font-weight: bold;
-}
-
-/* 展开状态 */
-.token-toggle-btn:not(.collapsed) {
- background: linear-gradient(135deg, #ffffff 0%, rgba(255, 248, 242, 0.9) 100%);
- color: var(--claude-accent);
- box-shadow: 0 3px 10px rgba(189, 93, 58, 0.18);
-}
-
-/* 收起状态 - 在标题栏下方露出一半 */
-.token-toggle-btn.collapsed {
- background: linear-gradient(135deg, var(--claude-accent) 0%, var(--claude-accent-strong) 100%);
- color: #fff8f2;
- border-color: rgba(255, 248, 242, 0.55);
- box-shadow: 0 3px 11px rgba(189, 93, 58, 0.22);
-}
-
-.token-toggle-btn:hover {
- transform: scale(1.05);
- box-shadow: 0 5px 16px rgba(189, 93, 58, 0.26);
-}
-
-.token-toggle-btn.collapsed:hover {
- background: linear-gradient(135deg, var(--claude-button-hover) 0%, var(--claude-button-active) 100%);
-}
-
-.token-toggle-btn:active {
- transform: scale(1.02);
-}
-
-/* 箭头样式 - 移除浮动动画 */
-.token-toggle-btn span {
- transition: all 0.3s ease;
- display: inline-block;
-}
-
-/* 移除动画效果 */
-/* .token-toggle-btn:not(.collapsed) span {
- animation: arrowBounceUp 2s ease-in-out infinite;
-}
-
-.token-toggle-btn.collapsed span {
- animation: arrowBounceDown 2s ease-in-out infinite;
-} */
-
-/* 保留动画定义,但不使用 */
-@keyframes arrowBounceUp {
- 0%, 100% { transform: translateY(0); }
- 50% { transform: translateY(-3px); }
-}
-
-@keyframes arrowBounceDown {
- 0%, 100% { transform: translateY(0); }
- 50% { transform: translateY(3px); }
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
- .token-stats {
- gap: 16px;
- }
-
- .token-item {
- min-width: 60px;
- }
-
- .token-value {
- font-size: 15px;
- }
-
- .token-value.current {
- font-size: 17px;
- }
-
- .token-label {
- font-size: 10px;
- }
-
- .token-toggle-btn {
- width: 32px;
- height: 32px;
- font-size: 12px;
- right: 16px;
- }
-}
/* Markdown列表样式 - 修复偏左问题 */
.text-content ul,
.text-content ol {
diff --git a/sub_agent/web_server.py b/sub_agent/web_server.py
index 3728e97..b12569e 100644
--- a/sub_agent/web_server.py
+++ b/sub_agent/web_server.py
@@ -74,6 +74,7 @@ user_terminals: Dict[str, WebTerminal] = {}
terminal_rooms: Dict[str, set] = {}
connection_users: Dict[str, str] = {}
stop_flags: Dict[str, Dict[str, Any]] = {}
+terminated_tasks: set = set()
DEFAULT_PORT = 8092
@@ -269,6 +270,7 @@ def _purge_sub_agent_task(task_id: str):
terminal.close()
except Exception:
pass
+ terminated_tasks.discard(task_id)
room_sids = sub_agent_rooms.pop(task_id, set())
for sid in list(room_sids):
sub_agent_connections.pop(sid, None)
@@ -2065,6 +2067,7 @@ def create_sub_agent_task(payload: Dict[str, Any]) -> Dict[str, Any]:
metadata=metadata,
message_callback=None,
)
+ terminal.task_id = task_id
def _finish_hook(result: Dict[str, Any]):
reason = result.get("reason", "")
@@ -2152,10 +2155,10 @@ def force_terminate_sub_agent(task_id: str) -> Dict[str, Any]:
update_sub_agent_task(task_id, status="terminated")
broadcast_sub_agent_event(task_id, "sub_agent_status", {
"status": "terminated",
- "message": "子智能体已强制关闭"
+ "message": "子智能体已被手动关闭"
})
_purge_sub_agent_task(task_id)
- return {"success": True, "message": "子智能体已强制关闭"}
+ return {"success": True, "message": "子智能体已被手动关闭"}
# ==========================================
@@ -2438,6 +2441,7 @@ def process_message_task(terminal: WebTerminal, message: str, sender, client_sid
stop_flags[client_sid]['task'] = task
stop_flags[client_sid]['terminal'] = terminal
+ task_id = getattr(terminal, "task_id", None)
try:
loop.run_until_complete(task)
except asyncio.CancelledError:
@@ -2447,6 +2451,9 @@ def process_message_task(terminal: WebTerminal, message: str, sender, client_sid
'reason': 'user_requested'
})
reset_system_state(terminal)
+ finally:
+ if task_id:
+ terminated_tasks.discard(task_id)
loop.close()
except Exception as e:
@@ -3145,6 +3152,10 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
for iteration in range(max_iterations):
total_iterations += 1
debug_log(f"\n--- 迭代 {iteration + 1}/{max_iterations} 开始 ---")
+ task_id = getattr(web_terminal, "task_id", None)
+ if task_id and task_id in terminated_tasks:
+ sender('system_message', {'content': '🛑 子智能体已被手动关闭。'})
+ break
# 检查是否超过总工具调用限制
if total_tool_calls >= MAX_TOTAL_TOOL_CALLS: