"""Agent 客户端:通过 HTTP 调用宿主上的 Agent 服务,并提供路径映射工具。""" from __future__ import annotations import json import os import urllib.request import urllib.error from pathlib import Path, PurePosixPath, PureWindowsPath from typing import Any, Dict, List, Optional, Union, Union class AgentClient: def __init__( self, base_url: str = "http://localhost:8100", *, host_cloud_root: Optional[Path | str] = None, container_cloud_root: Optional[Path | str] = None, ): self.base_url = base_url.rstrip("/") self.host_cloud_root = Path(host_cloud_root) if host_cloud_root else None self.container_cloud_root = Path(container_cloud_root) if container_cloud_root else None @classmethod def from_env(cls, *, container_cloud_root: Optional[Path | str] = None) -> "AgentClient": base_url = os.environ.get("AGENT_BASE_URL", "http://host.docker.internal:8100") host_root = os.environ.get("AGENT_HOST_CLOUD_ROOT") return cls(base_url=base_url, host_cloud_root=host_root, container_cloud_root=container_cloud_root) # ---------------------- 路径映射 (核心修正) ---------------------- def map_cloud_to_host(self, path: Union[str, Path]) -> str: """ 【Docker -> Host】 将容器内的路径(/opt/...)转换为宿主机的路径(D:\...)。 返回 str 以便直接传给 Agent API。 """ # 输入可能是 pathlib.Path (Posix) 或 字符串 p_obj = PurePosixPath(path) # 如果没有配置映射,直接返回字符串 if not self.container_cloud_root or not self.host_cloud_root: return str(p_obj) try: # 1. 计算相对路径 (Linux 逻辑) # 例如: /opt/tracestudio/cloud/traces/test.utrace -> traces/test.utrace rel = p_obj.relative_to(self.container_cloud_root) # 2. 拼接到 Windows 根路径 (Windows 逻辑) # 例如: D:\XGame\...\cloud + traces/test.utrace -> D:\XGame\...\cloud\traces\test.utrace return str(self.host_cloud_root / rel) except ValueError: # 如果路径不在 container_cloud_root 下,原样返回(可能是绝对路径或其他挂载) return str(p_obj) def map_host_to_container(self, path: Union[str, Path]) -> Path: """ 【Host -> Docker】 将 Agent 返回的 Windows 路径(D:\...)转换为容器内可读的路径(/opt/...)。 返回 Path 对象以便 Python 打开文件。 """ # 输入是 Windows 格式字符串 p_obj = PureWindowsPath(path) if not self.host_cloud_root or not self.container_cloud_root: # 无法映射,尝试直接转换(大概率读不到,但保持类型一致) return Path(str(p_obj)) try: # 1. 计算相对路径 (Windows 逻辑) # Windows 不区分大小写,但 relative_to 默认区分。 # 如果路径来源可靠,通常没问题。 rel = p_obj.relative_to(self.host_cloud_root) # 2. 拼接到 Linux 容器路径 # 这里的 Path 是具体平台的 Path (在 Docker 里就是 PosixPath) return Path(self.container_cloud_root / rel) except ValueError: return Path(str(p_obj)) # ---------------------- 调用 ---------------------- def run( self, tool: str, args: Optional[List[str]] = None, *, workdir: Optional[str] = None, timeout: Optional[int] = None, env: Optional[Dict[str, str]] = None, capture_output: Optional[bool] = None, strip_output: Optional[bool] = None, ) -> Dict[str, Any]: payload = { "tool": tool, "args": args or [], } if workdir: payload["workdir"] = workdir if timeout is not None: payload["timeout"] = timeout if env: payload["env"] = env if capture_output is not None: payload["capture_output"] = capture_output if strip_output is not None: payload["strip_output"] = strip_output url = f"{self.base_url}/run" data = json.dumps(payload).encode("utf-8") req = urllib.request.Request( url, data=data, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=timeout or 30) as resp: body = resp.read().decode("utf-8") return json.loads(body) except urllib.error.HTTPError as e: detail = e.read().decode("utf-8") if e.fp else str(e) raise RuntimeError(f"Agent HTTP {e.code}: {detail}") from e except Exception as e: raise RuntimeError(f"Agent 调用失败: {e}") from e __all__ = ["AgentClient"]