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 @@ - -
- -
-
-
-
- 当前上下文 - {{ formatTokenCount(currentContextTokens || 0) }} -
- -
- -
- 累计输入 - {{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }} -
- -
- 累计输出 - {{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }} -
-
-
-
- - - -
-
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: