refactor(openclaw): remove REST facade (port 8004), unify on WebSocket

Remove the redundant OpenClaw REST service (port 8004) since frontend
already uses WebSocket via Gateway (port 8765) → OpenClaw (port 18789).

Deleted:
- backend/apps/openclaw_service.py
- backend/api/openclaw.py
- backend/tests/test_openclaw_service_app.py
- backend/tests/test_service_clients.py
- shared/client/openclaw_client.py

Updated:
- backend/apps/__init__.py — remove openclaw_app exports
- backend/api/__init__.py — remove openclaw_router
- shared/client/__init__.py — remove OpenClawServiceClient
- backend/services/gateway_openclaw_handlers.py — update docstring
- start.sh — remove port 8004 service startup

Architecture:
- Before: Frontend → HTTP :8004 → subprocess openclaw CLI
- After: Frontend → WS :8765 → Gateway → WS :18789 → OpenClaw

Constraint: Frontend already uses WebSocket exclusively
Confidence: high
Scope-risk: low (frontend unchanged)
This commit is contained in:
2026-04-02 11:04:06 +08:00
parent 45c3996434
commit ecc7623093
10 changed files with 4 additions and 1625 deletions

View File

@@ -3,7 +3,6 @@
from shared.client.control_client import ControlPlaneClient
from shared.client.news_client import NewsServiceClient
from shared.client.openclaw_client import OpenClawServiceClient
from shared.client.runtime_client import RuntimeServiceClient
from shared.client.trading_client import TradingServiceClient
@@ -12,5 +11,4 @@ __all__ = [
"RuntimeServiceClient",
"TradingServiceClient",
"NewsServiceClient",
"OpenClawServiceClient",
]

View File

@@ -1,415 +0,0 @@
# -*- 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()