08fe06d074
Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
"""代理调用策略机。"""
|
|
|
|
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}",
|
|
}
|