Files
auto_control/mcp_server/platform_client.py
T

146 lines
5.8 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.
"""平台(auto_control)HTTP 客户端:登录会话 + CSRF + API 封装。
会话失效自动重登;CSRF token 每次登录后获取,POST 必带。
只读接口(GET)与写接口(POST)分离封装,写操作由 MCP 门控层决定是否调用。
"""
import httpx
import logging
_log = logging.getLogger("mcp.platform")
_CSRF_HEADER = "X-CSRF-Token"
class PlatformError(Exception):
def __init__(self, code, message):
super().__init__(message)
self.code = code
self.message = message
class PlatformClient:
def __init__(self, url, user, password, timeout=30.0):
self.url = url.rstrip("/")
self.user = user
self.password = password
self._client = httpx.Client(base_url=self.url, timeout=timeout,
follow_redirects=True)
self._csrf = ""
self._login()
# ---------- 会话 ----------
def _login(self):
"""登录平台,拿会话 cookie + CSRF token。"""
r = self._client.post("/login", data={
"username": self.user, "password": self.password})
if r.status_code != 302 and r.status_code != 200:
raise PlatformError("platform_unavailable",
f"平台登录失败 HTTP {r.status_code}: {r.text[:120]}")
csrf = self._client.get("/api/csrf")
if csrf.status_code == 200:
self._csrf = (csrf.json() or {}).get("token", "")
_log.info("平台登录成功: %s", self.user)
def _ensure_session(self):
"""会话过期(401/403/302 到登录页)时重登。"""
r = self._client.get("/api/status")
if r.status_code in (401, 403) or r.url.path.endswith("/login"):
_log.warning("会话失效,重新登录")
self._login()
return True
return False
# ---------- 基础请求 ----------
def _get(self, path, **params):
self._ensure_session()
return self._client.get(path, params=params)
def _post(self, path, json=None):
self._ensure_session()
headers = {_CSRF_HEADER: self._csrf} if self._csrf else {}
return self._client.post(path, json=json or {}, headers=headers)
# ---------- 平台 API 封装(M0) ----------
def list_devices(self):
"""设备列表与状态(GET /api/status)。"""
r = self._get("/api/status")
if r.status_code != 200:
raise PlatformError("platform_unavailable",
f"/api/status HTTP {r.status_code}")
j = r.json() or {}
return j.get("devices", [])
def screenshot(self, serial):
"""截图(GET /api/screen/thumb),返回 (jpeg_bytes, screen_state)。"""
r = self._get("/api/screen/thumb", serial=serial)
if r.status_code == 503:
raise PlatformError("device_offline", r.text[:120])
if r.status_code != 200:
raise PlatformError("platform_unavailable",
f"截图 HTTP {r.status_code}")
return r.content, r.headers.get("X-Screen-State", "unknown")
def screen_size(self, serial):
"""屏幕原生分辨率(GET /api/screen/size),返回 (w, h)。"""
r = self._get("/api/screen/size", serial=serial)
if r.status_code == 503:
raise PlatformError("device_offline", r.text[:120])
if r.status_code != 200:
raise PlatformError("platform_unavailable",
f"分辨率 HTTP {r.status_code}")
j = r.json() or {}
if not j.get("ok"):
raise PlatformError("device_offline", str(j.get("error", "取分辨率失败"))[:120])
return int(j["width"]), int(j["height"])
def tap(self, serial, x, y):
"""点击(POST /api/screen/tap)。"""
r = self._post("/api/screen/tap", json={"serial": serial,
"x": int(x), "y": int(y)})
return self._check_op(r, "tap")
def swipe(self, serial, x1, y1, x2, y2, duration=0.2):
"""滑动(POST /api/screen/swipe)。"""
r = self._post("/api/screen/swipe", json={
"serial": serial, "x1": int(x1), "y1": int(y1),
"x2": int(x2), "y2": int(y2),
"duration": float(duration)})
return self._check_op(r, "swipe")
def ui_elements(self, serial):
"""UI 元素树(GET /api/uiauto/elements,uiautodev 服务)。"""
r = self._get("/api/uiauto/elements", serial=serial)
if r.status_code == 503:
raise PlatformError("device_offline", r.text[:120])
if r.status_code != 200:
raise PlatformError("platform_unavailable",
f"元素树 HTTP {r.status_code}")
j = r.json() or {}
if not j.get("ok"):
raise PlatformError("device_offline", str(j.get("error", "取元素失败"))[:120])
return j.get("elements", [])
def wake(self, serial):
"""亮屏并解锁(POST /api/device/screen_all mode=on)。"""
r = self._post("/api/device/screen_all",
json={"mode": "on", "serials": [serial]})
return self._check_op(r, "wake")
def press_key(self, serial, key):
"""按键(POST /api/screen/key)。"""
r = self._post("/api/screen/key",
json={"serial": serial, "key": key})
return self._check_op(r, "key")
@staticmethod
def _check_op(r, name):
if r.status_code == 503:
raise PlatformError("device_offline", r.text[:120])
if r.status_code != 200:
raise PlatformError("platform_unavailable",
f"{name} HTTP {r.status_code}: {r.text[:120]}")
j = r.json() or {}
if not j.get("ok"):
raise PlatformError("device_offline", str(j.get("error", "操作失败"))[:120])
return j