TraceStudio/server/app/services/agent_client.py
2026-01-13 00:29:18 +08:00

128 lines
4.9 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.

"""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"]