agent-Specialization/utils/context_manager/runtime_mixin.py
JOJO 493ac160eb fix(windows): 平台适配与稳定性修复(进程终止/编码/原子写/事件竞态/子智能体)
- 进程终止:Windows 用 CTRL_BREAK + taskkill /F /T 替代不存在的 killpg/SIGKILL,
  超时/取消全路径有界;_is_pid_alive 改 ctypes(原 os.kill(pid,0) 在 Windows 会真杀进程);
  reader 任务 finally 收口,消除 Task was destroyed but it is pending / unclosed transport
- 子智能体调度链:_run_coro 超时 60s + 失败 cancel/close 防幽灵协程;create 改先调度后提交、
  失败回滚;state 归属守卫 + 新建 120s 宽限期防误标已终止;_ensure_event_loop 加锁;
  子智能体 todo 改 per-agent 隔离存储,不再串到主智能体前端
- 子智能体执行环境(execution_env_text.py):创建/切换/恢复三时点注入环境说明,
  Windows 下 sandbox=WSL2 bash、direct=cmd;inject_notification 纯通知不触发新一轮工作,
  tool 序列中延迟到安全点 flush;传统子智能体按既定语义不通知
- 编码:git 侧边栏 subprocess 显式 utf-8 + errors=replace(修中文 Windows GBK
  _readerthread 崩溃导致侧边栏空白);check_environment/install_package 用 self.python_cmd
- 安全/路径:permission.py Windows deny 列表生效 + 驱动器根拦截;路径比较统一 normcase;
  /tmp 白名单平台分流;正斜杠判断归一化;os-release 平台守卫;PowerShell -ExecutionPolicy Bypass
- 稳定性:任务事件轮询持锁快照(修 deque mutated during iteration);
  原子写新增 replace_with_retry(修 WinError 5/32 瞬时持锁);
  共享文件兜底链 symlink→os.link 硬链接→copy2
- 部署:新增 _bootstrap.bat / setup.bat / start.bat Windows 启动脚本
2026-07-31 11:33:57 +08:00

251 lines
9.1 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.

