front/xhs-agent-desktop/app/oms_client.py
2026-07-02 15:52:09 +08:00

154 lines
4.8 KiB
Python

"""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]}")
text = (resp.text or "").strip()
if not text:
return {}
try:
payload = resp.json()
except ValueError as exc:
raise OmsClientError(f"非 JSON 响应 {path}: {text[:200]}") 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 {}