feat: update openclaw workspace integration
This commit is contained in:
@@ -499,11 +499,29 @@ class Gateway:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_workspace_files(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_workspace_file":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_workspace_file(self, websocket, data)
|
||||
elif msg_type == "openclaw_resolve_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_resolve_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_create_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_create_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_send_message":
|
||||
await gateway_openclaw_handlers.handle_openclaw_send_message(self, websocket, data)
|
||||
elif msg_type == "openclaw_subscribe_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_subscribe_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_unsubscribe_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_unsubscribe_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_reset_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_reset_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_delete_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_delete_session(self, websocket, data)
|
||||
|
||||
except websockets.ConnectionClosed:
|
||||
pass
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
finally:
|
||||
subscriber_map = getattr(self, "_openclaw_session_subscribers", None)
|
||||
if isinstance(subscriber_map, dict):
|
||||
subscriber_map.pop(websocket, None)
|
||||
|
||||
async def _handle_get_stock_history(
|
||||
self,
|
||||
|
||||
@@ -13,6 +13,63 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ensure_session_bridge(gateway) -> None:
|
||||
"""Forward OpenClaw session events into EvoTraders frontend websockets."""
|
||||
if getattr(gateway, "_openclaw_session_bridge_ready", False):
|
||||
return
|
||||
|
||||
async def _forward(event) -> None:
|
||||
payload = event.payload or {}
|
||||
session_key = str(payload.get("sessionKey") or payload.get("key") or "").strip()
|
||||
if not session_key:
|
||||
return
|
||||
|
||||
subscriber_map = getattr(gateway, "_openclaw_session_subscribers", {})
|
||||
targets = [
|
||||
ws
|
||||
for ws, session_keys in list(subscriber_map.items())
|
||||
if session_key in session_keys
|
||||
]
|
||||
if not targets:
|
||||
return
|
||||
|
||||
message = json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_event",
|
||||
"event": event.event,
|
||||
"session_key": session_key,
|
||||
"payload": payload,
|
||||
}
|
||||
)
|
||||
stale = []
|
||||
for ws in targets:
|
||||
try:
|
||||
await ws.send(message)
|
||||
except Exception:
|
||||
stale.append(ws)
|
||||
|
||||
for ws in stale:
|
||||
try:
|
||||
subscriber_map.pop(ws, None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _handler(event) -> None:
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
asyncio.create_task(_forward(event))
|
||||
except Exception as exc:
|
||||
logger.debug("OpenClaw session bridge skipped event: %s", exc)
|
||||
|
||||
client = _get_ws_client(gateway)
|
||||
client.add_event_handler(_handler)
|
||||
gateway._openclaw_session_bridge_ready = True
|
||||
gateway._openclaw_session_bridge_handler = _handler
|
||||
if not hasattr(gateway, "_openclaw_session_subscribers"):
|
||||
gateway._openclaw_session_subscribers = {}
|
||||
|
||||
|
||||
def _get_ws_client(gateway) -> "OpenClawWebSocketClient":
|
||||
"""Get the OpenClaw WebSocket client from gateway."""
|
||||
from shared.client.openclaw_websocket_client import OpenClawWebSocketClient
|
||||
@@ -44,10 +101,18 @@ async def handle_get_openclaw_sessions(gateway, websocket, data: dict) -> None:
|
||||
|
||||
async def handle_get_openclaw_session_detail(gateway, websocket, data: dict) -> None:
|
||||
session_key = data.get("session_key", "")
|
||||
result = await _ws_call(gateway, "sessions.list", {"limit": 1, "agentId": session_key.split(":")[1] if session_key else None})
|
||||
result = await _ws_call(gateway, "sessions.list", {"limit": 200, "includeLastMessage": True})
|
||||
session = None
|
||||
if isinstance(result, dict):
|
||||
for item in result.get("sessions", []) or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get("key") == session_key or item.get("sessionKey") == session_key:
|
||||
session = item
|
||||
break
|
||||
await websocket.send(json.dumps({
|
||||
"type": "openclaw_session_detail_loaded",
|
||||
"data": result,
|
||||
"data": {"session": session, "error": None if session else f"session '{session_key}' not found"},
|
||||
"session_key": session_key,
|
||||
}))
|
||||
|
||||
@@ -55,14 +120,202 @@ async def handle_get_openclaw_session_detail(gateway, websocket, data: dict) ->
|
||||
async def handle_get_openclaw_session_history(gateway, websocket, data: dict) -> None:
|
||||
session_key = data.get("session_key", "")
|
||||
limit = data.get("limit", 20)
|
||||
result = await _ws_call(gateway, "sessions.list", {"limit": limit})
|
||||
try:
|
||||
from backend.services.openclaw_cli import OpenClawCliService
|
||||
|
||||
result = OpenClawCliService().get_session_history_model(session_key, limit=limit)
|
||||
payload = {
|
||||
"session_key": result.session_key,
|
||||
"session_id": result.session_id,
|
||||
"history": result.events,
|
||||
"events": result.events,
|
||||
"raw_text": result.raw_text,
|
||||
}
|
||||
except Exception as exc:
|
||||
payload = {"error": str(exc)[:200], "history": []}
|
||||
await websocket.send(json.dumps({
|
||||
"type": "openclaw_session_history_loaded",
|
||||
"data": result,
|
||||
"data": payload,
|
||||
"session_key": session_key,
|
||||
}))
|
||||
|
||||
|
||||
async def handle_openclaw_resolve_session(gateway, websocket, data: dict) -> None:
|
||||
params = {}
|
||||
agent_id = str(data.get("agent_id") or "").strip()
|
||||
label = str(data.get("label") or "").strip()
|
||||
channel = str(data.get("channel") or "").strip()
|
||||
if agent_id:
|
||||
params["agentId"] = agent_id
|
||||
if label:
|
||||
params["label"] = label
|
||||
if channel:
|
||||
params["channel"] = channel
|
||||
params["includeGlobal"] = bool(data.get("include_global", True))
|
||||
result = await _ws_call(gateway, "sessions.resolve", params)
|
||||
await websocket.send(json.dumps({"type": "openclaw_session_resolved", "data": result}))
|
||||
|
||||
|
||||
async def handle_openclaw_create_session(gateway, websocket, data: dict) -> None:
|
||||
params = {}
|
||||
agent_id = str(data.get("agent_id") or "").strip()
|
||||
label = str(data.get("label") or "").strip()
|
||||
model = str(data.get("model") or "").strip()
|
||||
initial_message = str(data.get("initial_message") or "").strip()
|
||||
if agent_id:
|
||||
params["agentId"] = agent_id
|
||||
if label:
|
||||
params["label"] = label
|
||||
if model:
|
||||
params["model"] = model
|
||||
if initial_message:
|
||||
params["message"] = initial_message
|
||||
result = await _ws_call(gateway, "sessions.create", params)
|
||||
await websocket.send(json.dumps({"type": "openclaw_session_created", "data": result}))
|
||||
|
||||
|
||||
async def handle_openclaw_send_message(gateway, websocket, data: dict) -> None:
|
||||
session_key = str(data.get("session_key") or "").strip()
|
||||
message = str(data.get("message") or "").strip()
|
||||
thinking = str(data.get("thinking") or "").strip()
|
||||
if not session_key or not message:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_message_sent",
|
||||
"data": {"error": "session_key and message are required"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
params = {"key": session_key, "message": message}
|
||||
if thinking:
|
||||
params["thinking"] = thinking
|
||||
result = await _ws_call(gateway, "sessions.send", params)
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_message_sent",
|
||||
"data": result,
|
||||
"session_key": session_key,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def handle_openclaw_subscribe_session(gateway, websocket, data: dict) -> None:
|
||||
session_key = str(data.get("session_key") or "").strip()
|
||||
if not session_key:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_subscribed",
|
||||
"data": {"error": "session_key is required"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
_ensure_session_bridge(gateway)
|
||||
result = await _ws_call(gateway, "sessions.messages.subscribe", {"key": session_key})
|
||||
if not isinstance(result, dict) or not result.get("error"):
|
||||
subscriber_map = getattr(gateway, "_openclaw_session_subscribers", {})
|
||||
subscriber_map.setdefault(websocket, set()).add(session_key)
|
||||
gateway._openclaw_session_subscribers = subscriber_map
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_subscribed",
|
||||
"data": result,
|
||||
"session_key": session_key,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def handle_openclaw_unsubscribe_session(gateway, websocket, data: dict) -> None:
|
||||
session_key = str(data.get("session_key") or "").strip()
|
||||
if not session_key:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_unsubscribed",
|
||||
"data": {"error": "session_key is required"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
result = await _ws_call(gateway, "sessions.messages.unsubscribe", {"key": session_key})
|
||||
subscriber_map = getattr(gateway, "_openclaw_session_subscribers", {})
|
||||
session_keys = subscriber_map.get(websocket)
|
||||
if isinstance(session_keys, set):
|
||||
session_keys.discard(session_key)
|
||||
if not session_keys:
|
||||
subscriber_map.pop(websocket, None)
|
||||
gateway._openclaw_session_subscribers = subscriber_map
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_unsubscribed",
|
||||
"data": result,
|
||||
"session_key": session_key,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def handle_openclaw_reset_session(gateway, websocket, data: dict) -> None:
|
||||
session_key = str(data.get("session_key") or "").strip()
|
||||
if not session_key:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_reset",
|
||||
"data": {"error": "session_key is required"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
result = await _ws_call(gateway, "sessions.reset", {"key": session_key})
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_reset",
|
||||
"data": result,
|
||||
"session_key": session_key,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def handle_openclaw_delete_session(gateway, websocket, data: dict) -> None:
|
||||
session_key = str(data.get("session_key") or "").strip()
|
||||
if not session_key:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_deleted",
|
||||
"data": {"error": "session_key is required"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
result = await _ws_call(gateway, "sessions.delete", {"key": session_key})
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "openclaw_session_deleted",
|
||||
"data": result,
|
||||
"session_key": session_key,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def handle_get_openclaw_cron(gateway, websocket, data: dict) -> None:
|
||||
result = await _ws_call(gateway, "cron.list")
|
||||
await websocket.send(json.dumps({"type": "openclaw_cron_loaded", "data": result}))
|
||||
@@ -75,6 +328,106 @@ async def handle_get_openclaw_approvals(gateway, websocket, data: dict) -> None:
|
||||
|
||||
async def handle_get_openclaw_agents(gateway, websocket, data: dict) -> None:
|
||||
result = await _ws_call(gateway, "agents.list")
|
||||
sessions_result = await _ws_call(
|
||||
gateway,
|
||||
"sessions.list",
|
||||
{"limit": 200, "includeLastMessage": True},
|
||||
)
|
||||
config_result = await _ws_call(gateway, "config.get")
|
||||
session_model_by_agent: dict[str, str] = {}
|
||||
default_session_model: str | None = None
|
||||
agent_skills_by_id: dict[str, list[str] | None] = {}
|
||||
default_agent_skills: list[str] | None = None
|
||||
|
||||
parsed_config = config_result.get("parsed") if isinstance(config_result, dict) else None
|
||||
if isinstance(parsed_config, dict):
|
||||
agents_cfg = parsed_config.get("agents")
|
||||
if isinstance(agents_cfg, dict):
|
||||
defaults_cfg = agents_cfg.get("defaults")
|
||||
if isinstance(defaults_cfg, dict):
|
||||
default_skills = defaults_cfg.get("skills")
|
||||
if isinstance(default_skills, list):
|
||||
default_agent_skills = [
|
||||
str(skill).strip()
|
||||
for skill in default_skills
|
||||
if str(skill).strip()
|
||||
]
|
||||
list_cfg = agents_cfg.get("list")
|
||||
if isinstance(list_cfg, list):
|
||||
for entry in list_cfg:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
agent_id = str(entry.get("id") or "").strip()
|
||||
if not agent_id:
|
||||
continue
|
||||
skills = entry.get("skills")
|
||||
if isinstance(skills, list):
|
||||
agent_skills_by_id[agent_id] = [
|
||||
str(skill).strip()
|
||||
for skill in skills
|
||||
if str(skill).strip()
|
||||
]
|
||||
elif skills == []:
|
||||
agent_skills_by_id[agent_id] = []
|
||||
|
||||
if isinstance(sessions_result, dict) and isinstance(sessions_result.get("sessions"), list):
|
||||
defaults = sessions_result.get("defaults")
|
||||
if isinstance(defaults, dict):
|
||||
value = (
|
||||
defaults.get("model")
|
||||
or defaults.get("modelName")
|
||||
or defaults.get("model_name")
|
||||
)
|
||||
if value:
|
||||
default_session_model = str(value)
|
||||
for session in sessions_result.get("sessions", []):
|
||||
if not isinstance(session, dict):
|
||||
continue
|
||||
agent_id = str(
|
||||
session.get("agentId")
|
||||
or session.get("agent_id")
|
||||
or ""
|
||||
).strip()
|
||||
if not agent_id:
|
||||
key = str(session.get("key") or session.get("sessionKey") or "").strip()
|
||||
parts = key.split(":")
|
||||
if len(parts) >= 3 and parts[0] == "agent":
|
||||
agent_id = parts[1]
|
||||
model_value = (
|
||||
session.get("model")
|
||||
or session.get("modelName")
|
||||
or session.get("model_name")
|
||||
or session.get("resolvedModel")
|
||||
or session.get("resolved_model")
|
||||
or session.get("defaultModel")
|
||||
or session.get("default_model")
|
||||
)
|
||||
if agent_id and model_value and agent_id not in session_model_by_agent:
|
||||
session_model_by_agent[agent_id] = str(model_value)
|
||||
|
||||
if isinstance(result, dict) and isinstance(result.get("agents"), list):
|
||||
normalized_agents = []
|
||||
for agent in result.get("agents", []):
|
||||
if not isinstance(agent, dict):
|
||||
normalized_agents.append(agent)
|
||||
continue
|
||||
normalized = dict(agent)
|
||||
if not normalized.get("model"):
|
||||
normalized["model"] = (
|
||||
normalized.get("modelName")
|
||||
or normalized.get("model_name")
|
||||
or normalized.get("resolvedModel")
|
||||
or normalized.get("resolved_model")
|
||||
or normalized.get("defaultModel")
|
||||
or normalized.get("default_model")
|
||||
or session_model_by_agent.get(str(normalized.get("id") or "").strip())
|
||||
or default_session_model
|
||||
)
|
||||
agent_id = str(normalized.get("id") or "").strip()
|
||||
if "skills" not in normalized:
|
||||
normalized["skills"] = agent_skills_by_id.get(agent_id, default_agent_skills)
|
||||
normalized_agents.append(normalized)
|
||||
result = {**result, "agents": normalized_agents}
|
||||
await websocket.send(json.dumps({"type": "openclaw_agents_loaded", "data": result}))
|
||||
|
||||
|
||||
@@ -84,7 +437,9 @@ async def handle_get_openclaw_agents_presence(gateway, websocket, data: dict) ->
|
||||
|
||||
|
||||
async def handle_get_openclaw_skills(gateway, websocket, data: dict) -> None:
|
||||
result = await _ws_call(gateway, "skills.status")
|
||||
agent_id = str(data.get("agent_id") or "").strip()
|
||||
params = {"agentId": agent_id} if agent_id else {}
|
||||
result = await _ws_call(gateway, "skills.status", params)
|
||||
await websocket.send(json.dumps({"type": "openclaw_skills_loaded", "data": result}))
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class StorageService:
|
||||
self,
|
||||
dashboard_dir: Path,
|
||||
initial_cash: float = 100000.0,
|
||||
config_name: str = "mock",
|
||||
config_name: str = "live",
|
||||
):
|
||||
"""
|
||||
Initialize storage service
|
||||
|
||||
@@ -311,6 +311,17 @@ class TestRiskAgent:
|
||||
|
||||
|
||||
class TestStorageService:
|
||||
def test_storage_service_defaults_to_live_config(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
storage = StorageService(
|
||||
dashboard_dir=Path(tmpdir),
|
||||
initial_cash=100000.0,
|
||||
)
|
||||
|
||||
assert storage.config_name == "live"
|
||||
|
||||
def test_calculate_portfolio_value_cash_only(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
|
||||
74
backend/tests/test_openclaw_websocket_client.py
Normal file
74
backend/tests/test_openclaw_websocket_client.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the OpenClaw WebSocket client session helpers."""
|
||||
|
||||
import pytest
|
||||
|
||||
from shared.client.openclaw_websocket_client import OpenClawWebSocketClient
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_session_parses_gateway_key_response():
|
||||
client = OpenClawWebSocketClient(gateway_token="test-token")
|
||||
|
||||
async def fake_send_request(method, params=None, _allow_handshake=False):
|
||||
assert method == "sessions.resolve"
|
||||
assert params["agentId"] == "main"
|
||||
return {"ok": True, "key": "agent:main:main"}
|
||||
|
||||
client._send_request = fake_send_request # type: ignore[method-assign]
|
||||
|
||||
resolved = await client.resolve_session(agent_id="main")
|
||||
|
||||
assert resolved == "agent:main:main"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_uses_session_send_payload():
|
||||
client = OpenClawWebSocketClient(gateway_token="test-token")
|
||||
|
||||
async def fake_send_request(method, params=None, _allow_handshake=False):
|
||||
assert method == "sessions.send"
|
||||
assert params == {
|
||||
"key": "agent:main:main",
|
||||
"message": "hello",
|
||||
"thinking": "medium",
|
||||
}
|
||||
return {"ok": True, "runId": "run-1"}
|
||||
|
||||
client._send_request = fake_send_request # type: ignore[method-assign]
|
||||
|
||||
result = await client.send_message("agent:main:main", "hello", thinking="medium")
|
||||
|
||||
assert result["runId"] == "run-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_session_history_uses_sessions_preview():
|
||||
client = OpenClawWebSocketClient(gateway_token="test-token")
|
||||
|
||||
async def fake_send_request(method, params=None, _allow_handshake=False):
|
||||
assert method == "sessions.preview"
|
||||
assert params == {"keys": ["agent:main:main"], "limit": 12}
|
||||
return {"previews": []}
|
||||
|
||||
client._send_request = fake_send_request # type: ignore[method-assign]
|
||||
|
||||
result = await client.get_session_history("agent:main:main", limit=12)
|
||||
|
||||
assert result == {"previews": []}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unsubscribe_uses_session_messages_unsubscribe():
|
||||
client = OpenClawWebSocketClient(gateway_token="test-token")
|
||||
|
||||
async def fake_send_request(method, params=None, _allow_handshake=False):
|
||||
assert method == "sessions.messages.unsubscribe"
|
||||
assert params == {"key": "agent:main:main"}
|
||||
return {"subscribed": False}
|
||||
|
||||
client._send_request = fake_send_request # type: ignore[method-assign]
|
||||
|
||||
result = await client.unsubscribe("agent:main:main")
|
||||
|
||||
assert result == {"subscribed": False}
|
||||
Reference in New Issue
Block a user