"""OMS 小红书 Agent HTTP 客户端。""" from __future__ import annotations from typing import Any import requests AGENT_VERSION = "1.0.0" class OmsClientError(RuntimeError): """OMS API 调用失败。""" class OmsAgentClient: """对接 OMS /xhs/* Agent 接口。""" def __init__( self, base_url: str, token: str, device_id: str, *, timeout_sec: int = 60, ) -> None: self.base_url = base_url.rstrip("/") self.device_id = device_id.strip() self.timeout_sec = timeout_sec self._session = requests.Session() self._session.headers.update( { "Authorization": f"Bearer {token.strip()}", "Content-Type": "application/json", } ) self._human_config_version = 0 self._agent_id: int | None = None @property def human_config_version(self) -> int: return self._human_config_version @property def agent_id(self) -> int | None: return self._agent_id def set_human_config_version(self, version: int) -> None: self._human_config_version = version def register(self, *, device_name: str | None = None, bridge_port: int = 9333) -> dict[str, Any]: data = self._post( "/xhs/agent/register", { "deviceId": self.device_id, "deviceName": device_name, "agentVersion": AGENT_VERSION, "bridgePort": bridge_port, }, ) self._human_config_version = int(data.get("humanConfigVersion") or 0) self._agent_id = data.get("agentId") return data def heartbeat( self, *, engine_running: bool, extension_connected: bool, xhs_logged_in: bool, last_error: str | None = None, ) -> dict[str, Any]: data = self._post( "/xhs/agent/heartbeat", { "deviceId": self.device_id, "engineRunning": engine_running, "extensionConnected": extension_connected, "xhsLoggedIn": xhs_logged_in, "lastError": last_error, }, ) self._human_config_version = int(data.get("humanConfigVersion") or self._human_config_version) return data def claim_tasks(self, *, limit: int = 1) -> list[dict[str, Any]]: data = self._post( "/xhs/task/claim", {"deviceId": self.device_id, "limit": limit}, ) items = data.get("items") or [] if not isinstance(items, list): return [] for item in items: version = item.get("humanConfigVersion") if version is not None: self._human_config_version = int(version) return items def report_task( self, task_id: int, *, status: str, progress_text: str | None = None, result: Any = None, error_message: str | None = None, ) -> None: payload: dict[str, Any] = {"taskId": task_id, "status": status} if progress_text: payload["progressText"] = progress_text if result is not None: payload["result"] = result if error_message: payload["errorMessage"] = error_message self._post("/xhs/task/report", payload) def pull_human_config(self, known_version: int) -> dict[str, Any]: return self._post( "/xhs/human-config/pull", {"deviceId": self.device_id, "knownVersion": known_version}, ) def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]: url = f"{self.base_url}{path}" try: resp = self._session.post(url, json=body, timeout=self.timeout_sec) except requests.RequestException as exc: raise OmsClientError(f"请求失败 {path}: {exc}") from exc if resp.status_code >= 400: raise OmsClientError(f"HTTP {resp.status_code} {path}: {resp.text[:500]}") try: payload = resp.json() except ValueError as exc: raise OmsClientError(f"非 JSON 响应 {path}") from exc code = payload.get("code", payload.get("Code")) if code not in (None, 0, "0", 200, "200"): message = payload.get("message") or payload.get("Message") or "业务错误" raise OmsClientError(f"{path}: {message}") data = payload.get("data", payload.get("Data")) if isinstance(data, dict): return data if data is None and isinstance(payload, dict) and "code" not in payload and "Code" not in payload: return payload return data if isinstance(data, dict) else {}