- Migrate OpenClaw from HTTP (port 8004) to WebSocket (port 18789) - Add workspace file list and content preview handlers - Add OpenClawStatus component with agent/skills view - Add OpenClawView panel in trader interface - Add Zustand store for OpenClaw state management - Fix gateway logging noise (yfinance, websockets) - Fix RunWorkspaceManager.get_agent_asset_dir attribute error - Handle missing workspace files gracefully in preview Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
416 lines
15 KiB
Python
416 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""OpenClaw service client — typed async wrapper for openclaw-service REST API."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
|
|
from shared.models.openclaw import (
|
|
AgentsList,
|
|
ApprovalRequest,
|
|
ApprovalsList,
|
|
CronJob,
|
|
CronList,
|
|
DaemonStatus,
|
|
HookStatusReport,
|
|
ModelAliasesList,
|
|
ModelFallbacksList,
|
|
ModelsList,
|
|
OpenClawStatus,
|
|
PairingListResponse,
|
|
QrCodeResponse,
|
|
SecretsAuditReport,
|
|
SecurityAuditResponse,
|
|
SessionEntry,
|
|
SessionHistory,
|
|
SessionsList,
|
|
SkillStatusReport,
|
|
SkillUpdateResult,
|
|
UpdateStatusResponse,
|
|
normalize_agents,
|
|
normalize_approvals,
|
|
normalize_cron_jobs,
|
|
normalize_daemon_status,
|
|
normalize_hooks,
|
|
normalize_model_aliases,
|
|
normalize_model_fallbacks,
|
|
normalize_models,
|
|
normalize_pairing,
|
|
normalize_qr,
|
|
normalize_security_audit,
|
|
normalize_secrets_audit,
|
|
normalize_session_history,
|
|
normalize_sessions,
|
|
normalize_skill_update,
|
|
normalize_skills,
|
|
normalize_status,
|
|
normalize_update_status,
|
|
normalize_plugins,
|
|
PluginsList,
|
|
)
|
|
|
|
|
|
class OpenClawServiceClient:
|
|
"""Async client for the openclaw-service API surface.
|
|
|
|
All methods return typed Pydantic models. The raw JSON dict is
|
|
accessible via the `.model_dump()` method on each result.
|
|
|
|
Example::
|
|
|
|
async with OpenClawServiceClient() as client:
|
|
status = await client.fetch_status()
|
|
print(status.runtime_version) # typed
|
|
print(status.model_dump()["runtimeVersion"]) # raw dict
|
|
"""
|
|
|
|
def __init__(self, base_url: str = "http://localhost:8004/api/openclaw"):
|
|
self.base_url = base_url.rstrip("/")
|
|
self._client: httpx.AsyncClient | None = None
|
|
|
|
async def __aenter__(self) -> "OpenClawServiceClient":
|
|
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
if self._client:
|
|
await self._client.aclose()
|
|
|
|
async def fetch_status(self) -> OpenClawStatus:
|
|
"""GET /status — returns parsed OpenClawStatus model."""
|
|
response = await self._client.get("/status")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_status(raw)
|
|
|
|
async def list_sessions(self) -> SessionsList:
|
|
"""GET /sessions — returns parsed SessionsList model."""
|
|
response = await self._client.get("/sessions")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_sessions(raw)
|
|
|
|
async def get_session(self, session_key: str) -> SessionEntry:
|
|
"""GET /sessions/{session_key} — returns parsed SessionEntry model."""
|
|
response = await self._client.get(f"/sessions/{session_key}")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
session = raw.get("session") or {}
|
|
return SessionEntry.model_validate(session, strict=False)
|
|
|
|
async def get_session_history(self, session_key: str, *, limit: int = 20) -> SessionHistory:
|
|
"""GET /sessions/{session_key}/history — returns parsed SessionHistory model."""
|
|
response = await self._client.get(
|
|
f"/sessions/{session_key}/history",
|
|
params={"limit": limit},
|
|
)
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_session_history(raw, session_key=session_key)
|
|
|
|
async def list_cron_jobs(self) -> CronList:
|
|
"""GET /cron — returns parsed CronList model."""
|
|
response = await self._client.get("/cron")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_cron_jobs(raw)
|
|
|
|
async def list_approvals(self) -> ApprovalsList:
|
|
"""GET /approvals — returns parsed ApprovalsList model."""
|
|
response = await self._client.get("/approvals")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_approvals(raw)
|
|
|
|
async def list_agents(self) -> AgentsList:
|
|
"""GET /agents — returns parsed AgentsList model."""
|
|
response = await self._client.get("/agents")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_agents(raw)
|
|
|
|
async def list_skills(self) -> SkillStatusReport:
|
|
"""GET /skills — returns parsed SkillStatusReport model."""
|
|
response = await self._client.get("/skills")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_skills(raw)
|
|
|
|
async def list_models(self) -> ModelsList:
|
|
"""GET /models — returns parsed ModelsList model."""
|
|
response = await self._client.get("/models")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_models(raw)
|
|
|
|
async def list_hooks(self) -> HookStatusReport:
|
|
response = await self._client.get("/hooks")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_hooks(raw)
|
|
|
|
async def list_plugins(self) -> PluginsList:
|
|
response = await self._client.get("/plugins")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_plugins(raw)
|
|
|
|
async def secrets_audit(self) -> SecretsAuditReport:
|
|
response = await self._client.get("/secrets-audit")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_secrets_audit(raw)
|
|
|
|
async def security_audit(self) -> SecurityAuditResponse:
|
|
response = await self._client.get("/security-audit")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_security_audit(raw)
|
|
|
|
async def daemon_status(self) -> DaemonStatus:
|
|
response = await self._client.get("/daemon-status")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_daemon_status(raw)
|
|
|
|
async def pairing_list(self) -> PairingListResponse:
|
|
response = await self._client.get("/pairing")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_pairing(raw)
|
|
|
|
async def qr_code(self) -> QrCodeResponse:
|
|
response = await self._client.get("/qr")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_qr(raw)
|
|
|
|
async def update_status(self) -> UpdateStatusResponse:
|
|
response = await self._client.get("/update-status")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_update_status(raw)
|
|
|
|
async def list_model_aliases(self) -> ModelAliasesList:
|
|
response = await self._client.get("/models-aliases")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_model_aliases(raw)
|
|
|
|
async def list_model_fallbacks(self) -> ModelFallbacksList:
|
|
response = await self._client.get("/models-fallbacks")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_model_fallbacks(raw)
|
|
|
|
async def list_model_image_fallbacks(self) -> ModelFallbacksList:
|
|
response = await self._client.get("/models-image-fallbacks")
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_model_fallbacks(raw)
|
|
|
|
async def skill_update(self, *, slug: str | None = None, all: bool = False) -> SkillUpdateResult:
|
|
params = {}
|
|
if slug is not None:
|
|
params["slug"] = slug
|
|
if all:
|
|
params["all"] = "true"
|
|
response = await self._client.get("/skill-update", params=params)
|
|
response.raise_for_status()
|
|
raw = response.json()
|
|
return normalize_skill_update(raw)
|
|
|
|
async def models_status(self, *, probe: bool = False) -> dict[str, Any]:
|
|
"""GET /models-status — returns parsed models status dict."""
|
|
params = {"probe": "true"} if probe else {}
|
|
response = await self._client.get("/models-status", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def channels_status(self, *, probe: bool = False) -> dict[str, Any]:
|
|
"""GET /channels-status — returns parsed channels status dict."""
|
|
params = {"probe": "true"} if probe else {}
|
|
response = await self._client.get("/channels-status", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def channels_list(self) -> dict[str, Any]:
|
|
"""GET /channels-list — returns parsed channels list dict."""
|
|
response = await self._client.get("/channels-list")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def hook_info(self, name: str) -> dict[str, Any]:
|
|
"""GET /hooks/info/{name} — returns parsed hook info dict."""
|
|
response = await self._client.get(f"/hooks/info/{name}")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def hooks_check(self) -> dict[str, Any]:
|
|
"""GET /hooks/check — returns parsed hooks check dict."""
|
|
response = await self._client.get("/hooks/check")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def plugins_inspect(self, *, plugin_id: str | None = None, all: bool = False) -> dict[str, Any]:
|
|
"""GET /plugins-inspect — returns parsed plugins inspect dict."""
|
|
params: dict[str, Any] = {}
|
|
if all:
|
|
params["all"] = "true"
|
|
elif plugin_id:
|
|
params["plugin_id"] = plugin_id
|
|
response = await self._client.get("/plugins-inspect", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def agents_bindings(self, *, agent: str | None = None) -> dict[str, Any]:
|
|
"""GET /agents-bindings — returns parsed agents bindings list dict."""
|
|
params: dict[str, Any] = {}
|
|
if agent:
|
|
params["agent"] = agent
|
|
response = await self._client.get("/agents-bindings", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def agents_presence(self) -> dict[str, Any]:
|
|
"""GET /agents/presence — returns runtime session presence for all agents."""
|
|
response = await self._client.get("/agents/presence")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def workspace_files(self, workspace_path: str) -> dict[str, Any]:
|
|
"""GET /workspace-files?workspace=<path> — list .md files in a workspace."""
|
|
response = await self._client.get("/workspace-files", params={"workspace": workspace_path})
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Write agents operations
|
|
# -------------------------------------------------------------------------
|
|
|
|
async def agents_add(
|
|
self,
|
|
name: str,
|
|
*,
|
|
workspace: str | None = None,
|
|
model: str | None = None,
|
|
agent_dir: str | None = None,
|
|
bind: list[str] | None = None,
|
|
non_interactive: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""POST /agents/add — create a new agent."""
|
|
params: dict[str, Any] = {"name": name}
|
|
if workspace:
|
|
params["workspace"] = workspace
|
|
if model:
|
|
params["model"] = model
|
|
if agent_dir:
|
|
params["agent_dir"] = agent_dir
|
|
if bind:
|
|
params["bind"] = bind
|
|
if non_interactive:
|
|
params["non_interactive"] = "true"
|
|
response = await self._client.post("/agents/add", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def agents_delete(self, id: str, *, force: bool = False) -> dict[str, Any]:
|
|
"""POST /agents/delete/{id} — delete an agent."""
|
|
params: dict[str, Any] = {}
|
|
if force:
|
|
params["force"] = "true"
|
|
response = await self._client.post(f"/agents/delete/{id}", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def agents_bind(
|
|
self,
|
|
*,
|
|
agent: str | None = None,
|
|
bind: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""POST /agents/bind — add routing bindings to an agent."""
|
|
params: dict[str, Any] = {}
|
|
if agent:
|
|
params["agent"] = agent
|
|
if bind:
|
|
params["bind"] = bind
|
|
response = await self._client.post("/agents/bind", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def agents_unbind(
|
|
self,
|
|
*,
|
|
agent: str | None = None,
|
|
bind: list[str] | None = None,
|
|
all: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""POST /agents/unbind — remove routing bindings from an agent."""
|
|
params: dict[str, Any] = {}
|
|
if agent:
|
|
params["agent"] = agent
|
|
if bind:
|
|
params["bind"] = bind
|
|
if all:
|
|
params["all"] = "true"
|
|
response = await self._client.post("/agents/unbind", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def agents_set_identity(
|
|
self,
|
|
*,
|
|
agent: str | None = None,
|
|
workspace: str | None = None,
|
|
identity_file: str | None = None,
|
|
name: str | None = None,
|
|
emoji: str | None = None,
|
|
theme: str | None = None,
|
|
avatar: str | None = None,
|
|
from_identity: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""POST /agents/set-identity — update agent identity."""
|
|
params: dict[str, Any] = {}
|
|
if agent:
|
|
params["agent"] = agent
|
|
if workspace:
|
|
params["workspace"] = workspace
|
|
if identity_file:
|
|
params["identity_file"] = identity_file
|
|
if name:
|
|
params["name"] = name
|
|
if emoji:
|
|
params["emoji"] = emoji
|
|
if theme:
|
|
params["theme"] = theme
|
|
if avatar:
|
|
params["avatar"] = avatar
|
|
if from_identity:
|
|
params["from_identity"] = "true"
|
|
response = await self._client.post("/agents/set-identity", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def gateway_status(self, *, url: str | None = None, token: str | None = None) -> dict[str, Any]:
|
|
"""GET /gateway-status — returns parsed gateway status dict."""
|
|
params: dict[str, Any] = {}
|
|
if url:
|
|
params["url"] = url
|
|
if token:
|
|
params["token"] = token
|
|
response = await self._client.get("/gateway-status", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def memory_status(self, *, agent: str | None = None, deep: bool = False) -> list[dict[str, Any]]:
|
|
"""GET /memory-status — returns list of per-agent memory status dicts."""
|
|
params: dict[str, Any] = {}
|
|
if agent:
|
|
params["agent"] = agent
|
|
if deep:
|
|
params["deep"] = "true"
|
|
response = await self._client.get("/memory-status", params=params)
|
|
response.raise_for_status()
|
|
return response.json()
|