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

@@ -11,7 +11,6 @@ Provides REST API endpoints for:
from .agents import router as agents_router from .agents import router as agents_router
from .workspaces import router as workspaces_router from .workspaces import router as workspaces_router
from .guard import router as guard_router from .guard import router as guard_router
from .openclaw import router as openclaw_router
from .runtime import router as runtime_router from .runtime import router as runtime_router
from .runs import router as runs_router from .runs import router as runs_router
@@ -19,7 +18,6 @@ __all__ = [
"agents_router", "agents_router",
"workspaces_router", "workspaces_router",
"guard_router", "guard_router",
"openclaw_router",
"runtime_router", "runtime_router",
"runs_router", "runs_router",
] ]

View File

@@ -1,839 +0,0 @@
# -*- coding: utf-8 -*-
"""Read-only OpenClaw CLI API routes — typed with Pydantic models."""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from backend.services.openclaw_cli import OpenClawCliError, OpenClawCliService
from shared.models.openclaw import OpenClawStatus
router = APIRouter(prefix="/api/openclaw", tags=["openclaw"])
def get_openclaw_cli_service() -> OpenClawCliService:
"""Build the OpenClaw CLI service dependency."""
return OpenClawCliService()
def _raise_cli_http_error(exc: OpenClawCliError) -> None:
detail = {
"message": str(exc),
"command": exc.command,
"exit_code": exc.exit_code,
"stdout": exc.stdout,
"stderr": exc.stderr,
}
status_code = 503 if exc.exit_code is None else 502
raise HTTPException(status_code=status_code, detail=detail) from exc
# ---------------------------------------------------------------------------
# Response wrappers
# ---------------------------------------------------------------------------
class StatusResponse(BaseModel):
status: object
class SessionsResponse(BaseModel):
sessions: list[object]
class SessionDetailResponse(BaseModel):
session: object | None
class SessionHistoryResponse(BaseModel):
session_key: str
session_id: str | None
events: list[object]
history: list[object]
raw_text: str | None
class CronResponse(BaseModel):
cron: list[object]
jobs: list[object]
class ApprovalsResponse(BaseModel):
approvals: list[object]
pending: list[object]
class AgentsResponse(BaseModel):
agents: list[object]
class SkillsResponse(BaseModel):
workspace_dir: str
managed_skills_dir: str
skills: list[object]
class ModelsResponse(BaseModel):
models: list[object]
class HooksResponse(BaseModel):
workspace_dir: str
managed_hooks_dir: str
hooks: list[object]
class PluginsResponse(BaseModel):
workspace_dir: str
plugins: list[object]
diagnostics: list[object]
class SecretsAuditResponse(BaseModel):
version: int
status: str
findings: list[object]
class SecurityAuditResponse2(BaseModel):
report: object | None
secret_diagnostics: list[str]
class DaemonStatusResponse(BaseModel):
service: object | None
port: object | None
rpc: object | None
health: object | None
class PairingListResponse2(BaseModel):
channel: str
requests: list[object]
class QrCodeResponse2(BaseModel):
setup_code: str
gateway_url: str
auth: str
url_source: str
class UpdateStatusResponse2(BaseModel):
update: object | None
channel: object | None
class ModelAliasesResponse(BaseModel):
aliases: dict[str, str]
class ModelFallbacksResponse(BaseModel):
key: str
label: str
items: list[object]
class SkillUpdateResponse(BaseModel):
ok: bool
slug: str
version: str
error: str | None
class ModelsStatusResponse(BaseModel):
configPath: str | None = None
agentId: str | None = None
agentDir: str | None = None
defaultModel: str | None = None
resolvedDefault: str | None = None
fallbacks: list[str] = Field(default_factory=list)
imageModel: str | None = None
imageFallbacks: list[str] = Field(default_factory=list)
aliases: dict[str, str] = Field(default_factory=dict)
allowed: list[str] = Field(default_factory=list)
auth: dict[str, Any] = Field(default_factory=dict)
class ChannelsStatusResponse(BaseModel):
reachable: bool | None = None
channelAccounts: dict[str, Any] = Field(default_factory=dict)
channels: list[str] = Field(default_factory=list)
issues: list[dict[str, Any]] = Field(default_factory=list)
class ChannelsListResponse(BaseModel):
chat: dict[str, list[str]] = Field(default_factory=dict)
auth: list[dict[str, Any]] = Field(default_factory=list)
usage: dict[str, Any] | None = None
class HookInfoResponse(BaseModel):
name: str | None = None
description: str | None = None
source: str | None = None
pluginId: str | None = None
filePath: str | None = None
handlerPath: str | None = None
hookKey: str | None = None
emoji: str | None = None
homepage: str | None = None
events: list[str] = Field(default_factory=list)
enabledByConfig: bool | None = None
loadable: bool | None = None
requirementsSatisfied: bool | None = None
requirements: dict[str, Any] = Field(default_factory=dict)
error: str | None = None
raw: str | None = None
class HooksCheckResponse(BaseModel):
workspace_dir: str = ""
managed_hooks_dir: str = ""
hooks: list[dict[str, Any]] = Field(default_factory=list)
eligible: bool | None = None
verbose: bool | None = None
class PluginInspectEntry(BaseModel):
plugin: dict[str, Any] = Field(default_factory=dict)
shape: str | None = None
capabilityMode: str | None = None
capabilityCount: int = 0
capabilities: list[dict[str, Any]] = Field(default_factory=list)
typedHooks: list[dict[str, Any]] = Field(default_factory=list)
customHooks: list[dict[str, Any]] = Field(default_factory=list)
tools: list[dict[str, Any]] = Field(default_factory=list)
commands: list[str] = Field(default_factory=list)
cliCommands: list[str] = Field(default_factory=list)
services: list[str] = Field(default_factory=list)
gatewayMethods: list[str] = Field(default_factory=list)
mcpServers: list[dict[str, Any]] = Field(default_factory=list)
lspServers: list[dict[str, Any]] = Field(default_factory=list)
httpRouteCount: int = 0
bundleCapabilities: list[str] = Field(default_factory=list)
class PluginsInspectResponse(BaseModel):
inspect: list[dict[str, Any]] = Field(default_factory=list)
class AgentBindingItem(BaseModel):
agentId: str
match: dict[str, Any]
description: str
class AgentsBindingsResponse(BaseModel):
bindings: list[AgentBindingItem]
# ---------------------------------------------------------------------------
# Routes — use typed model methods and return Pydantic models directly
# ---------------------------------------------------------------------------
@router.get("/status")
async def api_openclaw_status(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> OpenClawStatus:
"""Read `openclaw status --json` and return a typed model."""
try:
return service.status_model()
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/sessions")
async def api_openclaw_sessions(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> SessionsResponse:
"""Read `openclaw sessions --json` and return a typed SessionsList."""
try:
result = service.list_sessions_model()
return SessionsResponse(sessions=result.sessions)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/sessions/{session_key:path}/history")
async def api_openclaw_session_history(
session_key: str,
limit: int = Query(20, ge=1, le=200),
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> SessionHistoryResponse:
"""Read session history and return a typed SessionHistory."""
try:
result = service.get_session_history_model(session_key, limit=limit)
return SessionHistoryResponse(
session_key=result.session_key,
session_id=result.session_id,
events=result.events,
history=result.events, # alias for compat
raw_text=result.raw_text,
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/sessions/{session_key:path}")
async def api_openclaw_session_detail(
session_key: str,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> SessionDetailResponse:
"""Resolve a single session and return it as a typed model."""
try:
session = service.get_session_model(session_key)
return SessionDetailResponse(session=session)
except KeyError as exc:
raise HTTPException(status_code=404, detail=f"session '{session_key}' not found") from exc
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/cron")
async def api_openclaw_cron(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> CronResponse:
"""Read `openclaw cron list --json` and return a typed CronList."""
try:
result = service.list_cron_jobs_model()
return CronResponse(cron=list(result.cron), jobs=list(result.jobs))
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/approvals")
async def api_openclaw_approvals(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> ApprovalsResponse:
"""Read `openclaw approvals get --json` and return a typed ApprovalsList."""
try:
result = service.list_approvals_model()
return ApprovalsResponse(
approvals=list(result.approvals),
pending=list(result.pending),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/agents")
async def api_openclaw_agents(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> AgentsResponse:
"""Read `openclaw agents list --json` and return a typed AgentsList."""
try:
result = service.list_agents_model()
return AgentsResponse(agents=list(result.agents))
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/agents/presence")
async def api_openclaw_agents_presence(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> dict[str, Any]:
"""Read runtime session presence for all agents from session files."""
result = service.agents_presence()
return result
# ---------------------------------------------------------------------------
# Write agents routes
# ---------------------------------------------------------------------------
class AgentAddResponse(BaseModel):
agentId: str
name: str
workspace: str
agentDir: str
model: str | None = None
bindings: dict[str, Any] = Field(default_factory=dict)
class AgentDeleteResponse(BaseModel):
agentId: str
workspace: str
agentDir: str
sessionsDir: str
removedBindings: list[str] = Field(default_factory=list)
removedAllow: list[str] = Field(default_factory=list)
class AgentBindResponse(BaseModel):
agentId: str
added: list[str] = Field(default_factory=list)
updated: list[str] = Field(default_factory=list)
skipped: list[str] = Field(default_factory=list)
conflicts: list[str] = Field(default_factory=list)
class AgentUnbindResponse(BaseModel):
agentId: str
removed: list[str] = Field(default_factory=list)
missing: list[str] = Field(default_factory=list)
conflicts: list[str] = Field(default_factory=list)
class AgentIdentityResponse(BaseModel):
agentId: str
identity: dict[str, Any] = Field(default_factory=dict)
workspace: str | None = None
identityFile: str | None = None
@router.post("/agents/add")
async def api_openclaw_agents_add(
name: str,
*,
workspace: str | None = None,
model: str | None = None,
agent_dir: str | None = None,
bind: list[str] | None = None,
non_interactive: bool = False,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> AgentAddResponse:
"""Run `openclaw agents add <name>` and return JSON result."""
try:
result = service.agents_add(
name,
workspace=workspace,
model=model,
agent_dir=agent_dir,
bind=bind,
non_interactive=non_interactive,
)
return AgentAddResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.post("/agents/delete/{id}")
async def api_openclaw_agents_delete(
id: str,
force: bool = False,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> AgentDeleteResponse:
"""Run `openclaw agents delete <id> [--force]` and return JSON result."""
try:
result = service.agents_delete(id, force=force)
return AgentDeleteResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.post("/agents/bind")
async def api_openclaw_agents_bind(
*,
agent: str | None = None,
bind: list[str] | None = None,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> AgentBindResponse:
"""Run `openclaw agents bind [--agent <id>] [--bind <spec>]` and return JSON result."""
try:
result = service.agents_bind(agent=agent, bind=bind)
return AgentBindResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.post("/agents/unbind")
async def api_openclaw_agents_unbind(
*,
agent: str | None = None,
bind: list[str] | None = None,
all: bool = False,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> AgentUnbindResponse:
"""Run `openclaw agents unbind [--agent <id>] [--bind <spec>] [--all]` and return JSON result."""
try:
result = service.agents_unbind(agent=agent, bind=bind, all=all)
return AgentUnbindResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.post("/agents/set-identity")
async def api_openclaw_agents_set_identity(
*,
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,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> AgentIdentityResponse:
"""Run `openclaw agents set-identity` and return JSON result."""
try:
result = service.agents_set_identity(
agent=agent,
workspace=workspace,
identity_file=identity_file,
name=name,
emoji=emoji,
theme=theme,
avatar=avatar,
from_identity=from_identity,
)
return AgentIdentityResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/skills")
async def api_openclaw_skills(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> SkillsResponse:
"""Read `openclaw skills list --json` and return a typed SkillStatusReport."""
try:
result = service.list_skills_model()
return SkillsResponse(
workspace_dir=result.workspace_dir,
managed_skills_dir=result.managed_skills_dir,
skills=list(result.skills),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/models")
async def api_openclaw_models(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> ModelsResponse:
"""Read `openclaw models list --json` and return a typed ModelsList."""
try:
result = service.list_models_model()
return ModelsResponse(models=list(result.models))
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/hooks")
async def api_openclaw_hooks(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> HooksResponse:
try:
result = service.list_hooks_model()
return HooksResponse(
workspace_dir=result.workspace_dir,
managed_hooks_dir=result.managed_hooks_dir,
hooks=list(result.hooks),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/plugins")
async def api_openclaw_plugins(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> PluginsResponse:
try:
result = service.list_plugins_model()
return PluginsResponse(
workspace_dir=result.workspace_dir,
plugins=list(result.plugins),
diagnostics=list(result.diagnostics),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/secrets-audit")
async def api_openclaw_secrets_audit(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> SecretsAuditResponse:
try:
result = service.secrets_audit_model()
return SecretsAuditResponse(
version=result.version,
status=result.status,
findings=list(result.findings),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/security-audit")
async def api_openclaw_security_audit(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> SecurityAuditResponse2:
try:
result = service.security_audit_model()
return SecurityAuditResponse2(
report=result.report.model_dump() if result.report else None,
secret_diagnostics=list(result.secret_diagnostics),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/daemon-status")
async def api_openclaw_daemon_status(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> DaemonStatusResponse:
try:
result = service.daemon_status_model()
return DaemonStatusResponse(
service=result.service.model_dump() if result.service else None,
port=result.port.model_dump() if result.port else None,
rpc=result.rpc.model_dump() if result.rpc else None,
health=result.health.model_dump() if result.health else None,
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/pairing")
async def api_openclaw_pairing(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> PairingListResponse2:
try:
result = service.pairing_list_model()
return PairingListResponse2(
channel=result.channel,
requests=list(result.requests),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/qr")
async def api_openclaw_qr(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> QrCodeResponse2:
try:
result = service.qr_code_model()
return QrCodeResponse2(
setup_code=result.setup_code,
gateway_url=result.gateway_url,
auth=result.auth,
url_source=result.url_source,
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/update-status")
async def api_openclaw_update_status(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> UpdateStatusResponse2:
try:
result = service.update_status_model()
return UpdateStatusResponse2(
update=result.update.model_dump() if result.update else None,
channel=result.channel.model_dump() if result.channel else None,
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/models-aliases")
async def api_openclaw_models_aliases(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> ModelAliasesResponse:
try:
result = service.list_model_aliases_model()
return ModelAliasesResponse(aliases=result.aliases)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/models-fallbacks")
async def api_openclaw_models_fallbacks(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> ModelFallbacksResponse:
try:
result = service.list_model_fallbacks_model()
return ModelFallbacksResponse(
key=result.key,
label=result.label,
items=list(result.items),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/models-image-fallbacks")
async def api_openclaw_models_image_fallbacks(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> ModelFallbacksResponse:
try:
result = service.list_model_image_fallbacks_model()
return ModelFallbacksResponse(
key=result.key,
label=result.label,
items=list(result.items),
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/skill-update")
async def api_openclaw_skill_update(
slug: str | None = None,
all: bool = False,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> SkillUpdateResponse:
try:
result = service.skill_update_model(slug=slug, all=all)
return SkillUpdateResponse(
ok=result.ok,
slug=result.slug,
version=result.version,
error=result.error,
)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/models-status")
async def api_openclaw_models_status(
probe: bool = False,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> ModelsStatusResponse:
"""Read `openclaw models status --json [--probe]` and return a typed dict."""
try:
result = service.models_status_model(probe=probe)
return ModelsStatusResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/channels-status")
async def api_openclaw_channels_status(
probe: bool = False,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> ChannelsStatusResponse:
"""Read `openclaw channels status --json [--probe]` and return a typed dict."""
try:
result = service.channels_status_model(probe=probe)
return ChannelsStatusResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/channels-list")
async def api_openclaw_channels_list(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> ChannelsListResponse:
"""Read `openclaw channels list --json` and return a typed dict."""
try:
result = service.channels_list_model()
return ChannelsListResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/hooks/info/{name}")
async def api_openclaw_hook_info(
name: str,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> HookInfoResponse:
"""Read `openclaw hooks info <name> --json` and return a typed dict."""
try:
result = service.hook_info_model(name)
return HookInfoResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/hooks/check")
async def api_openclaw_hooks_check(
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> HooksCheckResponse:
"""Read `openclaw hooks check --json` and return a typed dict."""
try:
result = service.hooks_check_model()
return HooksCheckResponse.model_validate(result, strict=False)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/plugins-inspect")
async def api_openclaw_plugins_inspect(
plugin_id: str | None = None,
all: bool = False,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> PluginsInspectResponse:
"""Read `openclaw plugins inspect --json [--all]` and return a typed dict."""
try:
result = service.plugins_inspect_model(plugin_id=plugin_id, all=all)
inspect = result if isinstance(result, list) else result.get("inspect", [])
return PluginsInspectResponse(inspect=inspect)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
class AgentBindingItem(BaseModel):
agentId: str
match: dict[str, Any]
description: str
class AgentsBindingsResponse(BaseModel):
bindings: list[AgentBindingItem]
@router.get("/agents-bindings")
async def api_openclaw_agents_bindings(
agent: str | None = None,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> AgentsBindingsResponse:
"""Read `openclaw agents bindings --json [--agent <id>]` and return bindings list."""
try:
result = service.agents_bindings_model(agent=agent)
bindings = result if isinstance(result, list) else []
return AgentsBindingsResponse(bindings=bindings)
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/gateway-status")
async def api_openclaw_gateway_status(
url: str | None = None,
token: str | None = None,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> dict[str, Any]:
"""Read `openclaw gateway status --json [--url <url>] [--token <token>]`. Returns full gateway probe result."""
try:
result = service.gateway_status(url=url, token=token)
return result
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
@router.get("/memory-status")
async def api_openclaw_memory_status(
agent: str | None = None,
deep: bool = False,
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> list[dict[str, Any]]:
"""Read `openclaw memory status --json [--agent <id>] [--deep]`. Returns array of per-agent memory status."""
try:
result = service.memory_status(agent=agent, deep=deep)
return result if isinstance(result, list) else []
except OpenClawCliError as exc:
_raise_cli_http_error(exc)
class WorkspaceFilesResponse(BaseModel):
workspace: str
files: list[dict[str, Any]]
error: str | None = None
@router.get("/workspace-files")
async def api_openclaw_workspace_files(
workspace: str = Query(..., description="Path to the agent workspace directory"),
service: OpenClawCliService = Depends(get_openclaw_cli_service),
) -> WorkspaceFilesResponse:
"""List .md files in an OpenClaw agent workspace with their content previews."""
result = service.list_workspace_files(workspace)
return WorkspaceFilesResponse.model_validate(result, strict=False)

View File

@@ -5,8 +5,6 @@ from .agent_service import app as agent_app
from .agent_service import create_app as create_agent_app from .agent_service import create_app as create_agent_app
from .news_service import app as news_app from .news_service import app as news_app
from .news_service import create_app as create_news_app from .news_service import create_app as create_news_app
from .openclaw_service import app as openclaw_app
from .openclaw_service import create_app as create_openclaw_app
from .runtime_service import app as runtime_app from .runtime_service import app as runtime_app
from .runtime_service import create_app as create_runtime_app from .runtime_service import create_app as create_runtime_app
from .trading_service import app as trading_app from .trading_service import app as trading_app
@@ -23,8 +21,6 @@ __all__ = [
"create_agent_app", "create_agent_app",
"news_app", "news_app",
"create_news_app", "create_news_app",
"openclaw_app",
"create_openclaw_app",
"runtime_app", "runtime_app",
"create_runtime_app", "create_runtime_app",
"trading_app", "trading_app",

View File

@@ -1,65 +0,0 @@
# -*- coding: utf-8 -*-
"""Read-only OpenClaw CLI FastAPI surface.
COMPATIBILITY_SURFACE: deferred
OWNER: runtime-team
SEE: docs/legacy-inventory.md#openclaw-dual-integration
This is the REST facade (port 8004) for OpenClaw integration.
For the WebSocket gateway integration, see:
- backend/services/gateway_openclaw_handlers.py
- shared/client/openclaw_websocket_client.py
Key differences:
- REST facade: typed Pydantic models, request/response, polling
- WebSocket: event-driven, real-time updates, bidirectional
Decision needed: which surface becomes the long-term contract?
"""
from __future__ import annotations
from fastapi import Depends, FastAPI
from backend.api import openclaw_router
from backend.apps.cors import add_cors_middleware
from backend.api.openclaw import get_openclaw_cli_service
def create_app() -> FastAPI:
"""Create the OpenClaw service app."""
app = FastAPI(
title="大时代 OpenClaw Service",
description="Read-only OpenClaw CLI integration service surface",
version="0.1.0",
)
add_cors_middleware(app)
@app.get("/health")
async def health_check(
service=Depends(get_openclaw_cli_service),
) -> dict[str, object]:
return service.health()
@app.get("/api/status")
async def api_status(
service=Depends(get_openclaw_cli_service),
) -> dict[str, object]:
return {
"status": "operational",
"service": "openclaw-service",
"openclaw": service.health(),
}
app.include_router(openclaw_router)
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8004)

View File

@@ -1,22 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""OpenClaw WebSocket handlers — gateway calls OpenClaw Gateway via WebSocket. """OpenClaw WebSocket handlers — gateway calls OpenClaw Gateway via WebSocket.
COMPATIBILITY_SURFACE: deferred COMPATIBILITY_SURFACE: stable
OWNER: runtime-team OWNER: runtime-team
SEE: docs/legacy-inventory.md#openclaw-dual-integration
This is the WebSocket gateway integration for OpenClaw (port 18789). This is the WebSocket gateway integration for OpenClaw (port 18789).
For the REST facade, see: Frontend connects via Gateway WebSocket (port 8765) → OpenClaw Gateway (port 18789).
- backend/apps/openclaw_service.py (port 8004)
- backend/api/openclaw.py
Key differences:
- WebSocket: event-driven, real-time updates, bidirectional
- REST facade: typed Pydantic models, request/response, polling
Decision needed: which surface becomes the long-term contract?
""" """
from __future__ import annotations from __future__ import annotations
import json import json

View File

@@ -1,148 +0,0 @@
# -*- coding: utf-8 -*-
"""Tests for the extracted OpenClaw service app surface."""
from fastapi.testclient import TestClient
from backend.apps.openclaw_service import create_app
from backend.api import openclaw as openclaw_module
class _FakeOpenClawCliService:
def health(self):
return {
"status": "healthy",
"service": "openclaw-service",
"base_command": ["openclaw"],
"cwd": "/tmp/openclaw",
"binary_resolved": True,
"reference_entry_available": True,
"timeout_seconds": 15.0,
}
def status(self):
return {"runtimeVersion": "2026.3.24"}
def list_sessions(self):
return {
"sessions": [
{"key": "main/session-1", "agentId": "main"},
{"key": "analyst/session-2", "agentId": "analyst"},
]
}
def get_session(self, session_key: str):
for session in self.list_sessions()["sessions"]:
if session["key"] == session_key:
return session
raise KeyError(session_key)
def get_session_history(self, session_key: str, *, limit: int = 20):
return {
"sessionKey": session_key,
"limit": limit,
"items": [{"role": "assistant", "text": "hello"}],
}
def status_model(self):
from shared.models.openclaw import OpenClawStatus
return OpenClawStatus(runtimeVersion="2026.3.24")
def get_session_model(self, session_key: str):
from shared.models.openclaw import SessionEntry
for session in self.list_sessions()["sessions"]:
if session["key"] == session_key:
return SessionEntry.model_validate(session, strict=False)
raise KeyError(session_key)
def list_sessions_model(self):
from shared.models.openclaw import SessionsList, SessionEntry
sessions = [
SessionEntry.model_validate(s, strict=False)
for s in self.list_sessions()["sessions"]
]
return SessionsList(sessions=sessions)
def get_session_history_model(self, session_key: str, *, limit: int = 20):
from shared.models.openclaw import SessionHistory
raw = self.get_session_history(session_key, limit=limit)
return SessionHistory(
sessionKey=raw["sessionKey"],
session_id=None,
events=raw["items"],
history=raw["items"],
raw_text=None,
)
def list_cron_jobs(self):
return {"jobs": [{"id": "job-1", "name": "Daily sync"}]}
def list_cron_jobs_model(self):
from shared.models.openclaw import CronList
return CronList.from_raw(self.list_cron_jobs())
def list_approvals(self):
return {"approvals": [{"approvalId": "ap-1", "toolName": "test_tool", "status": "pending"}]}
def list_approvals_model(self):
from shared.models.openclaw import ApprovalsList
return ApprovalsList.from_raw(self.list_approvals())
def test_openclaw_service_routes_are_exposed():
app = create_app()
paths = {route.path for route in app.routes}
assert "/health" in paths
assert "/api/status" in paths
assert "/api/openclaw/status" in paths
assert "/api/openclaw/sessions" in paths
assert "/api/openclaw/sessions/{session_key:path}" in paths
assert "/api/openclaw/sessions/{session_key:path}/history" in paths
assert "/api/openclaw/cron" in paths
assert "/api/openclaw/approvals" in paths
def test_openclaw_service_read_routes():
app = create_app()
app.dependency_overrides[openclaw_module.get_openclaw_cli_service] = (
lambda: _FakeOpenClawCliService()
)
with TestClient(app) as client:
health = client.get("/health")
status = client.get("/api/status")
openclaw_status = client.get("/api/openclaw/status")
sessions = client.get("/api/openclaw/sessions")
session = client.get("/api/openclaw/sessions/main/session-1")
history = client.get("/api/openclaw/sessions/main/session-1/history", params={"limit": 5})
cron = client.get("/api/openclaw/cron")
approvals = client.get("/api/openclaw/approvals")
assert health.status_code == 200
assert health.json()["service"] == "openclaw-service"
assert status.status_code == 200
assert status.json()["status"] == "operational"
assert openclaw_status.status_code == 200
assert openclaw_status.json()["runtime_version"] == "2026.3.24"
assert sessions.status_code == 200
assert len(sessions.json()["sessions"]) == 2
assert session.status_code == 200
assert session.json()["session"]["agent_id"] == "main"
assert history.status_code == 200
assert len(history.json()["events"]) == 1
assert cron.status_code == 200
assert cron.json()["jobs"][0]["id"] == "job-1"
assert approvals.status_code == 200
assert approvals.json()["approvals"][0]["approval_id"] == "ap-1"
def test_openclaw_service_session_404():
app = create_app()
app.dependency_overrides[openclaw_module.get_openclaw_cli_service] = (
lambda: _FakeOpenClawCliService()
)
with TestClient(app) as client:
response = client.get("/api/openclaw/sessions/missing")
assert response.status_code == 404

View File

@@ -1,132 +0,0 @@
# -*- coding: utf-8 -*-
"""Tests for split-aware shared service clients."""
import pytest
from shared.client.control_client import ControlPlaneClient
from shared.client.openclaw_client import OpenClawServiceClient
from shared.client.runtime_client import RuntimeServiceClient
class _DummyResponse:
def __init__(self, payload):
self._payload = payload
def raise_for_status(self):
return None
def json(self):
return self._payload
class _DummyAsyncClient:
def __init__(self):
self.calls = []
async def get(self, path, params=None):
self.calls.append(("get", path, params))
if path == "/sessions/main/session-1":
return _DummyResponse({"session": {"key": "main/session-1", "agentId": "main"}})
return _DummyResponse({"path": path, "params": params})
async def post(self, path, json=None):
self.calls.append(("post", path, json))
return _DummyResponse({"path": path, "json": json})
async def put(self, path, json=None):
self.calls.append(("put", path, json))
return _DummyResponse({"path": path, "json": json})
async def aclose(self):
return None
@pytest.mark.asyncio
async def test_control_plane_client_hits_current_workspace_and_guard_routes():
client = ControlPlaneClient()
client._client = _DummyAsyncClient()
await client.list_workspaces()
await client.get_workspace("demo")
await client.list_agents("demo")
await client.get_agent("demo", "risk_manager")
await client.fetch_pending_approvals()
await client.approve_pending_approval("ap-1")
await client.deny_pending_approval("ap-2", reason="nope")
assert client._client.calls == [
("get", "/workspaces", None),
("get", "/workspaces/demo", None),
("get", "/workspaces/demo/agents", None),
("get", "/workspaces/demo/agents/risk_manager", None),
("get", "/guard/pending", None),
(
"post",
"/guard/approve",
{
"approval_id": "ap-1",
"one_time": True,
"expires_in_minutes": 30,
},
),
(
"post",
"/guard/deny",
{
"approval_id": "ap-2",
"reason": "nope",
},
),
]
@pytest.mark.asyncio
async def test_runtime_service_client_hits_current_runtime_routes():
client = RuntimeServiceClient()
client._client = _DummyAsyncClient()
await client.fetch_context()
await client.fetch_agents()
await client.fetch_events()
await client.fetch_gateway_port()
await client.start_runtime({"tickers": ["AAPL"]})
await client.stop_runtime(force=True)
await client.restart_runtime({"tickers": ["MSFT"]})
await client.fetch_current_runtime()
await client.get_runtime_config()
await client.update_runtime_config({"schedule_mode": "intraday"})
assert client._client.calls == [
("get", "/context", None),
("get", "/agents", None),
("get", "/events", None),
("get", "/gateway/port", None),
("post", "/start", {"tickers": ["AAPL"]}),
("post", "/stop?force=true", None),
("post", "/restart", {"tickers": ["MSFT"]}),
("get", "/current", None),
("get", "/config", None),
("put", "/config", {"schedule_mode": "intraday"}),
]
@pytest.mark.asyncio
async def test_openclaw_service_client_hits_current_openclaw_routes():
client = OpenClawServiceClient()
client._client = _DummyAsyncClient()
await client.fetch_status()
await client.list_sessions()
await client.get_session("main/session-1")
await client.get_session_history("main/session-1", limit=5)
await client.list_cron_jobs()
await client.list_approvals()
assert client._client.calls == [
("get", "/status", None),
("get", "/sessions", None),
("get", "/sessions/main/session-1", None),
("get", "/sessions/main/session-1/history", {"limit": 5}),
("get", "/cron", None),
("get", "/approvals", None),
]

View File

@@ -3,7 +3,6 @@
from shared.client.control_client import ControlPlaneClient from shared.client.control_client import ControlPlaneClient
from shared.client.news_client import NewsServiceClient from shared.client.news_client import NewsServiceClient
from shared.client.openclaw_client import OpenClawServiceClient
from shared.client.runtime_client import RuntimeServiceClient from shared.client.runtime_client import RuntimeServiceClient
from shared.client.trading_client import TradingServiceClient from shared.client.trading_client import TradingServiceClient
@@ -12,5 +11,4 @@ __all__ = [
"RuntimeServiceClient", "RuntimeServiceClient",
"TradingServiceClient", "TradingServiceClient",
"NewsServiceClient", "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()

View File

@@ -149,7 +149,7 @@ kill_port() {
do_stop() { do_stop() {
echo -e "${CYAN}停止所有服务...${NC}" echo -e "${CYAN}停止所有服务...${NC}"
for svc in frontend agent_service trading_service news_service runtime_service openclaw_service; do for svc in frontend agent_service trading_service news_service runtime_service; do
stop_service "${svc}" stop_service "${svc}"
done done
echo -e "${GREEN}已停止${NC}" echo -e "${GREEN}已停止${NC}"
@@ -162,7 +162,6 @@ do_status() {
print_status "trading_service" 8001 print_status "trading_service" 8001
print_status "news_service" 8002 print_status "news_service" 8002
print_status "runtime_service" 8003 print_status "runtime_service" 8003
print_status "openclaw_service" 8004
print_status "frontend" "${FRONTEND_PORT}" print_status "frontend" "${FRONTEND_PORT}"
echo "" echo ""
echo -e " ${CYAN}${NC} Gateway 由 runtime_service 管理,通过前端启动任务触发" echo -e " ${CYAN}${NC} Gateway 由 runtime_service 管理,通过前端启动任务触发"
@@ -214,7 +213,6 @@ start_daemon() {
start_single_daemon "trading_service" "backend.apps.trading_service:app" 8001 start_single_daemon "trading_service" "backend.apps.trading_service:app" 8001
start_single_daemon "news_service" "backend.apps.news_service:app" 8002 start_single_daemon "news_service" "backend.apps.news_service:app" 8002
start_single_daemon "runtime_service" "backend.apps.runtime_service:app" 8003 start_single_daemon "runtime_service" "backend.apps.runtime_service:app" 8003
start_single_daemon "openclaw_service" "backend.apps.openclaw_service:app" 8004
fi fi
echo -e " ${GREEN}${NC} frontend → http://0.0.0.0:${FRONTEND_PORT}" echo -e " ${GREEN}${NC} frontend → http://0.0.0.0:${FRONTEND_PORT}"
@@ -268,7 +266,6 @@ start_foreground() {
start_single_foreground "trading_service" "backend.apps.trading_service:app" 8001 start_single_foreground "trading_service" "backend.apps.trading_service:app" 8001
start_single_foreground "news_service" "backend.apps.news_service:app" 8002 start_single_foreground "news_service" "backend.apps.news_service:app" 8002
start_single_foreground "runtime_service" "backend.apps.runtime_service:app" 8003 start_single_foreground "runtime_service" "backend.apps.runtime_service:app" 8003
start_single_foreground "openclaw_service" "backend.apps.openclaw_service:app" 8004
fi fi
echo -e " ${GREEN}${NC} frontend → http://0.0.0.0:${FRONTEND_PORT}" echo -e " ${GREEN}${NC} frontend → http://0.0.0.0:${FRONTEND_PORT}"
@@ -292,13 +289,12 @@ do_start() {
export TRADING_SERVICE_URL="${TRADING_SERVICE_URL:-http://localhost:8001}" export TRADING_SERVICE_URL="${TRADING_SERVICE_URL:-http://localhost:8001}"
export NEWS_SERVICE_URL="${NEWS_SERVICE_URL:-http://localhost:8002}" export NEWS_SERVICE_URL="${NEWS_SERVICE_URL:-http://localhost:8002}"
export RUNTIME_SERVICE_URL="${RUNTIME_SERVICE_URL:-http://localhost:8003}" export RUNTIME_SERVICE_URL="${RUNTIME_SERVICE_URL:-http://localhost:8003}"
export OPENCLAW_SERVICE_URL="${OPENCLAW_SERVICE_URL:-http://localhost:8004}"
build_frontend build_frontend
echo "" echo ""
echo -e "${CYAN}停止已有服务...${NC}" echo -e "${CYAN}停止已有服务...${NC}"
for svc in frontend agent_service trading_service news_service runtime_service openclaw_service; do for svc in frontend agent_service trading_service news_service runtime_service; do
stop_service "${svc}" stop_service "${svc}"
done done