放弃完全隔离策略,改为在现有 MainTerminal/SubAgentManager/SubAgentTask 主链路 按对话级开关 metadata.multi_agent_mode=true 增加多智能体分支。 新增模块: - modules/multi_agent/__init__.py: 模块入口 - modules/multi_agent/role_store.py: 角色 Markdown Frontmatter 解析与归档 - modules/multi_agent/state.py: 多智能体会话状态机与消息格式化 - modules/multi_agent/prompts.py: 主智能体(Team Leader) + 子智能体提示词 - modules/multi_agent/tools.py: 9 个主智能体工具 + 4 个子智能体工具定义 - server/multi_agent.py: /multiagent/new 页面 + /api/multiagent/* 蓝图 现有代码改动: - modules/sub_agent/task.py: 扩展 multi_agent_mode/multi_agent_state/display_name 字段, 增加 ask_master/ask_other_agent/answer_other_agent/list_active_sub_agents 工具处理逻辑, 子智能体自然结束 assistant 输出即本轮结束(不调用 finish_task),上下文保留。 - modules/sub_agent/manager.py: create_sub_agent 增加 multi_agent_mode/role_id/display_name 参数, 增加 get_or_create_multi_agent_state/get_multi_agent_state/inject_message_to_sub_agent/_on_multi_agent_task_done 方法。 - core/main_terminal_parts/tools_definition/agent_tools.py: 多智能体模式下用 modules.multi_agent.tools 替换旧版工具集。 - core/main_terminal_parts/context/messages.py: 多智能体模式下追加 Team Leader 系统提示词。 - core/main_terminal_parts/tools_execution.py: create_sub_agent handler 增加多智能体分支,新增 send_message_to_sub_agent/ask_sub_agent/answer_sub_agent_question/create_custom_agent/list_agents/list_active_sub_agents handler。 - core/web_terminal.py: load_conversation 时检测 metadata.multi_agent_mode 设置 self.multi_agent_mode。 - server/app_legacy.py: 注册 multi_agent_bp 蓝图。 前端改动: - static/src/auth/LoginApp.vue: 登录页增加'多智能体模式(beta)'按钮 - static/src/app/methods/ui/route.ts: 识别 /multiagent/new 和 /multiagent/conv_xxx 路径,进入多智能体模式并创建带 metadata.multi_agent_mode=true 的对话 - static/src/app/state.ts: 增加 multiAgentMode 状态字段 数据: - ~/.astrion/astrion/host/mutiagents/agents/: 4 个预置角色 ui-operator / full-stack-engineer / code-reviewer / researcher - ~/.astrion/astrion/host/mutiagents/conversations/: 会话数据 验证:所有 Python 文件语法检查通过;冒烟测试 test.test_server_refactor_smoke 6 项全通过;前端构建通过(6.04s);模块导入与功能断言测试全部通过。
796 lines
36 KiB
Python
796 lines
36 KiB
Python
# core/web_terminal.py - Web终端(集成对话持久化)
|
||
|
||
import json
|
||
import time
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional, Callable, TYPE_CHECKING
|
||
import os
|
||
from core.main_terminal import MainTerminal
|
||
from utils.logger import setup_logger
|
||
from modules.personalization_manager import load_personalization_config
|
||
from modules.versioning_manager import ConversationVersioningManager, VersioningError
|
||
from utils.perf_log import perf_log, PerfTimer
|
||
try:
|
||
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
|
||
except ImportError:
|
||
import sys
|
||
from pathlib import Path
|
||
project_root = Path(__file__).resolve().parents[1]
|
||
if str(project_root) not in sys.path:
|
||
sys.path.insert(0, str(project_root))
|
||
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
|
||
from modules.terminal_manager import TerminalManager
|
||
|
||
if TYPE_CHECKING:
|
||
from modules.user_container_manager import ContainerHandle
|
||
|
||
logger = setup_logger(__name__)
|
||
|
||
class WebTerminal(MainTerminal):
|
||
"""Web版本的终端,继承自MainTerminal,包含对话持久化功能"""
|
||
|
||
def _ensure_conversation(self):
|
||
"""确保Web端在首次进入时自动加载或创建对话"""
|
||
if self.context_manager.current_conversation_id:
|
||
return
|
||
|
||
latest_list = self.context_manager.get_conversation_list(limit=1, offset=0)
|
||
conversations = latest_list.get("conversations", []) if latest_list else []
|
||
|
||
if conversations:
|
||
latest = conversations[0]
|
||
conv_id = latest.get("id")
|
||
if conv_id:
|
||
result = self.load_conversation(conv_id)
|
||
if result.get("success"):
|
||
print(f"[WebTerminal] 已加载最近对话: {conv_id}")
|
||
return
|
||
|
||
conversation_id = self.context_manager.start_new_conversation(
|
||
project_path=self.project_path,
|
||
thinking_mode=self.thinking_mode
|
||
)
|
||
print(f"[WebTerminal] 自动创建新对话: {conversation_id}")
|
||
|
||
def __init__(
|
||
self,
|
||
project_path: str,
|
||
thinking_mode: bool = False,
|
||
run_mode: Optional[str] = None,
|
||
message_callback: Optional[Callable] = None,
|
||
data_dir: Optional[str] = None,
|
||
container_session: Optional["ContainerHandle"] = None,
|
||
usage_tracker: Optional[object] = None,
|
||
):
|
||
# 调用父类初始化(包含对话持久化功能)
|
||
super().__init__(
|
||
project_path,
|
||
thinking_mode,
|
||
run_mode=run_mode,
|
||
data_dir=data_dir,
|
||
container_session=container_session,
|
||
usage_tracker=usage_tracker
|
||
)
|
||
|
||
# Web特有属性
|
||
self.message_callback = message_callback
|
||
self.web_mode = True
|
||
|
||
# 默认允许输出(api_client.web_mode=False 表示允许 _print),若需静默可设置 WEB_API_SILENT=1
|
||
self.api_client.web_mode = bool(os.environ.get("WEB_API_SILENT"))
|
||
|
||
# 重新初始化终端管理器
|
||
self.terminal_manager = TerminalManager(
|
||
project_path=project_path,
|
||
max_terminals=MAX_TERMINALS,
|
||
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
|
||
terminal_display_size=TERMINAL_DISPLAY_SIZE,
|
||
broadcast_callback=message_callback,
|
||
container_session=self.container_session
|
||
)
|
||
# 让 run_command 与实时终端共享同一容器环境
|
||
self.terminal_ops.attach_terminal_manager(self.terminal_manager)
|
||
|
||
print(f"[WebTerminal] 初始化完成,项目路径: {project_path}")
|
||
print(f"[WebTerminal] 初始模式: {self.run_mode}")
|
||
print(f"[WebTerminal] 对话管理已就绪")
|
||
|
||
# 设置token更新回调
|
||
if message_callback is not None:
|
||
self.context_manager._web_terminal_callback = message_callback
|
||
print(f"[WebTerminal] 实时token统计已启用")
|
||
else:
|
||
print(f"[WebTerminal] 警告:message_callback为None,无法启用实时token统计")
|
||
# ===========================================
|
||
# 新增:对话管理相关方法(Web版本)
|
||
# ===========================================
|
||
|
||
def create_new_conversation(self, thinking_mode: bool = None, run_mode: Optional[str] = None) -> Dict:
|
||
"""
|
||
创建新对话(Web版本)
|
||
|
||
Args:
|
||
thinking_mode: 思考模式,None则使用当前设置
|
||
run_mode: 显式的运行模式(fast/thinking/deep)
|
||
|
||
Returns:
|
||
Dict: 包含新对话信息
|
||
"""
|
||
perf_log("create_new_conversation enter")
|
||
t0 = time.perf_counter()
|
||
prefer_defaults = thinking_mode is None and run_mode is None
|
||
thinking_mode_explicit = thinking_mode is not None
|
||
|
||
# 先加载个性化默认配置(用于 start_new_conversation 的 metadata)
|
||
prefs = {}
|
||
preferred_model = None
|
||
if prefer_defaults:
|
||
try:
|
||
prefs = load_personalization_config(self.data_dir)
|
||
except Exception as exc:
|
||
logger.warning("加载个性化偏好失败,将使用内置默认: %s", exc)
|
||
preferred_model = prefs.get("default_model")
|
||
preferred_mode = prefs.get("default_run_mode")
|
||
preferred_permission_mode = prefs.get("default_permission_mode") or None
|
||
if preferred_permission_mode not in ("readonly", "approval", "auto_approval", "unrestricted"):
|
||
try:
|
||
preferred_permission_mode = self.get_permission_mode()
|
||
except Exception:
|
||
preferred_permission_mode = None
|
||
if not isinstance(preferred_permission_mode, str) or not preferred_permission_mode.strip():
|
||
preferred_permission_mode = "unrestricted"
|
||
if isinstance(preferred_mode, str) and preferred_mode.lower() in {"fast", "thinking", "deep"}:
|
||
try:
|
||
self.set_run_mode(preferred_mode.lower())
|
||
except ValueError as exc:
|
||
logger.warning("忽略无效默认运行模式 %s: %s", preferred_mode, exc)
|
||
else:
|
||
# 未配置默认模式时回到快速模式
|
||
self.set_run_mode("fast")
|
||
try:
|
||
self.set_permission_mode(preferred_permission_mode, persist=False)
|
||
except Exception:
|
||
self.set_permission_mode("unrestricted", persist=False)
|
||
|
||
if isinstance(run_mode, str):
|
||
try:
|
||
self.set_run_mode(run_mode)
|
||
thinking_mode = self.thinking_mode
|
||
except ValueError:
|
||
logger.warning("无效的 run_mode 参数: %s", run_mode)
|
||
elif thinking_mode_explicit:
|
||
# run_mode 未显式传入但思考开关给定时,基于布尔值决定模式
|
||
try:
|
||
self.set_run_mode("thinking" if bool(thinking_mode) else "fast")
|
||
except ValueError:
|
||
pass
|
||
|
||
if thinking_mode is None:
|
||
thinking_mode = self.thinking_mode
|
||
|
||
try:
|
||
# 先创建新对话。start_new_conversation 会先 save_current_conversation(),
|
||
# 此时 terminal.model_key 仍是旧对话的模型,避免把旧对话覆盖成默认模型。
|
||
conversation_id = self.context_manager.start_new_conversation(
|
||
project_path=self.project_path,
|
||
thinking_mode=thinking_mode,
|
||
run_mode=self.run_mode,
|
||
metadata_overrides={
|
||
"permission_mode": self.get_permission_mode(),
|
||
"execution_mode": self.get_execution_mode() if hasattr(self, "get_execution_mode") else "sandbox",
|
||
"pending_permission_mode": None,
|
||
"pending_execution_mode": None,
|
||
# frozen_*_prompt 不在创建时预设,由第一次 build_messages 根据当时的实际模式懒加载并冻结
|
||
},
|
||
)
|
||
|
||
# 新对话创建完成后再应用默认模型(此时旧对话已安全保存)。
|
||
if prefer_defaults and preferred_model:
|
||
# 新对话视为“干净”会话,清除图片限制便于切换模型
|
||
self.context_manager.has_images = False
|
||
self.context_manager.has_videos = False
|
||
try:
|
||
self.set_model(preferred_model)
|
||
except Exception as exc:
|
||
logger.warning("忽略无效默认模型 %s: %s", preferred_model, exc)
|
||
|
||
# 把默认模型同步到新对话的 metadata
|
||
try:
|
||
self.context_manager.conversation_manager.save_conversation(
|
||
conversation_id=conversation_id,
|
||
messages=self.context_manager.conversation_history,
|
||
project_path=str(self.project_path),
|
||
todo_list=self.context_manager.todo_list,
|
||
thinking_mode=self.thinking_mode,
|
||
run_mode=self.run_mode,
|
||
model_key=self.model_key,
|
||
has_images=False,
|
||
has_videos=False,
|
||
)
|
||
except Exception as exc:
|
||
logger.warning("保存新对话默认模型失败: %s", exc)
|
||
|
||
perf_log("create_new_conversation before default versioning", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": conversation_id})
|
||
# 根据个性化设置默认开启版本控制
|
||
try:
|
||
default_versioning_enabled = bool((prefs or {}).get("versioning_enabled_by_default", True))
|
||
if default_versioning_enabled:
|
||
self._ensure_conversation_versioning_enabled(conversation_id)
|
||
except Exception as exc:
|
||
logger.warning("新对话应用默认版本控制失败: %s", exc)
|
||
perf_log("create_new_conversation after default versioning", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": conversation_id})
|
||
|
||
# 重置相关状态
|
||
if self.thinking_mode:
|
||
self.api_client.start_new_task()
|
||
|
||
self.current_session_id += 1
|
||
|
||
perf_log("create_new_conversation done", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": conversation_id})
|
||
return {
|
||
"success": True,
|
||
"conversation_id": conversation_id,
|
||
"message": f"已创建新对话: {conversation_id}"
|
||
}
|
||
except Exception as e:
|
||
perf_log("create_new_conversation error", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"error": str(e)})
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"message": f"创建新对话失败: {e}"
|
||
}
|
||
|
||
def _ensure_conversation_versioning_enabled(self, conversation_id: str) -> None:
|
||
"""为指定对话启用版本控制(初始快照)。"""
|
||
normalized_id = conversation_id if conversation_id.startswith("conv_") else f"conv_{conversation_id}"
|
||
t0 = time.perf_counter()
|
||
perf_log("_ensure_conversation_versioning_enabled enter", extra={"conv_id": normalized_id})
|
||
is_host = bool(getattr(self, "_is_host_mode", lambda: False)())
|
||
from modules.personalization_manager import load_personalization_config
|
||
personal_config = load_personalization_config(self.data_dir)
|
||
backup_mode = str(personal_config.get("versioning_backup_mode") or "shallow").strip().lower()
|
||
backup_mode = "full" if backup_mode == "full" else "shallow"
|
||
# 浅备份模式下不启用完整 workspace git 备份,只保留对话记录回溯
|
||
if backup_mode == "shallow":
|
||
tracking_mode = ConversationVersioningManager.TRACKING_MODE_CONVERSATION_ONLY
|
||
elif is_host:
|
||
tracking_mode = ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
|
||
else:
|
||
tracking_mode = ConversationVersioningManager.TRACKING_MODE_CONVERSATION_ONLY
|
||
manager = ConversationVersioningManager(
|
||
project_path=self.project_path,
|
||
data_dir=self.data_dir,
|
||
conversation_id=normalized_id,
|
||
)
|
||
perf_log("_ensure_conversation_versioning_enabled manager created", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": normalized_id})
|
||
meta = manager.set_enabled(enabled=True, mode="overwrite", tracking_mode=tracking_mode)
|
||
perf_log("_ensure_conversation_versioning_enabled set_enabled done", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": normalized_id})
|
||
conv_data = self.context_manager.conversation_manager.load_conversation(normalized_id) or {}
|
||
snapshot_payload = {
|
||
"conversation_id": normalized_id,
|
||
"title": conv_data.get("title"),
|
||
"metadata": conv_data.get("metadata") or {},
|
||
"messages": conv_data.get("messages") or [],
|
||
"message_index": -1,
|
||
"run_status": "initial",
|
||
}
|
||
init_result = manager.ensure_initial_checkpoint(
|
||
workspace_path=str(self.project_path),
|
||
conversation_snapshot=snapshot_payload,
|
||
tracking_mode=tracking_mode,
|
||
)
|
||
perf_log("_ensure_conversation_versioning_enabled initial checkpoint done", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": normalized_id})
|
||
init_row = init_result.get("row") or {}
|
||
if init_row.get("tree_hash"):
|
||
meta["last_tree_hash"] = init_row.get("tree_hash")
|
||
self.context_manager.conversation_manager.update_conversation_metadata(
|
||
normalized_id,
|
||
{
|
||
"versioning": {
|
||
"enabled": True,
|
||
"mode": "overwrite",
|
||
"tracking_mode": tracking_mode,
|
||
"backup_mode": backup_mode,
|
||
"last_commit": meta.get("last_tree_hash"),
|
||
"last_input_seq": int(meta.get("last_input_seq") or 0),
|
||
"updated_at": datetime.now().isoformat(),
|
||
}
|
||
},
|
||
)
|
||
perf_log("_ensure_conversation_versioning_enabled done", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": normalized_id})
|
||
|
||
def load_conversation(self, conversation_id: str) -> Dict:
|
||
"""
|
||
加载指定对话(Web版本)
|
||
|
||
Args:
|
||
conversation_id: 对话ID
|
||
|
||
Returns:
|
||
Dict: 加载结果
|
||
"""
|
||
try:
|
||
success = self.context_manager.load_conversation_by_id(conversation_id)
|
||
if success:
|
||
# 根据对话元数据同步思考模式
|
||
try:
|
||
conv_data = self.context_manager.conversation_manager.load_conversation(conversation_id) or {}
|
||
meta = conv_data.get("metadata", {}) or {}
|
||
mode = bool(meta.get("thinking_mode", self.thinking_mode))
|
||
self.thinking_mode = mode
|
||
self.api_client.thinking_mode = mode
|
||
self.api_client.start_new_task()
|
||
permission_mode = str(meta.get("permission_mode") or "").strip().lower()
|
||
if permission_mode:
|
||
try:
|
||
self.set_permission_mode(permission_mode, persist=False)
|
||
except Exception:
|
||
pass
|
||
execution_mode = str(meta.get("execution_mode") or "").strip().lower()
|
||
if execution_mode in {"sandbox", "direct"}:
|
||
try:
|
||
self.set_execution_mode(execution_mode)
|
||
except Exception:
|
||
pass
|
||
network_permission = str(meta.get("network_permission") or "").strip().lower()
|
||
if network_permission in {"restricted", "full", "none"}:
|
||
try:
|
||
self.set_network_permission(network_permission)
|
||
except Exception:
|
||
pass
|
||
saved_model_key = meta.get("model_key")
|
||
if saved_model_key:
|
||
try:
|
||
self.set_model(saved_model_key)
|
||
except Exception as exc:
|
||
logger.warning("加载对话模型 %s 失败: %s", saved_model_key, exc)
|
||
self.pending_permission_mode = str(meta.get("pending_permission_mode") or "").strip().lower() or None
|
||
self.pending_execution_mode = str(meta.get("pending_execution_mode") or "").strip().lower() or None
|
||
self.pending_network_permission = str(meta.get("pending_network_permission") or "").strip().lower() or None
|
||
# 多智能体模式:以会话 metadata.multi_agent_mode 为唯一切换开关
|
||
self.multi_agent_mode = bool(meta.get("multi_agent_mode", False))
|
||
# 同步主智能体 sub_agent_manager 的 开关
|
||
try:
|
||
if hasattr(self, "sub_agent_manager"):
|
||
self.sub_agent_manager.multi_agent_mode = self.multi_agent_mode
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
# 重置相关状态
|
||
self.current_session_id += 1
|
||
|
||
# 获取对话信息
|
||
conversation_data = self.context_manager.conversation_manager.load_conversation(conversation_id)
|
||
if not conversation_data:
|
||
return {
|
||
"success": False,
|
||
"error": "对话数据缺失",
|
||
"message": f"对话数据缺失: {conversation_id}"
|
||
}
|
||
|
||
return {
|
||
"success": True,
|
||
"conversation_id": conversation_id,
|
||
"title": conversation_data.get("title", "未知对话"),
|
||
"messages_count": len(self.context_manager.conversation_history),
|
||
"run_mode": self.run_mode,
|
||
"thinking_mode": self.thinking_mode,
|
||
"model_key": getattr(self, "model_key", None),
|
||
"message": f"对话已加载: {conversation_id}"
|
||
}
|
||
else:
|
||
return {
|
||
"success": False,
|
||
"error": "对话不存在或加载失败",
|
||
"message": f"对话加载失败: {conversation_id}"
|
||
}
|
||
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"message": f"加载对话异常: {e}"
|
||
}
|
||
|
||
def get_conversations_list(self, limit: int = 20, offset: int = 0, non_empty: bool = False) -> Dict:
|
||
"""获取对话列表(Web版本)"""
|
||
try:
|
||
result = self.context_manager.get_conversation_list(limit=limit, offset=offset, non_empty=non_empty)
|
||
return {
|
||
"success": True,
|
||
"data": result
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"message": f"获取对话列表失败: {e}"
|
||
}
|
||
|
||
def delete_conversation(self, conversation_id: str) -> Dict:
|
||
"""删除指定对话(Web版本)"""
|
||
try:
|
||
success = self.context_manager.delete_conversation_by_id(conversation_id)
|
||
if success:
|
||
return {
|
||
"success": True,
|
||
"message": f"对话已删除: {conversation_id}"
|
||
}
|
||
else:
|
||
return {
|
||
"success": False,
|
||
"error": "删除失败",
|
||
"message": f"对话删除失败: {conversation_id}"
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"message": f"删除对话异常: {e}"
|
||
}
|
||
|
||
def search_conversations(self, query: str, limit: int = 20) -> Dict:
|
||
"""搜索对话(Web版本)"""
|
||
try:
|
||
results = self.context_manager.search_conversations(query, limit)
|
||
return {
|
||
"success": True,
|
||
"results": results,
|
||
"count": len(results)
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"message": f"搜索对话失败: {e}"
|
||
}
|
||
|
||
# ===========================================
|
||
# 修改现有方法,保持兼容性
|
||
# ===========================================
|
||
|
||
def get_status(self) -> Dict:
|
||
"""获取系统状态(Web版本,集成对话信息)"""
|
||
# 获取基础状态
|
||
context_status = self.context_manager.check_context_size()
|
||
memory_stats = self.memory_manager.get_memory_stats()
|
||
structure = self.context_manager.get_project_structure()
|
||
|
||
# 聚焦功能已废弃
|
||
focused_files_dict = {}
|
||
|
||
# 终端状态
|
||
terminal_status = None
|
||
if self.terminal_manager:
|
||
terminal_status = self.terminal_manager.list_terminals()
|
||
|
||
# 构建状态信息
|
||
limit_bytes = getattr(self, "project_storage_limit_bytes", None)
|
||
status = {
|
||
"project_path": self.project_path,
|
||
"thinking_mode": self.thinking_mode,
|
||
"thinking_status": self.get_thinking_mode_status(),
|
||
"run_mode": self.run_mode,
|
||
"model_key": getattr(self, "model_key", None),
|
||
"permission_mode": self.get_permission_mode() if hasattr(self, "get_permission_mode") else "unrestricted",
|
||
"execution_mode": self.get_execution_mode_state() if hasattr(self, "get_execution_mode_state") else {"mode": "sandbox"},
|
||
"network_permission": self.get_network_permission() if hasattr(self, "get_network_permission") else "restricted",
|
||
"pending_runtime_modes": self.get_pending_runtime_modes() if hasattr(self, "get_pending_runtime_modes") else {},
|
||
"execution_mode_enabled": bool(self._is_host_mode()) and getattr(self, "user_role", "user") == "admin",
|
||
"network_permission_enabled": bool(self._is_host_mode()) and getattr(self, "user_role", "user") == "admin",
|
||
"has_images": getattr(self.context_manager, "has_images", False),
|
||
"has_videos": getattr(self.context_manager, "has_videos", False),
|
||
"context": {
|
||
"usage_percent": context_status['usage_percent'],
|
||
"total_size": context_status['sizes']['total'],
|
||
"conversation_count": len(self.context_manager.conversation_history)
|
||
},
|
||
"focused_files": focused_files_dict,
|
||
"focused_files_count": 0,
|
||
"terminals": terminal_status,
|
||
"project": {
|
||
"total_files": structure['total_files'],
|
||
"total_size": structure['total_size'],
|
||
"limit_bytes": limit_bytes,
|
||
"limit_label": self.project_storage_limit,
|
||
"usage_percent": (structure['total_size'] / limit_bytes * 100) if limit_bytes else None
|
||
},
|
||
"memory": {
|
||
"main": memory_stats['main_memory']['lines'],
|
||
"task": memory_stats['task_memory']['lines']
|
||
},
|
||
# 新增:对话状态
|
||
"conversation": {
|
||
"current_id": self.context_manager.current_conversation_id,
|
||
# 首屏不再同步计算全量对话统计;统计面板按需调用
|
||
# /api/conversations/statistics,避免刷新时读取所有历史对话文件。
|
||
"total_conversations": 0,
|
||
"total_messages": 0,
|
||
"total_tools": 0
|
||
}
|
||
}
|
||
status["todo_list"] = self.context_manager.get_todo_snapshot()
|
||
|
||
return status
|
||
|
||
def get_thinking_mode_status(self) -> str:
|
||
"""获取思考模式状态描述"""
|
||
return "思考模式" if self.thinking_mode else "快速模式"
|
||
|
||
def broadcast(self, event_type: str, data: Dict):
|
||
"""广播事件到WebSocket"""
|
||
if self.message_callback:
|
||
payload = dict(data or {})
|
||
payload.setdefault('conversation_id', self.context_manager.current_conversation_id)
|
||
self.message_callback(event_type, payload)
|
||
|
||
# ===========================================
|
||
# 覆盖父类方法,添加Web特有的广播功能
|
||
# ===========================================
|
||
|
||
async def handle_tool_call(self, tool_name: str, arguments: Dict) -> str:
|
||
"""
|
||
处理工具调用(Web版本)
|
||
覆盖父类方法,添加增强的实时广播功能
|
||
"""
|
||
# 立即广播工具执行开始事件(不等待)
|
||
self.broadcast('tool_execution_start', {
|
||
'tool': tool_name,
|
||
'arguments': arguments,
|
||
'status': 'executing',
|
||
'message': f'正在执行 {tool_name}...'
|
||
})
|
||
|
||
# 对于某些工具,发送更详细的状态
|
||
if tool_name == "create_file":
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'creating',
|
||
'detail': f'创建文件: {arguments.get("path", "未知路径")}'
|
||
})
|
||
elif tool_name == "read_file":
|
||
read_type = arguments.get("type", "read")
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'reading',
|
||
'detail': f'读取文件({read_type}): {arguments.get("path", "未知路径")}'
|
||
})
|
||
elif tool_name == "delete_file":
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'deleting',
|
||
'detail': f'删除文件: {arguments.get("path", "未知路径")}'
|
||
})
|
||
elif tool_name == "web_search":
|
||
query = arguments.get("query", "")
|
||
filters = []
|
||
topic = arguments.get("topic")
|
||
if topic:
|
||
filters.append(f"topic={topic}")
|
||
else:
|
||
filters.append("topic=general")
|
||
if arguments.get("time_range"):
|
||
filters.append(f"time_range={arguments['time_range']}")
|
||
if arguments.get("days") is not None:
|
||
filters.append(f"days={arguments.get('days')}")
|
||
if arguments.get("start_date") and arguments.get("end_date"):
|
||
filters.append(f"{arguments.get('start_date')}~{arguments.get('end_date')}")
|
||
if arguments.get("country"):
|
||
filters.append(f"country={arguments.get('country')}")
|
||
include_domains = arguments.get("include_domains")
|
||
if isinstance(include_domains, list) and include_domains:
|
||
filters.append(f"include_domains={len(include_domains)}")
|
||
filter_text = " | ".join(filter_item for filter_item in filters if filter_item)
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'searching',
|
||
'detail': f'搜索: {query}' + (f' ({filter_text})' if filter_text else '')
|
||
})
|
||
elif tool_name == "extract_webpage":
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'extracting',
|
||
'detail': f'提取网页: {arguments.get("url", "")}'
|
||
})
|
||
elif tool_name == "save_webpage":
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'saving_webpage',
|
||
'detail': f'保存网页: {arguments.get("url", "")}'
|
||
})
|
||
elif tool_name == "run_command":
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'running_command',
|
||
'detail': f'执行命令: {arguments.get("command", "")}'
|
||
})
|
||
elif tool_name == "terminal_session":
|
||
action = arguments.get("action", "")
|
||
session_name = arguments.get("session_name", "default")
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': f'terminal_{action}',
|
||
'detail': f'终端操作: {action} - {session_name}'
|
||
})
|
||
elif tool_name == "terminal_input":
|
||
command = arguments.get("command", "")
|
||
# 只显示命令的前50个字符避免过长
|
||
display_command = command[:50] + "..." if len(command) > 50 else command
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'sending_input',
|
||
'detail': f'发送终端输入: {display_command}'
|
||
})
|
||
elif tool_name == "sleep":
|
||
seconds = arguments.get("seconds", 1)
|
||
reason = arguments.get("reason", "等待操作完成")
|
||
self.broadcast('tool_status', {
|
||
'tool': tool_name,
|
||
'status': 'waiting',
|
||
'detail': f'等待 {seconds} 秒: {reason}'
|
||
})
|
||
|
||
# 调用父类的工具处理(包含我们的新逻辑)
|
||
result = await super().handle_tool_call(tool_name, arguments)
|
||
logger.debug(
|
||
"[SubAgent][WebTerminal] tool=%s 执行完成,result前200=%s",
|
||
tool_name,
|
||
result[:200] if isinstance(result, str) else result,
|
||
)
|
||
|
||
# 解析结果并广播工具结束事件
|
||
try:
|
||
result_data = json.loads(result)
|
||
success = result_data.get('success', False)
|
||
|
||
# 特殊处理某些错误类型
|
||
if not success:
|
||
error_msg = result_data.get('error')
|
||
if not error_msg:
|
||
error_msg = result_data.get('message')
|
||
if not error_msg:
|
||
error_msg = '执行失败'
|
||
if not isinstance(error_msg, str):
|
||
error_msg = str(error_msg)
|
||
|
||
# 检查是否是参数预检查失败
|
||
if error_msg and ('参数过大' in error_msg or '内容过长' in error_msg):
|
||
self.broadcast('tool_execution_end', {
|
||
'tool': tool_name,
|
||
'success': False,
|
||
'result': result_data,
|
||
'message': f'{tool_name} 执行失败: 参数过长',
|
||
'error_type': 'parameter_too_long',
|
||
'suggestion': result_data.get('suggestion', '建议分块处理')
|
||
})
|
||
elif error_msg and ('JSON解析' in error_msg or '参数解析失败' in error_msg):
|
||
self.broadcast('tool_execution_end', {
|
||
'tool': tool_name,
|
||
'success': False,
|
||
'result': result_data,
|
||
'message': f'{tool_name} 执行失败: 参数格式错误',
|
||
'error_type': 'parameter_format_error',
|
||
'suggestion': result_data.get('suggestion', '请检查参数格式')
|
||
})
|
||
else:
|
||
# 一般错误
|
||
self.broadcast('tool_execution_end', {
|
||
'tool': tool_name,
|
||
'success': False,
|
||
'result': result_data,
|
||
'message': f'{tool_name} 执行失败: {error_msg}',
|
||
'error_type': 'general_error'
|
||
})
|
||
else:
|
||
# 成功的情况
|
||
success_msg = result_data.get('message', f'{tool_name} 执行成功')
|
||
self.broadcast('tool_execution_end', {
|
||
'tool': tool_name,
|
||
'success': True,
|
||
'result': result_data,
|
||
'message': success_msg
|
||
})
|
||
|
||
except json.JSONDecodeError:
|
||
# 无法解析JSON结果
|
||
success = False
|
||
result_data = {'output': result, 'raw_result': True}
|
||
self.broadcast('tool_execution_end', {
|
||
'tool': tool_name,
|
||
'success': False,
|
||
'result': result_data,
|
||
'message': f'{tool_name} 返回了非JSON格式结果',
|
||
'error_type': 'invalid_result_format'
|
||
})
|
||
|
||
# 如果是终端相关操作,广播终端更新
|
||
if tool_name in ['terminal_session', 'terminal_input'] and self.terminal_manager:
|
||
try:
|
||
terminals = self.terminal_manager.get_terminal_list()
|
||
self.broadcast('terminal_list_update', {
|
||
'terminals': terminals,
|
||
'active': self.terminal_manager.active_terminal
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"广播终端更新失败: {e}")
|
||
|
||
# 如果是文件操作,广播文件树更新
|
||
if tool_name in ['create_file', 'delete_file', 'rename_file', 'create_folder', 'save_webpage']:
|
||
if not self.context_manager._is_host_mode_without_safety():
|
||
try:
|
||
structure = self.context_manager.get_project_structure()
|
||
self.broadcast('file_tree_update', structure)
|
||
except Exception as e:
|
||
logger.error(f"广播文件树更新失败: {e}")
|
||
|
||
# 如果是记忆操作,广播记忆状态更新
|
||
if tool_name == 'update_memory':
|
||
try:
|
||
memory_stats = self.memory_manager.get_memory_stats()
|
||
self.broadcast('memory_update', {
|
||
'main': memory_stats['main_memory']['lines'],
|
||
'task': memory_stats['task_memory']['lines']
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"广播记忆更新失败: {e}")
|
||
|
||
return result
|
||
|
||
def build_context(self) -> Dict:
|
||
"""构建上下文(Web版本)"""
|
||
context = super().build_context()
|
||
|
||
# 添加Web特有的上下文信息
|
||
context['web_mode'] = True
|
||
context['terminal_sessions'] = []
|
||
|
||
if self.terminal_manager:
|
||
for name, terminal in self.terminal_manager.terminals.items():
|
||
context['terminal_sessions'].append({
|
||
'name': name,
|
||
'is_active': name == self.terminal_manager.active_terminal,
|
||
'is_running': terminal.is_running
|
||
})
|
||
|
||
# 添加对话信息
|
||
context['conversation_info'] = {
|
||
'current_id': self.context_manager.current_conversation_id,
|
||
'messages_count': len(self.context_manager.conversation_history)
|
||
}
|
||
|
||
return context
|
||
|
||
async def confirm_action(self, action: str, arguments: Dict) -> bool:
|
||
"""
|
||
确认危险操作(Web版本)
|
||
在Web模式下,我们自动确认或通过WebSocket请求确认
|
||
"""
|
||
# 在Web模式下,暂时自动确认
|
||
# 未来可以通过WebSocket向前端请求确认
|
||
print(f"[WebTerminal] 自动确认操作: {action}")
|
||
|
||
# 广播确认事件,让前端知道正在执行危险操作
|
||
self.broadcast('dangerous_action', {
|
||
'action': action,
|
||
'arguments': arguments,
|
||
'auto_confirmed': True
|
||
})
|
||
|
||
return True
|
||
|
||
def __del__(self):
|
||
"""析构函数,确保资源释放"""
|
||
try:
|
||
# 保存当前对话
|
||
if hasattr(self, 'context_manager') and self.context_manager:
|
||
if self.context_manager.current_conversation_id:
|
||
self.context_manager.save_current_conversation()
|
||
|
||
# 关闭所有终端
|
||
if hasattr(self, 'terminal_manager') and self.terminal_manager:
|
||
self.terminal_manager.close_all()
|
||
|
||
except Exception as e:
|
||
print(f"[WebTerminal] 资源清理失败: {e}")
|