feat: update openclaw workspace integration
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,7 +51,6 @@ node_modules
|
||||
outputs/
|
||||
/production/
|
||||
/smoke_test/
|
||||
/smoke_live_mock/
|
||||
|
||||
# Local tooling state
|
||||
.omc/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"timestamp": "2026-03-26T17:14:45.135Z",
|
||||
"timestamp": "2026-03-27T04:53:52.906Z",
|
||||
"backgroundTasks": [],
|
||||
"sessionStartTimestamp": "2026-03-26T17:13:16.686Z",
|
||||
"sessionId": "83f172c1-eb0f-4418-87a5-b9d4b6ce5b61"
|
||||
"sessionStartTimestamp": "2026-03-27T04:53:21.944Z",
|
||||
"sessionId": "cbb9004e-771b-4e82-95d4-cea6d9753642"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"session_id":"83f172c1-eb0f-4418-87a5-b9d4b6ce5b61","transcript_path":"/Users/cillin/.claude/projects/-Users-cillin-workspeace-evotraders/83f172c1-eb0f-4418-87a5-b9d4b6ce5b61.jsonl","cwd":"/Users/cillin/workspeace/evotraders","model":{"id":"MiniMax-M2.7-highspeed","display_name":"MiniMax-M2.7-highspeed"},"workspace":{"current_dir":"/Users/cillin/workspeace/evotraders","project_dir":"/Users/cillin/workspeace/evotraders","added_dirs":[]},"version":"2.1.78","output_style":{"name":"default"},"cost":{"total_cost_usd":98.95595149999994,"total_duration_ms":43461876,"total_api_duration_ms":7482894,"total_lines_added":2289,"total_lines_removed":1132},"context_window":{"total_input_tokens":949049,"total_output_tokens":356074,"context_window_size":200000,"current_usage":{"input_tokens":507,"output_tokens":72,"cache_creation_input_tokens":346,"cache_read_input_tokens":82368},"used_percentage":42,"remaining_percentage":58},"exceeds_200k_tokens":false}
|
||||
{"session_id":"cbb9004e-771b-4e82-95d4-cea6d9753642","transcript_path":"/Users/cillin/.claude/projects/-Users-cillin-workspeace-evotraders/cbb9004e-771b-4e82-95d4-cea6d9753642.jsonl","cwd":"/Users/cillin/workspeace/evotraders","model":{"id":"MiniMax-M2.7-highspeed","display_name":"MiniMax-M2.7-highspeed"},"workspace":{"current_dir":"/Users/cillin/workspeace/evotraders","project_dir":"/Users/cillin/workspeace/evotraders","added_dirs":[]},"version":"2.1.78","output_style":{"name":"default"},"cost":{"total_cost_usd":0.660433,"total_duration_ms":168502,"total_api_duration_ms":37670,"total_lines_added":0,"total_lines_removed":0},"context_window":{"total_input_tokens":14416,"total_output_tokens":1705,"context_window_size":200000,"current_usage":{"input_tokens":461,"output_tokens":214,"cache_creation_input_tokens":0,"cache_read_input_tokens":53991},"used_percentage":27,"remaining_percentage":73},"exceeds_200k_tokens":false}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"lastSentAt": "2026-03-27T03:08:22.675Z"
|
||||
"lastSentAt": "2026-03-27T04:55:49.635Z"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "ace758bdbd117358d",
|
||||
"agent_type": "Explore",
|
||||
"started_at": "2026-03-26T17:16:09.450Z",
|
||||
"parent_mode": "none",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-03-26T17:17:33.704Z",
|
||||
"duration_ms": 84254
|
||||
}
|
||||
],
|
||||
"total_spawned": 1,
|
||||
"total_completed": 1,
|
||||
"total_failed": 0,
|
||||
"last_updated": "2026-03-27T03:08:25.014Z"
|
||||
}
|
||||
@@ -20,7 +20,6 @@ uv pip install -e .
|
||||
evotraders backtest --start 2025-11-01 --end 2025-12-01 # 回测模式
|
||||
evotraders backtest --start 2025-11-01 --end 2025-12-01 --enable-memory # 带记忆回测
|
||||
evotraders live # 实盘交易
|
||||
evotraders live --mock # 模拟/测试模式
|
||||
evotraders live -t 22:30 # 定时每日交易
|
||||
evotraders frontend # 启动可视化界面
|
||||
|
||||
@@ -28,7 +27,7 @@ evotraders frontend # 启动可视化界面
|
||||
./start-dev.sh # 启动全部 4 个微服务 (agent, runtime, trading, news)
|
||||
|
||||
# Gateway WebSocket 服务器
|
||||
python backend/main.py --mode live --config-name mock --mock
|
||||
python backend/main.py --mode live --config-name live
|
||||
|
||||
# 单独启动微服务
|
||||
python -m uvicorn backend.apps.runtime_service:app --host 0.0.0.0 --port 8003 --reload
|
||||
@@ -189,7 +188,6 @@ backend/
|
||||
│ ├── schema.py # 数据 schema
|
||||
│ ├── historical_price_manager.py # 历史价格管理
|
||||
│ ├── polling_price_manager.py # 轮询价格管理
|
||||
│ ├── mock_price_manager.py # Mock 价格管理
|
||||
│ ├── news_alignment.py # 新闻对齐
|
||||
│ ├── polygon_client.py # Polygon.io 客户端
|
||||
│ └── ret_data_updater.py # 离线数据更新
|
||||
|
||||
@@ -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}
|
||||
@@ -2626,6 +2626,5 @@
|
||||
"trading_days_completed": 5,
|
||||
"server_mode": "backtest",
|
||||
"is_backtest": true,
|
||||
"is_mock_mode": false,
|
||||
"last_saved": "2026-03-12T23:07:31.098122"
|
||||
}
|
||||
@@ -13,6 +13,9 @@
|
||||
"preview:host": "vite preview --host"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dicebear/collection": "^9.4.2",
|
||||
"@dicebear/core": "^9.4.2",
|
||||
"@lobehub/icons": "^5.0.1",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
|
||||
@@ -234,6 +234,26 @@ export default function LiveTradingApp() {
|
||||
workspaceFilesByAgent,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSocketReady || !clientRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
AGENTS.forEach((agent) => {
|
||||
if (!agent?.id) {
|
||||
return;
|
||||
}
|
||||
if (!agentProfilesByAgent[agent.id]) {
|
||||
requestAgentProfile(agent.id);
|
||||
}
|
||||
});
|
||||
}, [
|
||||
agentProfilesByAgent,
|
||||
clientRef,
|
||||
isSocketReady,
|
||||
requestAgentProfile,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const symbols = runtimeControls.displayTickers
|
||||
.map((ticker) => ticker.symbol)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ASSETS } from '../config/constants';
|
||||
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
|
||||
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||
|
||||
/**
|
||||
* Get rank medal/trophy
|
||||
@@ -207,14 +208,18 @@ export default function AgentCard({ agent, onClose, isClosing }) {
|
||||
justifyContent: 'center',
|
||||
marginBottom: 4
|
||||
}}>
|
||||
{modelInfo.logoPath ? (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
{agent.modelName || modelInfo.logoPath ? (
|
||||
<LobeModelLogo
|
||||
model={agent.modelName}
|
||||
provider={agent.modelProvider}
|
||||
fallbackSrc={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
size={36}
|
||||
type="color"
|
||||
shape="square"
|
||||
style={{
|
||||
maxHeight: '100%',
|
||||
maxWidth: '100%',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { formatTime } from '../utils/formatters';
|
||||
import { MESSAGE_COLORS, getAgentColors, AGENTS, ASSETS } from '../config/constants';
|
||||
import { getModelIcon } from '../utils/modelIcons';
|
||||
import MarkdownModal from './MarkdownModal';
|
||||
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||
|
||||
const isAnalyst = (agentId, agentName) => {
|
||||
if (agentId && agentId.includes('analyst')) return true;
|
||||
@@ -167,11 +168,11 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
// Get current selection display info
|
||||
const getCurrentSelectionInfo = () => {
|
||||
if (selectedAgent === 'all') {
|
||||
return { label: '全部角色', modelInfo: null };
|
||||
return { label: '全部角色', modelInfo: null, agentInfo: null };
|
||||
}
|
||||
const agentInfo = getAgentInfoByName(selectedAgent);
|
||||
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
|
||||
return { label: selectedAgent, modelInfo };
|
||||
return { label: selectedAgent, modelInfo, agentInfo };
|
||||
};
|
||||
|
||||
const currentSelection = getCurrentSelectionInfo();
|
||||
@@ -189,11 +190,16 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
onBlur={() => setTimeout(() => setDropdownOpen(false), 200)}
|
||||
>
|
||||
<div className="custom-select-value">
|
||||
{currentSelection.modelInfo?.logoPath && (
|
||||
<img
|
||||
src={currentSelection.modelInfo.logoPath}
|
||||
alt={currentSelection.modelInfo.provider}
|
||||
{(currentSelection.agentInfo?.modelName || currentSelection.modelInfo?.logoPath) && (
|
||||
<LobeModelLogo
|
||||
model={currentSelection.agentInfo?.modelName}
|
||||
provider={currentSelection.agentInfo?.modelProvider}
|
||||
fallbackSrc={currentSelection.modelInfo?.logoPath}
|
||||
alt={currentSelection.modelInfo?.provider}
|
||||
size={18}
|
||||
className="select-model-icon"
|
||||
shape="square"
|
||||
type="color"
|
||||
/>
|
||||
)}
|
||||
<span>{currentSelection.label}</span>
|
||||
@@ -223,11 +229,16 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{modelInfo?.logoPath && (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
{(agentInfo?.modelName || modelInfo?.logoPath) && (
|
||||
<LobeModelLogo
|
||||
model={agentInfo?.modelName}
|
||||
provider={agentInfo?.modelProvider}
|
||||
fallbackSrc={modelInfo?.logoPath}
|
||||
alt={modelInfo?.provider}
|
||||
size={18}
|
||||
className="select-model-icon"
|
||||
shape="square"
|
||||
type="color"
|
||||
/>
|
||||
)}
|
||||
<span>{agent}</span>
|
||||
@@ -363,16 +374,16 @@ function ConferenceMessage({ message, getAgentModelInfo }) {
|
||||
return (
|
||||
<div className="conf-message-item">
|
||||
<div className="conf-agent-name" style={{ color: agentColors.text, display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
|
||||
{modelInfo.logoPath && (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
{(agentModelData.modelName || modelInfo.logoPath) && (
|
||||
<LobeModelLogo
|
||||
model={agentModelData.modelName}
|
||||
provider={agentModelData.modelProvider}
|
||||
fallbackSrc={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
size={20}
|
||||
shape="circle"
|
||||
type="color"
|
||||
style={{ borderRadius: '50%' }}
|
||||
/>
|
||||
)}
|
||||
{message.agent}
|
||||
@@ -591,16 +602,16 @@ function MessageItem({ message, itemId, isHighlighted, getAgentModelInfo }) {
|
||||
>
|
||||
<div className="feed-item-header">
|
||||
<span className="feed-item-title" style={{ color: colors.text, display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
|
||||
{modelInfo.logoPath && message.agent !== 'Memory' && (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
{message.agent !== 'Memory' && (agentModelData.modelName || modelInfo.logoPath) && (
|
||||
<LobeModelLogo
|
||||
model={agentModelData.modelName}
|
||||
provider={agentModelData.modelProvider}
|
||||
fallbackSrc={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
size={20}
|
||||
shape="circle"
|
||||
type="color"
|
||||
style={{ borderRadius: '50%' }}
|
||||
/>
|
||||
)}
|
||||
{title}
|
||||
|
||||
78
frontend/src/components/LobeModelLogo.jsx
Normal file
78
frontend/src/components/LobeModelLogo.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import ModelIcon from '@lobehub/icons/es/features/ModelIcon';
|
||||
import ProviderIcon from '@lobehub/icons/es/features/ProviderIcon';
|
||||
|
||||
export default function LobeModelLogo({
|
||||
model,
|
||||
provider,
|
||||
fallbackSrc = null,
|
||||
alt = '',
|
||||
size = 28,
|
||||
shape = 'square',
|
||||
type = 'color',
|
||||
style = {},
|
||||
className = '',
|
||||
}) {
|
||||
const hasModel = typeof model === 'string' && model.trim().length > 0;
|
||||
const hasProvider = typeof provider === 'string' && provider.trim().length > 0;
|
||||
|
||||
try {
|
||||
if (hasModel) {
|
||||
return (
|
||||
<ModelIcon
|
||||
model={model}
|
||||
size={size}
|
||||
shape={shape}
|
||||
type={type}
|
||||
className={className}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasProvider) {
|
||||
return (
|
||||
<ProviderIcon
|
||||
provider={provider.toLowerCase()}
|
||||
size={size}
|
||||
shape={shape}
|
||||
type={type}
|
||||
className={className}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Fall through to local fallback asset.
|
||||
}
|
||||
|
||||
if (fallbackSrc) {
|
||||
return (
|
||||
<img
|
||||
src={fallbackSrc}
|
||||
alt={alt}
|
||||
className={className}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
objectFit: 'contain',
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: shape === 'circle' ? '50%' : 8,
|
||||
background: '#F3F4F6',
|
||||
border: '1px solid #D1D5DB',
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,5 @@
|
||||
import { OpenClawStatus } from './OpenClawStatus';
|
||||
|
||||
export default function OpenClawView() {
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
padding: '16px',
|
||||
background: '#F3F4F6',
|
||||
}}>
|
||||
<OpenClawStatus />
|
||||
</div>
|
||||
);
|
||||
return <OpenClawStatus />;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'
|
||||
import { ASSETS, SCENE_NATIVE, AGENT_SEATS, AGENTS } from '../config/constants';
|
||||
import AgentCard from './AgentCard';
|
||||
import { getModelIcon } from '../utils/modelIcons';
|
||||
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||
|
||||
/**
|
||||
* Custom hook to load an image
|
||||
@@ -518,21 +519,23 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
{medal}
|
||||
</span>
|
||||
)}
|
||||
{modelInfo.logoPath && (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
{(agentData?.modelName || modelInfo.logoPath) && (
|
||||
<LobeModelLogo
|
||||
model={agentData?.modelName}
|
||||
provider={agentData?.modelProvider}
|
||||
fallbackSrc={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
size={25}
|
||||
shape="circle"
|
||||
type="color"
|
||||
className="agent-model-badge"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -12,
|
||||
right: -12,
|
||||
width: 25,
|
||||
height: 25,
|
||||
borderRadius: '50%',
|
||||
border: '2px solid #ffffff',
|
||||
background: '#ffffff',
|
||||
objectFit: 'contain',
|
||||
padding: 2,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
pointerEvents: 'none'
|
||||
@@ -642,10 +645,15 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
|
||||
{/* Agent header with model icon */}
|
||||
<div className="room-bubble-header">
|
||||
{modelInfo.logoPath && (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
{(agentData?.modelName || modelInfo.logoPath) && (
|
||||
<LobeModelLogo
|
||||
model={agentData?.modelName}
|
||||
provider={agentData?.modelProvider}
|
||||
fallbackSrc={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
size={18}
|
||||
shape="circle"
|
||||
type="color"
|
||||
className="bubble-model-icon"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import JSZip from 'jszip';
|
||||
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
|
||||
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||
|
||||
export default function TraderView({
|
||||
agents,
|
||||
@@ -249,13 +250,16 @@ export default function TraderView({
|
||||
alignItems: 'center',
|
||||
gap: 10
|
||||
}}>
|
||||
{modelInfo.logoPath && (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
style={{ width: 26, height: 26, borderRadius: 999 }}
|
||||
/>
|
||||
)}
|
||||
<LobeModelLogo
|
||||
model={profile.model_name}
|
||||
provider={profile.model_provider}
|
||||
fallbackSrc={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
size={26}
|
||||
shape="circle"
|
||||
type="color"
|
||||
style={{ borderRadius: 999 }}
|
||||
/>
|
||||
<div style={{ display: 'grid', gap: 2 }}>
|
||||
<div style={{ fontSize: 11, color: '#4B5563', fontWeight: 700 }}>模型</div>
|
||||
<div style={{ fontSize: 12, color: '#111111', fontWeight: 800 }}>
|
||||
|
||||
@@ -58,6 +58,92 @@ export function useOpenClawPanel() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resolveSession = useCallback(({ agentId, label = null, channel = null, includeGlobal = true }) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setChatError?.(null);
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "openclaw_resolve_session",
|
||||
agent_id: agentId,
|
||||
label,
|
||||
channel,
|
||||
include_global: includeGlobal,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const createSession = useCallback(({ agentId, label = null, model = null, initialMessage = null }) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client || !agentId) return;
|
||||
store.setChatError?.(null);
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "openclaw_create_session",
|
||||
agent_id: agentId,
|
||||
label,
|
||||
model,
|
||||
initial_message: initialMessage,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const subscribeSession = useCallback((sessionKey) => {
|
||||
const client = getStore().clientRef?.current;
|
||||
if (!client || !sessionKey) return;
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "openclaw_subscribe_session",
|
||||
session_key: sessionKey,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const unsubscribeSession = useCallback((sessionKey) => {
|
||||
const client = getStore().clientRef?.current;
|
||||
if (!client || !sessionKey) return;
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "openclaw_unsubscribe_session",
|
||||
session_key: sessionKey,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetSession = useCallback((sessionKey) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client || !sessionKey) return;
|
||||
store.setChatError?.(null);
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "openclaw_reset_session",
|
||||
session_key: sessionKey,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteSession = useCallback((sessionKey) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client || !sessionKey) return;
|
||||
store.setChatError?.(null);
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "openclaw_delete_session",
|
||||
session_key: sessionKey,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const sendSessionMessage = useCallback((sessionKey, message, thinking = null) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client || !sessionKey || !message?.trim()) return;
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "openclaw_subscribe_session",
|
||||
session_key: sessionKey,
|
||||
});
|
||||
store.setOpenclawChatSendingForSession?.(sessionKey, true);
|
||||
store.setChatError?.(null);
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "openclaw_send_message",
|
||||
session_key: sessionKey,
|
||||
message: message.trim(),
|
||||
thinking,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestCron = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
@@ -91,13 +177,13 @@ export function useOpenClawPanel() {
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_agents_presence" });
|
||||
}, []);
|
||||
|
||||
const requestSkills = useCallback(() => {
|
||||
const requestSkills = useCallback((agentId = null) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setSkillsLoading(true);
|
||||
store.setSkillsError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_skills" });
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_skills", agent_id: agentId });
|
||||
}, []);
|
||||
|
||||
const requestModels = useCallback(() => {
|
||||
@@ -239,6 +325,13 @@ export function useOpenClawPanel() {
|
||||
requestSessions,
|
||||
requestSessionDetail,
|
||||
requestSessionHistory,
|
||||
resolveSession,
|
||||
createSession,
|
||||
subscribeSession,
|
||||
unsubscribeSession,
|
||||
resetSession,
|
||||
deleteSession,
|
||||
sendSessionMessage,
|
||||
requestCron,
|
||||
requestApprovals,
|
||||
requestAgents,
|
||||
|
||||
@@ -66,6 +66,306 @@ function buildTickersFromSymbols(symbols, previousTickers = []) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeOpenClawHistoryItems(history) {
|
||||
if (!Array.isArray(history)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return history
|
||||
.map((item, index) => {
|
||||
const role = item?.role || item?.senderRole || item?.kind || item?.type || 'event';
|
||||
const isFinal = hasOpenClawFinalTag(item);
|
||||
const text = extractOpenClawText(item);
|
||||
if (!shouldKeepOpenClawMessage(item)) {
|
||||
return null;
|
||||
}
|
||||
const timestamp = item?.timestamp || item?.ts || item?.createdAt || item?.time || null;
|
||||
const nestedMeta = item?.message?.__openclaw || item?.__openclaw || null;
|
||||
const seq = item?.messageSeq ?? item?.seq ?? nestedMeta?.seq ?? null;
|
||||
const messageId = item?.messageId ?? item?.id ?? nestedMeta?.id ?? null;
|
||||
|
||||
return {
|
||||
id: messageId || (seq !== null ? `seq:${seq}` : `${timestamp || 'history'}:${index}`),
|
||||
role,
|
||||
text: String(text || ''),
|
||||
timestamp,
|
||||
seq,
|
||||
messageId,
|
||||
isFinal,
|
||||
raw: item,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function unwrapOpenClawFinal(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const match = value.match(/<final>([\s\S]*?)<\/final>/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return match[1].trim();
|
||||
}
|
||||
|
||||
function stripOpenClawFinalTags(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value ? String(value) : '';
|
||||
}
|
||||
return value.replace(/<\/?final>/gi, '').trim();
|
||||
}
|
||||
|
||||
function shouldHideOpenClawMessage({ role, text }) {
|
||||
const normalizedRole = String(role || '').toLowerCase();
|
||||
const normalizedText = String(text || '').trim();
|
||||
|
||||
if (normalizedRole === 'system') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedRole === 'user') {
|
||||
if (normalizedText.startsWith('Sender (untrusted metadata):')) {
|
||||
return true;
|
||||
}
|
||||
if (normalizedText.startsWith('[Fri ') || normalizedText.startsWith('[Sat ') || normalizedText.startsWith('[Sun ')
|
||||
|| normalizedText.startsWith('[Mon ') || normalizedText.startsWith('[Tue ') || normalizedText.startsWith('[Wed ')
|
||||
|| normalizedText.startsWith('[Thu ')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldKeepOpenClawMessage(item) {
|
||||
const role = item?.role || item?.senderRole || item?.kind || item?.type || 'event';
|
||||
const text = extractOpenClawText(item);
|
||||
const isFinal = hasOpenClawFinalTag(item);
|
||||
|
||||
if (shouldHideOpenClawMessage({ role, text })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedRole = String(role || '').toLowerCase();
|
||||
if (normalizedRole === 'assistant') {
|
||||
return isFinal;
|
||||
}
|
||||
|
||||
if (!normalizedRole || normalizedRole === 'event') {
|
||||
return isFinal;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasOpenClawFinalTag(item) {
|
||||
if (typeof item === 'string') {
|
||||
return /<final>[\s\S]*?<\/final>/i.test(item);
|
||||
}
|
||||
|
||||
if (!item || typeof item !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
if (typeof item.text === 'string') candidates.push(item.text);
|
||||
if (typeof item.message === 'string') candidates.push(item.message);
|
||||
if (typeof item.content === 'string') candidates.push(item.content);
|
||||
|
||||
const nestedMessage = item.message && typeof item.message === 'object' ? item.message : null;
|
||||
if (nestedMessage) {
|
||||
if (typeof nestedMessage.content === 'string') candidates.push(nestedMessage.content);
|
||||
if (Array.isArray(nestedMessage.content)) {
|
||||
nestedMessage.content.forEach((entry) => {
|
||||
if (typeof entry === 'string') candidates.push(entry);
|
||||
if (entry?.type === 'text' && typeof entry?.text === 'string') candidates.push(entry.text);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(item.content)) {
|
||||
item.content.forEach((entry) => {
|
||||
if (typeof entry === 'string') candidates.push(entry);
|
||||
if (entry?.type === 'text' && typeof entry?.text === 'string') candidates.push(entry.text);
|
||||
});
|
||||
}
|
||||
|
||||
return candidates.some((value) => /<final>[\s\S]*?<\/final>/i.test(value));
|
||||
}
|
||||
|
||||
function extractOpenClawText(item) {
|
||||
if (typeof item === 'string') {
|
||||
return unwrapOpenClawFinal(item) || stripOpenClawFinalTags(item);
|
||||
}
|
||||
|
||||
if (!item || typeof item !== 'object') {
|
||||
return item ? String(item) : '';
|
||||
}
|
||||
|
||||
if (typeof item.text === 'string' && item.text.trim()) {
|
||||
return unwrapOpenClawFinal(item.text) || stripOpenClawFinalTags(item.text);
|
||||
}
|
||||
if (typeof item.message === 'string' && item.message.trim()) {
|
||||
return unwrapOpenClawFinal(item.message) || stripOpenClawFinalTags(item.message);
|
||||
}
|
||||
if (typeof item.content === 'string' && item.content.trim()) {
|
||||
return unwrapOpenClawFinal(item.content) || stripOpenClawFinalTags(item.content);
|
||||
}
|
||||
|
||||
const nestedMessage = item.message && typeof item.message === 'object' ? item.message : null;
|
||||
if (nestedMessage) {
|
||||
if (typeof nestedMessage.content === 'string' && nestedMessage.content.trim()) {
|
||||
return unwrapOpenClawFinal(nestedMessage.content) || stripOpenClawFinalTags(nestedMessage.content);
|
||||
}
|
||||
if (Array.isArray(nestedMessage.content)) {
|
||||
const textBlock = nestedMessage.content.find((entry) => entry?.type === 'text' && typeof entry?.text === 'string');
|
||||
if (textBlock?.text) {
|
||||
return unwrapOpenClawFinal(textBlock.text) || stripOpenClawFinalTags(textBlock.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(item.content)) {
|
||||
const textParts = item.content
|
||||
.map((entry) => {
|
||||
if (typeof entry === 'string') return entry;
|
||||
if (entry?.type === 'text' && typeof entry?.text === 'string') return entry.text;
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (textParts.length > 0) {
|
||||
const merged = textParts.join('\n');
|
||||
return unwrapOpenClawFinal(merged) || stripOpenClawFinalTags(merged);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof item.summary === 'string' && item.summary.trim()) {
|
||||
return item.summary;
|
||||
}
|
||||
if (typeof item.value === 'string' && item.value.trim()) {
|
||||
return item.value;
|
||||
}
|
||||
|
||||
return JSON.stringify(item);
|
||||
}
|
||||
|
||||
function normalizeOpenClawLiveEvent(evt) {
|
||||
const payload = evt?.payload || {};
|
||||
const nestedMessage = payload?.message && typeof payload.message === 'object' ? payload.message : null;
|
||||
const nestedMeta = nestedMessage?.__openclaw || payload?.__openclaw || null;
|
||||
const isFinal = hasOpenClawFinalTag(payload);
|
||||
const text = extractOpenClawText(payload) || evt?.event || '';
|
||||
const role =
|
||||
payload.role
|
||||
|| nestedMessage?.role
|
||||
|| payload.senderRole
|
||||
|| payload.kind
|
||||
|| evt?.event
|
||||
|| 'event';
|
||||
const seq = payload.messageSeq ?? payload.seq ?? nestedMeta?.seq ?? null;
|
||||
const messageId = payload.messageId ?? payload.id ?? nestedMeta?.id ?? null;
|
||||
|
||||
return {
|
||||
id: messageId || (seq !== null ? `seq:${seq}` : `${evt?.event || 'event'}:${Date.now()}`),
|
||||
role,
|
||||
text: String(text),
|
||||
timestamp: payload.timestamp || payload.ts || new Date().toISOString(),
|
||||
seq,
|
||||
messageId,
|
||||
isFinal,
|
||||
raw: payload,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldAppendOpenClawLiveEvent(evt) {
|
||||
const name = String(evt?.event || '');
|
||||
const payload = evt?.payload || {};
|
||||
if (name === 'session.message') {
|
||||
return shouldKeepOpenClawMessage(payload);
|
||||
}
|
||||
return Boolean(payload.text || payload.message || payload.content);
|
||||
}
|
||||
|
||||
function requestOpenClawSessionHistory(clientRef, sessionKey, limit = 30) {
|
||||
const client = clientRef?.current;
|
||||
if (!client || !sessionKey) {
|
||||
return false;
|
||||
}
|
||||
return client.send(JSON.stringify({
|
||||
type: 'get_openclaw_session_history',
|
||||
session_key: sessionKey,
|
||||
limit,
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeOpenClawAgents(agents, presence, sessionsPayload = null) {
|
||||
const normalizedAgents = Array.isArray(agents) ? agents : [];
|
||||
const presenceAgents = presence?.agents || presence || {};
|
||||
const sessionDefaults = sessionsPayload?.defaults || {};
|
||||
const sessions = Array.isArray(sessionsPayload?.sessions) ? sessionsPayload.sessions : [];
|
||||
|
||||
const sessionModelByAgent = new Map();
|
||||
sessions.forEach((session) => {
|
||||
if (!session || typeof session !== 'object') return;
|
||||
let agentId = String(session.agentId || session.agent_id || '').trim();
|
||||
if (!agentId) {
|
||||
const key = String(session.key || session.sessionKey || '').trim();
|
||||
const parts = key.split(':');
|
||||
if (parts.length >= 3 && parts[0] === 'agent') {
|
||||
agentId = parts[1];
|
||||
}
|
||||
}
|
||||
const modelValue =
|
||||
session.model ||
|
||||
session.modelName ||
|
||||
session.model_name ||
|
||||
session.resolvedModel ||
|
||||
session.resolved_model ||
|
||||
null;
|
||||
if (agentId && modelValue && !sessionModelByAgent.has(agentId)) {
|
||||
sessionModelByAgent.set(agentId, modelValue);
|
||||
}
|
||||
});
|
||||
|
||||
return normalizedAgents.map((agent) => {
|
||||
if (!agent || typeof agent !== 'object') {
|
||||
return agent;
|
||||
}
|
||||
|
||||
const agentId = String(agent.id || agent.agentId || '').trim();
|
||||
const presenceEntry = agentId ? presenceAgents?.[agentId] : null;
|
||||
const presenceSessions = Array.isArray(presenceEntry?.sessions) ? presenceEntry.sessions : [];
|
||||
const firstPresenceSession = presenceSessions.find((session) => {
|
||||
const value = session?.model || session?.modelName || session?.model_name || session?.resolvedModel;
|
||||
return typeof value === 'string' && value.trim();
|
||||
});
|
||||
|
||||
const model =
|
||||
agent.model ||
|
||||
agent.modelName ||
|
||||
agent.model_name ||
|
||||
agent.resolvedModel ||
|
||||
agent.resolved_model ||
|
||||
agent.defaultModel ||
|
||||
agent.default_model ||
|
||||
sessionModelByAgent.get(agentId) ||
|
||||
sessionDefaults.model ||
|
||||
sessionDefaults.modelName ||
|
||||
sessionDefaults.model_name ||
|
||||
firstPresenceSession?.model ||
|
||||
firstPresenceSession?.modelName ||
|
||||
firstPresenceSession?.model_name ||
|
||||
firstPresenceSession?.resolvedModel ||
|
||||
null;
|
||||
|
||||
return {
|
||||
...agent,
|
||||
model: typeof model === 'string' && model.trim() ? model.trim() : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for WebSocket connection lifecycle and event handling.
|
||||
* Manages clientRef, connection, and ALL event handlers.
|
||||
@@ -805,7 +1105,15 @@ export function useWebSocketConnection({
|
||||
useOpenClawStore.getState().setStatusLoading(false);
|
||||
},
|
||||
openclaw_sessions_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawSessions(e.data || e);
|
||||
const payload = e.data || e;
|
||||
useOpenClawStore.getState().setOpenclawSessions(payload);
|
||||
const currentAgents = useOpenClawStore.getState().agents || [];
|
||||
const presence = useOpenClawStore.getState().agentsPresence;
|
||||
if (currentAgents.length > 0) {
|
||||
useOpenClawStore.getState().setAgents(
|
||||
normalizeOpenClawAgents(currentAgents, presence, payload),
|
||||
);
|
||||
}
|
||||
useOpenClawStore.getState().setSessionsLoading(false);
|
||||
},
|
||||
openclaw_session_detail_loaded: (e) => {
|
||||
@@ -813,7 +1121,120 @@ export function useWebSocketConnection({
|
||||
useOpenClawStore.getState().setSessionDetailLoading(false);
|
||||
},
|
||||
openclaw_session_history_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawSessionHistory(e.data || e);
|
||||
const data = e.data || e;
|
||||
const sessionKey = e.session_key || data?.session_key || useOpenClawStore.getState().selectedSessionKey;
|
||||
useOpenClawStore.getState().setOpenclawSessionHistory(data);
|
||||
if (sessionKey) {
|
||||
useOpenClawStore.getState().replaceOpenclawChatHistory(
|
||||
sessionKey,
|
||||
normalizeOpenClawHistoryItems(data?.history || []),
|
||||
);
|
||||
}
|
||||
},
|
||||
openclaw_session_resolved: (e) => {
|
||||
const d = e.data || {};
|
||||
useOpenClawStore.getState().setOpenclawResolvedSessionKey(d.key || null);
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setChatError(d.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setChatError(null);
|
||||
}
|
||||
},
|
||||
openclaw_session_created: (e) => {
|
||||
const d = e.data || {};
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setChatError(d.error);
|
||||
return;
|
||||
}
|
||||
if (d?.entry || d?.key) {
|
||||
const createdKey = d?.key || d?.entry?.key || d?.entry?.sessionKey || '';
|
||||
useOpenClawStore.getState().appendOpenclawSession(
|
||||
d.entry || {
|
||||
key: createdKey,
|
||||
sessionKey: createdKey,
|
||||
agentId: String(createdKey).split(':')[1] || '',
|
||||
}
|
||||
);
|
||||
}
|
||||
if (d?.key) {
|
||||
useOpenClawStore.getState().setSelectedSessionKey(d.key);
|
||||
useOpenClawStore.getState().setOpenclawResolvedSessionKey(d.key);
|
||||
useOpenClawStore.getState().setChatError(null);
|
||||
}
|
||||
},
|
||||
openclaw_session_subscribed: (e) => {
|
||||
const sessionKey = e.session_key || e.data?.key || null;
|
||||
if (sessionKey) {
|
||||
useOpenClawStore.getState().setOpenclawSessionSubscribed(sessionKey, true);
|
||||
}
|
||||
if (e.data?.error) {
|
||||
useOpenClawStore.getState().setChatError(e.data.error);
|
||||
}
|
||||
},
|
||||
openclaw_session_unsubscribed: (e) => {
|
||||
const sessionKey = e.session_key || e.data?.key || null;
|
||||
if (sessionKey) {
|
||||
useOpenClawStore.getState().setOpenclawSessionSubscribed(sessionKey, false);
|
||||
}
|
||||
},
|
||||
openclaw_session_reset: (e) => {
|
||||
const sessionKey = e.session_key || e.data?.key || null;
|
||||
if (e.data?.error) {
|
||||
useOpenClawStore.getState().setChatError(e.data.error);
|
||||
return;
|
||||
}
|
||||
if (sessionKey) {
|
||||
useOpenClawStore.getState().replaceOpenclawChatHistory(sessionKey, []);
|
||||
useOpenClawStore.getState().setChatError(null);
|
||||
}
|
||||
},
|
||||
openclaw_session_deleted: (e) => {
|
||||
const sessionKey = e.session_key || e.data?.key || null;
|
||||
if (e.data?.error) {
|
||||
useOpenClawStore.getState().setChatError(e.data.error);
|
||||
return;
|
||||
}
|
||||
if (sessionKey) {
|
||||
useOpenClawStore.getState().removeOpenclawSession(sessionKey);
|
||||
useOpenClawStore.getState().setChatError(null);
|
||||
}
|
||||
},
|
||||
openclaw_message_sent: (e) => {
|
||||
const sessionKey = e.session_key || e.data?.key || useOpenClawStore.getState().selectedSessionKey;
|
||||
if (sessionKey) {
|
||||
useOpenClawStore.getState().setOpenclawChatSendingForSession(sessionKey, false);
|
||||
}
|
||||
if (e.data?.error) {
|
||||
useOpenClawStore.getState().setChatError(e.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setChatError(null);
|
||||
if (sessionKey && (e.data?.status || e.data?.runId || e.data?.messageSeq !== undefined)) {
|
||||
const statusBits = [
|
||||
e.data?.status ? `status=${e.data.status}` : null,
|
||||
e.data?.runId ? `runId=${e.data.runId}` : null,
|
||||
e.data?.messageSeq !== undefined ? `seq=${e.data.messageSeq}` : null,
|
||||
].filter(Boolean);
|
||||
useOpenClawStore.getState().appendOpenclawChatMessage(sessionKey, {
|
||||
id: `send-meta:${e.data?.runId || Date.now()}`,
|
||||
role: 'system',
|
||||
text: `消息已提交到 OpenClaw${statusBits.length ? ` (${statusBits.join(', ')})` : ''}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
if (sessionKey) {
|
||||
window.setTimeout(() => requestOpenClawSessionHistory(clientRef, sessionKey, 30), 600);
|
||||
}
|
||||
}
|
||||
},
|
||||
openclaw_session_event: (e) => {
|
||||
const sessionKey = e.session_key || e.payload?.sessionKey || e.payload?.key;
|
||||
if (!sessionKey || !shouldAppendOpenClawLiveEvent(e)) {
|
||||
return;
|
||||
}
|
||||
useOpenClawStore.getState().appendOpenclawChatMessage(
|
||||
sessionKey,
|
||||
normalizeOpenClawLiveEvent(e),
|
||||
);
|
||||
},
|
||||
openclaw_cron_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawCronJobs(e.data || e);
|
||||
@@ -829,12 +1250,30 @@ export function useWebSocketConnection({
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setAgentsError(d.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setAgents(d?.agents || []);
|
||||
const presence = useOpenClawStore.getState().agentsPresence;
|
||||
const sessionsPayload = {
|
||||
sessions: useOpenClawStore.getState().openclawSessions || [],
|
||||
defaults: useOpenClawStore.getState().openclawSessionsDefaults || null,
|
||||
};
|
||||
useOpenClawStore.getState().setAgents(
|
||||
normalizeOpenClawAgents(d?.agents || [], presence, sessionsPayload),
|
||||
);
|
||||
useOpenClawStore.getState().setAgentsError(null);
|
||||
}
|
||||
},
|
||||
openclaw_agents_presence_loaded: (e) => {
|
||||
useOpenClawStore.getState().setAgentsPresence((e.data?.data ?? e.data) || {});
|
||||
const presencePayload = (e.data?.data ?? e.data) || {};
|
||||
useOpenClawStore.getState().setAgentsPresence(presencePayload);
|
||||
const currentAgents = useOpenClawStore.getState().agents || [];
|
||||
if (currentAgents.length > 0) {
|
||||
const sessionsPayload = {
|
||||
sessions: useOpenClawStore.getState().openclawSessions || [],
|
||||
defaults: useOpenClawStore.getState().openclawSessionsDefaults || null,
|
||||
};
|
||||
useOpenClawStore.getState().setAgents(
|
||||
normalizeOpenClawAgents(currentAgents, presencePayload, sessionsPayload),
|
||||
);
|
||||
}
|
||||
},
|
||||
openclaw_skills_loaded: (e) => {
|
||||
useOpenClawStore.getState().setSkillsLoading(false);
|
||||
|
||||
@@ -7,10 +7,16 @@ export const useOpenClawStore = create(
|
||||
// Raw data
|
||||
openclawStatus: null,
|
||||
openclawSessions: [],
|
||||
openclawSessionsDefaults: null,
|
||||
openclawSessionDetail: null,
|
||||
openclawSessionHistory: [],
|
||||
openclawCronJobs: [],
|
||||
openclawApprovals: [],
|
||||
openclawResolvedSessionKey: null,
|
||||
openclawChatMessagesBySession: {},
|
||||
openclawChatDraftBySession: {},
|
||||
openclawChatSendingBySession: {},
|
||||
openclawSessionSubscriptions: {},
|
||||
|
||||
// Loading states
|
||||
isStatusLoading: false,
|
||||
@@ -18,6 +24,7 @@ export const useOpenClawStore = create(
|
||||
isSessionDetailLoading: false,
|
||||
isCronLoading: false,
|
||||
isApprovalsLoading: false,
|
||||
isChatSending: false,
|
||||
|
||||
// Error states
|
||||
statusError: null,
|
||||
@@ -25,6 +32,7 @@ export const useOpenClawStore = create(
|
||||
sessionDetailError: null,
|
||||
cronError: null,
|
||||
approvalsError: null,
|
||||
chatError: null,
|
||||
|
||||
// Agents state
|
||||
agents: [],
|
||||
@@ -119,11 +127,129 @@ export const useOpenClawStore = create(
|
||||
|
||||
// Setters
|
||||
setOpenclawStatus: (data) => set({ openclawStatus: data, statusError: null }),
|
||||
setOpenclawSessions: (data) => set({ openclawSessions: data?.sessions || [], sessionsError: null }),
|
||||
setOpenclawSessionDetail: (data) => set({ openclawSessionDetail: data?.session || null, sessionDetailError: null }),
|
||||
setOpenclawSessionHistory: (data) => set({ openclawSessionHistory: data?.history || [], sessionDetailError: null }),
|
||||
setOpenclawSessions: (data) => set({
|
||||
openclawSessions: data?.sessions || [],
|
||||
openclawSessionsDefaults: data?.defaults || null,
|
||||
sessionsError: null,
|
||||
}),
|
||||
appendOpenclawSession: (session) => set((state) => {
|
||||
const key = session?.key || session?.sessionKey;
|
||||
if (!key) {
|
||||
return {};
|
||||
}
|
||||
const existing = state.openclawSessions || [];
|
||||
const deduped = existing.filter((item) => (item?.key || item?.sessionKey) !== key);
|
||||
return { openclawSessions: [session, ...deduped] };
|
||||
}),
|
||||
removeOpenclawSession: (sessionKey) => set((state) => ({
|
||||
openclawSessions: (state.openclawSessions || []).filter(
|
||||
(item) => (item?.key || item?.sessionKey) !== sessionKey
|
||||
),
|
||||
selectedSessionKey:
|
||||
state.selectedSessionKey === sessionKey ? null : state.selectedSessionKey,
|
||||
})),
|
||||
setOpenclawSessionDetail: (data) => set({ openclawSessionDetail: data?.session || null, sessionDetailError: data?.error || null }),
|
||||
setOpenclawSessionHistory: (data) => set({ openclawSessionHistory: data?.history || [], sessionDetailError: data?.error || null }),
|
||||
setOpenclawCronJobs: (data) => set({ openclawCronJobs: data?.cron || [], cronError: null }),
|
||||
setOpenclawApprovals: (data) => set({ openclawApprovals: data?.approvals || [], approvalsError: null }),
|
||||
setOpenclawResolvedSessionKey: (key) => set({ openclawResolvedSessionKey: key || null }),
|
||||
setOpenclawChatDraft: (sessionKey, value) => set((state) => ({
|
||||
openclawChatDraftBySession: { ...state.openclawChatDraftBySession, [sessionKey]: value },
|
||||
})),
|
||||
appendOpenclawChatMessage: (sessionKey, message) => set((state) => {
|
||||
const current = state.openclawChatMessagesBySession[sessionKey] || [];
|
||||
const sameMessageIndex = current.findIndex((item) => {
|
||||
const sameId = Boolean(message?.id && item?.id && message.id === item.id);
|
||||
const sameMessageId = Boolean(
|
||||
message?.messageId &&
|
||||
item?.messageId &&
|
||||
message.messageId === item.messageId
|
||||
);
|
||||
const sameSeq = Boolean(
|
||||
message?.seq !== undefined &&
|
||||
message?.seq !== null &&
|
||||
item?.seq !== undefined &&
|
||||
item?.seq !== null &&
|
||||
message.seq === item.seq &&
|
||||
message?.role === item?.role
|
||||
);
|
||||
const incomingText = String(message?.text || '').trim();
|
||||
const existingText = String(item?.text || '').trim();
|
||||
const incomingTs = Date.parse(message?.timestamp || '');
|
||||
const existingTs = Date.parse(item?.timestamp || '');
|
||||
const nearInTime =
|
||||
Number.isFinite(incomingTs) &&
|
||||
Number.isFinite(existingTs) &&
|
||||
Math.abs(incomingTs - existingTs) < 1500;
|
||||
const sameAssistantText =
|
||||
message?.role === 'assistant' &&
|
||||
item?.role === 'assistant' &&
|
||||
incomingText &&
|
||||
existingText &&
|
||||
(
|
||||
incomingText === existingText ||
|
||||
incomingText.startsWith(existingText) ||
|
||||
existingText.startsWith(incomingText)
|
||||
) &&
|
||||
nearInTime;
|
||||
return sameId || sameMessageId || sameSeq || sameAssistantText;
|
||||
});
|
||||
|
||||
if (sameMessageIndex >= 0) {
|
||||
const next = [...current];
|
||||
next[sameMessageIndex] = { ...next[sameMessageIndex], ...message };
|
||||
return {
|
||||
openclawChatMessagesBySession: {
|
||||
...state.openclawChatMessagesBySession,
|
||||
[sessionKey]: next,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
openclawChatMessagesBySession: {
|
||||
...state.openclawChatMessagesBySession,
|
||||
[sessionKey]: [...current, message],
|
||||
},
|
||||
};
|
||||
}),
|
||||
replaceOpenclawChatHistory: (sessionKey, messages) => set((state) => {
|
||||
const incoming = Array.isArray(messages) ? messages : [];
|
||||
const existing = state.openclawChatMessagesBySession[sessionKey] || [];
|
||||
const merged = [];
|
||||
const seen = new Set();
|
||||
|
||||
const signatureFor = (message) => {
|
||||
if (!message) return "";
|
||||
if (message.id) return `id:${message.id}`;
|
||||
if (message.messageId) return `mid:${message.messageId}`;
|
||||
if (message.seq !== undefined && message.seq !== null) return `seq:${message.seq}`;
|
||||
return `txt:${message.role || ""}:${String(message.text || "").trim()}`;
|
||||
};
|
||||
|
||||
for (const message of [...incoming, ...existing]) {
|
||||
const signature = signatureFor(message);
|
||||
if (!signature || seen.has(signature)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(signature);
|
||||
merged.push(message);
|
||||
}
|
||||
|
||||
return {
|
||||
openclawChatMessagesBySession: {
|
||||
...state.openclawChatMessagesBySession,
|
||||
[sessionKey]: merged,
|
||||
},
|
||||
};
|
||||
}),
|
||||
setOpenclawChatSendingForSession: (sessionKey, value) => set((state) => ({
|
||||
openclawChatSendingBySession: { ...state.openclawChatSendingBySession, [sessionKey]: Boolean(value) },
|
||||
isChatSending: Boolean(value),
|
||||
})),
|
||||
setOpenclawSessionSubscribed: (sessionKey, value) => set((state) => ({
|
||||
openclawSessionSubscriptions: { ...state.openclawSessionSubscriptions, [sessionKey]: Boolean(value) },
|
||||
})),
|
||||
|
||||
setSelectedSessionKey: (key) => set({ selectedSessionKey: key }),
|
||||
|
||||
@@ -138,6 +264,7 @@ export const useOpenClawStore = create(
|
||||
setSessionDetailError: (e) => set({ sessionDetailError: e }),
|
||||
setCronError: (e) => set({ cronError: e }),
|
||||
setApprovalsError: (e) => set({ approvalsError: e }),
|
||||
setChatError: (e) => set({ chatError: e }),
|
||||
|
||||
setAgents: (agents) => set({ agents }),
|
||||
setAgentsLoading: (loading) => set({ agentsLoading: loading }),
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"trading_days_completed": 0,
|
||||
"server_mode": "live",
|
||||
"is_backtest": false,
|
||||
"is_mock_mode": false,
|
||||
"data_sources": {
|
||||
"preferred": [
|
||||
"yfinance",
|
||||
|
||||
@@ -517,11 +517,13 @@ class OpenClawWebSocketClient:
|
||||
params["agentId"] = agent_id
|
||||
if label:
|
||||
params["label"] = label
|
||||
if channel:
|
||||
params["channel"] = channel
|
||||
|
||||
result = await self._send_request("sessions.resolve", params)
|
||||
sessions = result.get("sessions", [])
|
||||
if sessions:
|
||||
return sessions[0].get("key")
|
||||
key = result.get("key")
|
||||
if isinstance(key, str) and key.strip():
|
||||
return key.strip()
|
||||
return None
|
||||
|
||||
async def send_message(
|
||||
@@ -549,12 +551,30 @@ class OpenClawWebSocketClient:
|
||||
if thinking:
|
||||
params["thinking"] = thinking
|
||||
|
||||
# Use shorter timeout for send since it waits for agent response
|
||||
result = await self._send_request(
|
||||
"sessions.send",
|
||||
params,
|
||||
)
|
||||
return result
|
||||
previous_timeout_ms = self.timeout_ms
|
||||
if timeout_ms is not None:
|
||||
self.timeout_ms = timeout_ms
|
||||
try:
|
||||
return await self._send_request("sessions.send", params)
|
||||
finally:
|
||||
self.timeout_ms = previous_timeout_ms
|
||||
|
||||
async def unsubscribe(self, session_key: str) -> dict[str, Any]:
|
||||
"""Unsubscribe from messages for a session."""
|
||||
return await self._send_request("sessions.messages.unsubscribe", {"key": session_key})
|
||||
|
||||
async def get_session_history(
|
||||
self,
|
||||
session_key: str,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
"""Best-effort session history read.
|
||||
|
||||
OpenClaw's public Gateway surface is subscription-first for live message flow.
|
||||
History is not consistently exposed over the same WS methods across builds, so
|
||||
callers should still keep a CLI or REST fallback available.
|
||||
"""
|
||||
return await self._send_request("sessions.preview", {"keys": [session_key], "limit": limit})
|
||||
|
||||
async def subscribe(self, session_key: str) -> AsyncMessageIterator:
|
||||
"""Subscribe to messages from a session.
|
||||
@@ -670,6 +690,11 @@ class AsyncMessageIterator:
|
||||
async def __anext__(self) -> MessageEvent:
|
||||
return await self._queue.get()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if self._handler_added:
|
||||
self._client.remove_event_handler(self._on_event)
|
||||
self._handler_added = False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Synchronous convenience functions
|
||||
|
||||
Reference in New Issue
Block a user