agent-Specialization/core/web_terminal.py
JOJO 478d24c4dd feat(versioning): 重构版本控制为对话级浅备份 + 可选完全备份
本次变更是对版本控制功能的大范围重构,核心目标:
- 默认使用浅备份(只追踪 write_file/edit_file 实际修改的文件),避免大工作区首次初始化耗时十几秒;
- 保留可选的完全备份(git write-tree),不污染 git log;
- 回溯时统一选择范围(仅对话 / 对话+工作区)和模式(覆盖 / 复制);
- 修复复制对话、侧边栏同步、新建对话延迟、diff 统计与渲染等连锁问题。

后端改动:
- modules/shallow_versioning.py(新增)
  - 实现 ShallowVersioningManager:track_edit、make_snapshot、rewind、list_snapshots。
  - 基于 difflib.SequenceMatcher 计算真实 insertions/deletions,replace 场景正确显示 +N/-M。
  - diff 详情使用结构化 add/remove/context 行,不再解析 unified_diff 文本。
- modules/versioning_manager.py
  - 完全备份从 git commit 改为 git write-tree,字段 commit/parent_commit 改为 tree_hash/parent_tree_hash。
  - 恢复改用 read-tree + checkout-index;create_checkpoint 支持 diff_summary 参数。
  - get_checkpoint_detail 保留 API 字段名 last_commit/restored_commit 以兼容前端。
- core/main_terminal_parts/tools_execution.py
  - write_file/edit_file 在执行前调用浅备份 track_edit,保留编辑前状态。
- server/chat_flow_task_main.py
  - 消息轮次结束时根据 backup_mode 选择完全备份 create_checkpoint 或浅备份 make_snapshot。
  - 浅备份模式下同时写入 ConversationVersioningManager 检查点行,让前端能统一展示。
- server/conversation.py
  - restore/detail/list API 支持 shallow 模式;list/detail 对浅备份实时重新计算统计。
  - 创建对话时读取 personalization 的 versioning_backup_mode,shallow 模式固定 tracking_mode 为 conversation_only。
- core/web_terminal.py
  - _ensure_conversation_versioning_enabled 读取默认备份模式并写入 versioning meta。
- modules/file_manager/crud_mixin.py / replace_mixin.py
  - write_file / edit_file 返回 original_file / new_file / replacement_details(真实旧行/新行)。
- modules/personalization_manager.py
  - 新增 versioning_enabled_by_default、versioning_backup_mode(shallow/full)。
- utils/perf_log.py(新增)
  - 创建新对话全链路性能日志,用于定位延迟瓶颈。
- utils/conversation_manager/{index,list_search,crud}_mixin.py
  - 加性能日志;index_mixin 主动检查新增对话文件,修复侧边栏不同步。

前端改动:
- static/src/components/overlay/VersioningDialog.vue
  - 去掉顶部管理范围下拉;底部新增回溯范围与回溯模式选择。
  - diff 详情改为 div.diff-line 网格布局(marker + content),支持横向滚动,状态显示缩写 A/M/D。
  - 修复 pre 标签空白黑行、容器被挤窄导致折行等问题。
- static/src/app/methods/versioning.ts
  - 移除 mismatch 弹窗逻辑;支持 copy 模式;selectVersioningCheckpoint 增强错误捕获。
- static/src/components/personalization/PersonalizationDrawer.vue
  - 「工作区与权限」新增版本控制默认开关与备份方式选择。
- static/src/stores/personalization.ts
  - 新增 versioning_enabled_by_default、versioning_backup_mode 字段。
- static/src/components/chat/actions/ToolAction.vue / toolRenderers.ts
  - edit_file / write_file 工具展示基于 result.details / result.new_file 的真实替换结果。
- static/src/app/state.ts / watchers.ts / App.vue
  - 新增 versioningInitializingBackupToastId,完全备份初始化时显示 toast。
- static/src/app/methods/conversation/action.ts / message/send.ts
  - host + 完全备份 + 默认开启时,创建新对话/从 /new 发送首条消息显示「正在初始化备份」toast。

验证:
- /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -m unittest test.test_server_refactor_smoke 通过。
- npm run build 通过。
- 宿主机与 docker 模式新建对话、编辑文件、回溯、复制对话均验证通过。
2026-07-07 12:07:44 +08:00

788 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
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}")