agent-Specialization/modules/toolbox_container.py

157 lines
5.4 KiB
Python

# modules/toolbox_container.py - 专用工具容器管理器
import asyncio
import time
import uuid
import shlex
from pathlib import Path
from typing import Optional, Dict
from modules.persistent_terminal import PersistentTerminal
from config import (
TERMINAL_SANDBOX_MODE,
TERMINAL_SANDBOX_IMAGE,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_SHELL,
TERMINAL_SANDBOX_NETWORK,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
TERMINAL_SANDBOX_BINDS,
TERMINAL_SANDBOX_BIN,
TERMINAL_SANDBOX_NAME_PREFIX,
TERMINAL_SANDBOX_ENV,
TERMINAL_SANDBOX_REQUIRE,
TOOLBOX_TERMINAL_IDLE_SECONDS,
)
def _build_sandbox_options() -> Dict:
"""构造与终端一致的沙箱配置."""
name_prefix = TERMINAL_SANDBOX_NAME_PREFIX
if name_prefix:
name_prefix = f"{name_prefix}-toolbox"
else:
name_prefix = "toolbox-term"
return {
"image": TERMINAL_SANDBOX_IMAGE,
"mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
"shell": TERMINAL_SANDBOX_SHELL,
"network": TERMINAL_SANDBOX_NETWORK,
"cpus": TERMINAL_SANDBOX_CPUS,
"memory": TERMINAL_SANDBOX_MEMORY,
"binds": list(TERMINAL_SANDBOX_BINDS),
"bin": TERMINAL_SANDBOX_BIN,
"name_prefix": name_prefix,
"env": dict(TERMINAL_SANDBOX_ENV),
"require": TERMINAL_SANDBOX_REQUIRE,
}
class ToolboxContainer:
"""为 run_command/run_python 提供的专用容器."""
def __init__(
self,
project_path: str,
sandbox_mode: Optional[str] = None,
sandbox_options: Optional[Dict] = None,
idle_timeout: int = TOOLBOX_TERMINAL_IDLE_SECONDS,
):
self.project_path = Path(project_path).resolve()
self.sandbox_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
options = _build_sandbox_options()
if sandbox_options:
for key, value in sandbox_options.items():
if key == "binds" and isinstance(value, list):
options[key] = list(value)
elif key == "env" and isinstance(value, dict):
options[key] = dict(value)
else:
options[key] = value
self.sandbox_options = options
self.idle_timeout = max(0, int(idle_timeout)) if idle_timeout is not None else 0
self._terminal: Optional[PersistentTerminal] = None
self._lock = asyncio.Lock()
self._session_name = f"toolbox-{uuid.uuid4().hex[:10]}"
self._last_used = 0.0
async def _ensure_terminal(self) -> PersistentTerminal:
"""确保容器已启动。"""
async with self._lock:
if self._terminal and self._terminal.is_running:
return self._terminal
terminal = PersistentTerminal(
session_name=self._session_name,
working_dir=str(self.project_path),
broadcast_callback=None,
project_path=str(self.project_path),
sandbox_mode=self.sandbox_mode,
sandbox_options=self.sandbox_options,
)
if not terminal.start():
raise RuntimeError("工具容器启动失败,请检查 Docker 或本地 shell 环境。")
self._terminal = terminal
self._last_used = time.time()
print(f"[Toolbox] 工具容器已启动 (session={self._session_name}, mode={terminal.execution_mode})")
return terminal
def _wrap_command(self, command: str, work_path: Path, execution_mode: Optional[str]) -> str:
"""根据执行模式动态拼接 cd 指令。"""
if work_path == self.project_path:
return command
try:
relative = work_path.relative_to(self.project_path).as_posix()
except ValueError:
relative = ""
if not relative:
return command
if execution_mode == "docker":
mount_path = self.sandbox_options.get("mount_path", "/workspace").rstrip("/")
target = f"{mount_path}/{relative}"
else:
target = str(work_path)
return f"cd {shlex.quote(target)} && {command}"
async def run(self, command: str, work_path: Path, timeout: Optional[float] = None) -> Dict:
"""在容器/主机终端中执行命令。"""
terminal = await self._ensure_terminal()
shell_command = self._wrap_command(command, work_path, terminal.execution_mode)
result = await asyncio.to_thread(
terminal.send_command,
shell_command,
True,
timeout,
)
self._last_used = time.time()
self._cleanup_if_idle()
return result
def _cleanup_if_idle(self):
if self.idle_timeout <= 0 or not self._terminal:
return
if time.time() - self._last_used >= self.idle_timeout:
self._terminal.close()
print(f"[Toolbox] 工具容器空闲超时,已释放 (session={self._session_name})")
self._terminal = None
def shutdown(self):
"""立即关闭容器。"""
if self._terminal:
self._terminal.close()
print(f"[Toolbox] 工具容器已关闭 (session={self._session_name})")
self._terminal = None
@property
def execution_mode(self) -> Optional[str]:
if self._terminal:
return self._terminal.execution_mode
return None