"""代理调用策略机。""" from __future__ import annotations import logging from typing import Any import httpx from .config import get_control_settings from .crypto import unseal logger = logging.getLogger(__name__) def node_token_plain(node: dict[str, Any]) -> str | None: sealed = (node.get("token_sealed") or "").strip() if not sealed: return None secret = get_control_settings().control_auth_secret try: return unseal(sealed, secret) except Exception: logger.exception("unseal fleet token failed node=%s", node.get("id")) return None async def call_node( node: dict[str, Any], method: str, path: str, *, require_token: bool = True, json_body: dict | None = None, ) -> tuple[int, Any]: settings = get_control_settings() base = str(node["base_url"]).rstrip("/") headers: dict[str, str] = {} if require_token: tok = node_token_plain(node) if not tok: return 400, {"detail": "该机尚未生成/保存中控 Token"} headers["X-Fleet-Token"] = tok headers["Authorization"] = f"Fleet {tok}" url = f"{base}{path}" timeout = settings.control_http_timeout_sec try: async with httpx.AsyncClient(timeout=timeout, follow_redirects=False) as client: res = await client.request(method, url, headers=headers, json=json_body) # 少数反代会 301/302 补尾斜杠;手动跟一次并保留 Token 头 if res.status_code in (301, 302, 307, 308) and res.headers.get("location"): loc = res.headers["location"] if loc.startswith("/"): from urllib.parse import urljoin loc = urljoin(url, loc) res = await client.request(method, loc, headers=headers, json=json_body) try: data = res.json() except Exception: data = {"detail": res.text[:500]} return res.status_code, data except Exception as e: return 502, {"detail": f"连接失败: {e}"} async def probe_health(node: dict[str, Any]) -> dict[str, Any]: code, data = await call_node(node, "GET", "/health", require_token=False) if code == 200 and isinstance(data, dict): return {"online": True, "health": data, "error": None} # fallback fleet status if health blocked code2, data2 = await call_node(node, "GET", "/api/fleet/status", require_token=True) if code2 == 200 and isinstance(data2, dict): return {"online": True, "health": data2, "error": None} detail = "" if isinstance(data, dict): detail = str(data.get("detail") or "") return { "online": False, "health": None, "error": detail or f"HTTP {code}", }