# utils/context_manager.py - 上下文管理器集成对话持久化和Token统计
import os
import json
import base64
import mimetypes
import io
import uuid
import platform
import shutil
import subprocess
from copy import deepcopy
from typing import Dict, List, Optional, Any
from pathlib import Path
from datetime import datetime
try:
from config import (
MAX_CONTEXT_SIZE,
DATA_DIR,
PROMPTS_DIR,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
PROJECT_MAX_STORAGE_MB,
TERMINAL_SANDBOX_MODE,
LINUX_SAFETY,
)
from config.model_profiles import (
get_model_prompt_replacements,
get_registered_model_keys,
model_supports_image,
model_supports_video,
)
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_CONTEXT_SIZE,
DATA_DIR,
PROMPTS_DIR,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
PROJECT_MAX_STORAGE_MB,
TERMINAL_SANDBOX_MODE,
LINUX_SAFETY,
)
from config.model_profiles import (
get_model_prompt_replacements,
get_registered_model_keys,
model_supports_image,
model_supports_video,
)
from utils.conversation_manager import ConversationManager
from utils.host_workspace_debug import write_host_workspace_debug
from utils.media_store import MediaStore
from utils.token_usage import normalize_usage_payload
AUTO_SHALLOW_PLACEHOLDER = "过早的工具结果已经被自动压缩"
AUTO_SHALLOW_TOOL_WHITELIST = {
"write_file",
"read_file",
"edit_file",
"terminal_input",
"terminal_snapshot",
"web_search",
"extract_webpage",
"run_command",
"view_image",
"view_video",
}
class RuntimeMixin:
"""ContextManager runtime mixin 能力 mixin。"""
def _run_command(self, cmd: List[str], *, timeout: float = 1.5, cwd: Optional[Path] = None) -> str:
"""运行命令并返回标准输出 / Run command and return stdout."""
try:
completed = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
cwd=str(cwd) if cwd else None,
)
except (OSError, subprocess.TimeoutExpired):
return ""
if completed.returncode != 0:
return ""
return (completed.stdout or "").strip()
def _read_first_line(self, path: Path) -> str:
"""读取文件首行并去除空白 / Read first line and strip."""
try:
with path.open("r", encoding="utf-8", errors="ignore") as fh:
return fh.readline().strip()
except OSError:
return ""
def _read_os_release_pretty(self) -> str:
"""读取 Linux 发行版信息 / Read Linux distro from os-release."""
if sys.platform != "linux":
return ""
path = Path("/etc/os-release")
if not path.exists():
return ""
try:
content = path.read_text(encoding="utf-8", errors="ignore")
except OSError:
return ""
for line in content.splitlines():
if line.startswith("PRETTY_NAME="):
value = line.split("=", 1)[1].strip().strip('"')
return value
return ""
def _get_os_description(self) -> str:
"""获取 OS 描述 / Get OS description."""
system = platform.system()
if system == "Darwin":
version = platform.mac_ver()[0] or platform.release()
return f"macOS {version}".strip()
if system == "Windows":
release, version, _csd, _ptype = platform.win32_ver()
if release and version and version not in release:
return f"Windows {release} ({version})".strip()
return f"Windows {release or version or platform.release()}".strip()
if system == "Linux":
pretty = self._read_os_release_pretty()
if pretty:
return f"Linux {pretty}".strip()
version = platform.release() or platform.version()
return f"Linux {version}".strip()
version = platform.release() or platform.version()
name = system or "Unknown"
return f"{name} {version}".strip()
def _parse_wmic_model(self, output: str) -> str:
"""解析 WMIC 输出 / Parse WMIC output."""
if not output:
return ""
lines = [line.strip() for line in output.splitlines() if line.strip()]
for line in lines:
if line.lower() == "model":
continue
return line
return ""
def _get_device_model(self) -> str:
"""获取设备型号 / Get device model."""
system = platform.system()
if system == "Darwin":
return self._run_command(["sysctl", "-n", "hw.model"])
if system == "Windows":
model = self._run_command(
["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "(Get-CimInstance -ClassName Win32_ComputerSystem).Model"]
)
if model:
return model
output = self._run_command(["wmic", "computersystem", "get", "model"])
return self._parse_wmic_model(output)
if system == "Linux":
product = self._read_first_line(Path("/sys/devices/virtual/dmi/id/product_name"))
vendor = self._read_first_line(Path("/sys/devices/virtual/dmi/id/sys_vendor"))
if vendor and product and vendor not in product:
return f"{vendor} {product}".strip()
return product or vendor
return ""
def _get_python_info(self) -> str:
"""获取 Python 版本与可用命令 / Get Python version and commands."""
version = platform.python_version()
commands: List[str] = []
if shutil.which("python"):
commands.append("python")
if shutil.which("python3"):
commands.append("python3")
if commands:
return f"{version} ({', '.join(commands)})"
return f"{version} (未在PATH)"
def _get_node_info(self) -> str:
"""获取 Node 版本信息 / Get Node version info."""
node_cmd = None
if shutil.which("node"):
node_cmd = "node"
elif shutil.which("nodejs"):
node_cmd = "nodejs"
if not node_cmd:
return "nodejs 未安装"
version = self._run_command([node_cmd, "-v"])
if version:
if node_cmd == "nodejs":
return f"{version} (nodejs)"
return version
return f"{node_cmd} 可用"
def _get_git_info(self) -> str:
"""获取 Git 分支与状态 / Get git branch and status."""
if not shutil.which("git"):
return "无git环境"
cwd = self.project_path if self.project_path.exists() else None
if not cwd:
return "未初始化"
inside = self._run_command(["git", "rev-parse", "--is-inside-work-tree"], cwd=cwd)
if inside.strip() != "true":
return "未初始化"
branch = self._run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd)
if not branch or branch == "HEAD":
sha = self._run_command(["git", "rev-parse", "--short", "HEAD"], cwd=cwd)
branch = f"detached@{sha}" if sha else "detached"
status = self._run_command(["git", "status", "--porcelain"], cwd=cwd)
dirty = bool(status.strip())
return f"{branch} ({'dirty' if dirty else 'clean'})"
def _get_host_runtime_cache(self) -> Dict[str, str]:
"""获取宿主机固定信息 / Get cached host info."""
if self._host_runtime_cache:
return self._host_runtime_cache
os_desc = self._get_os_description() or "unknown"
arch = platform.machine() or platform.processor() or "unknown"
model = self._get_device_model() or "unknown"
python_info = self._get_python_info() or "unknown"
node_info = self._get_node_info() or "unknown"
git_info = self._get_git_info() or "unknown"
self._host_runtime_cache = {
"os": os_desc,
"arch": arch,
"model": model,
"python": python_info,
"node": node_info,
"git": git_info,
}
return self._host_runtime_cache
def _build_host_runtime_environment(self) -> str:
"""构建宿主机运行环境提示 / Build host runtime environment text."""
base = self._get_host_runtime_cache()
lines = [
"宿主机模式",
f" OS: {base.get('os', 'unknown')} | Arch: {base.get('arch', 'unknown')} | Model: {base.get('model', 'unknown')}",
f" Python: {base.get('python', 'unknown')}",
f" Node: {base.get('node', 'unknown')}",
f" Git: {base.get('git', 'unknown')}",
]
return "\n".join(lines)