feat: update openclaw workspace integration
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,7 +51,6 @@ node_modules
|
|||||||
outputs/
|
outputs/
|
||||||
/production/
|
/production/
|
||||||
/smoke_test/
|
/smoke_test/
|
||||||
/smoke_live_mock/
|
|
||||||
|
|
||||||
# Local tooling state
|
# Local tooling state
|
||||||
.omc/
|
.omc/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-03-26T17:14:45.135Z",
|
"timestamp": "2026-03-27T04:53:52.906Z",
|
||||||
"backgroundTasks": [],
|
"backgroundTasks": [],
|
||||||
"sessionStartTimestamp": "2026-03-26T17:13:16.686Z",
|
"sessionStartTimestamp": "2026-03-27T04:53:21.944Z",
|
||||||
"sessionId": "83f172c1-eb0f-4418-87a5-b9d4b6ce5b61"
|
"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 # 回测模式
|
||||||
evotraders backtest --start 2025-11-01 --end 2025-12-01 --enable-memory # 带记忆回测
|
evotraders backtest --start 2025-11-01 --end 2025-12-01 --enable-memory # 带记忆回测
|
||||||
evotraders live # 实盘交易
|
evotraders live # 实盘交易
|
||||||
evotraders live --mock # 模拟/测试模式
|
|
||||||
evotraders live -t 22:30 # 定时每日交易
|
evotraders live -t 22:30 # 定时每日交易
|
||||||
evotraders frontend # 启动可视化界面
|
evotraders frontend # 启动可视化界面
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ evotraders frontend # 启动可视化界面
|
|||||||
./start-dev.sh # 启动全部 4 个微服务 (agent, runtime, trading, news)
|
./start-dev.sh # 启动全部 4 个微服务 (agent, runtime, trading, news)
|
||||||
|
|
||||||
# Gateway WebSocket 服务器
|
# 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
|
python -m uvicorn backend.apps.runtime_service:app --host 0.0.0.0 --port 8003 --reload
|
||||||
@@ -189,7 +188,6 @@ backend/
|
|||||||
│ ├── schema.py # 数据 schema
|
│ ├── schema.py # 数据 schema
|
||||||
│ ├── historical_price_manager.py # 历史价格管理
|
│ ├── historical_price_manager.py # 历史价格管理
|
||||||
│ ├── polling_price_manager.py # 轮询价格管理
|
│ ├── polling_price_manager.py # 轮询价格管理
|
||||||
│ ├── mock_price_manager.py # Mock 价格管理
|
|
||||||
│ ├── news_alignment.py # 新闻对齐
|
│ ├── news_alignment.py # 新闻对齐
|
||||||
│ ├── polygon_client.py # Polygon.io 客户端
|
│ ├── polygon_client.py # Polygon.io 客户端
|
||||||
│ └── ret_data_updater.py # 离线数据更新
|
│ └── ret_data_updater.py # 离线数据更新
|
||||||
|
|||||||
@@ -499,11 +499,29 @@ class Gateway:
|
|||||||
await gateway_openclaw_handlers.handle_get_openclaw_workspace_files(self, websocket, data)
|
await gateway_openclaw_handlers.handle_get_openclaw_workspace_files(self, websocket, data)
|
||||||
elif msg_type == "get_openclaw_workspace_file":
|
elif msg_type == "get_openclaw_workspace_file":
|
||||||
await gateway_openclaw_handlers.handle_get_openclaw_workspace_file(self, websocket, data)
|
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:
|
except websockets.ConnectionClosed:
|
||||||
pass
|
pass
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
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(
|
async def _handle_get_stock_history(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -13,6 +13,63 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
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":
|
def _get_ws_client(gateway) -> "OpenClawWebSocketClient":
|
||||||
"""Get the OpenClaw WebSocket client from gateway."""
|
"""Get the OpenClaw WebSocket client from gateway."""
|
||||||
from shared.client.openclaw_websocket_client import OpenClawWebSocketClient
|
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:
|
async def handle_get_openclaw_session_detail(gateway, websocket, data: dict) -> None:
|
||||||
session_key = data.get("session_key", "")
|
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({
|
await websocket.send(json.dumps({
|
||||||
"type": "openclaw_session_detail_loaded",
|
"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,
|
"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:
|
async def handle_get_openclaw_session_history(gateway, websocket, data: dict) -> None:
|
||||||
session_key = data.get("session_key", "")
|
session_key = data.get("session_key", "")
|
||||||
limit = data.get("limit", 20)
|
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({
|
await websocket.send(json.dumps({
|
||||||
"type": "openclaw_session_history_loaded",
|
"type": "openclaw_session_history_loaded",
|
||||||
"data": result,
|
"data": payload,
|
||||||
"session_key": session_key,
|
"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:
|
async def handle_get_openclaw_cron(gateway, websocket, data: dict) -> None:
|
||||||
result = await _ws_call(gateway, "cron.list")
|
result = await _ws_call(gateway, "cron.list")
|
||||||
await websocket.send(json.dumps({"type": "openclaw_cron_loaded", "data": result}))
|
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:
|
async def handle_get_openclaw_agents(gateway, websocket, data: dict) -> None:
|
||||||
result = await _ws_call(gateway, "agents.list")
|
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}))
|
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:
|
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}))
|
await websocket.send(json.dumps({"type": "openclaw_skills_loaded", "data": result}))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class StorageService:
|
|||||||
self,
|
self,
|
||||||
dashboard_dir: Path,
|
dashboard_dir: Path,
|
||||||
initial_cash: float = 100000.0,
|
initial_cash: float = 100000.0,
|
||||||
config_name: str = "mock",
|
config_name: str = "live",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize storage service
|
Initialize storage service
|
||||||
|
|||||||
@@ -311,6 +311,17 @@ class TestRiskAgent:
|
|||||||
|
|
||||||
|
|
||||||
class TestStorageService:
|
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):
|
def test_calculate_portfolio_value_cash_only(self):
|
||||||
from backend.services.storage import StorageService
|
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,
|
"trading_days_completed": 5,
|
||||||
"server_mode": "backtest",
|
"server_mode": "backtest",
|
||||||
"is_backtest": true,
|
"is_backtest": true,
|
||||||
"is_mock_mode": false,
|
|
||||||
"last_saved": "2026-03-12T23:07:31.098122"
|
"last_saved": "2026-03-12T23:07:31.098122"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
"preview:host": "vite preview --host"
|
"preview:host": "vite preview --host"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
|||||||
@@ -234,6 +234,26 @@ export default function LiveTradingApp() {
|
|||||||
workspaceFilesByAgent,
|
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(() => {
|
useEffect(() => {
|
||||||
const symbols = runtimeControls.displayTickers
|
const symbols = runtimeControls.displayTickers
|
||||||
.map((ticker) => ticker.symbol)
|
.map((ticker) => ticker.symbol)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ASSETS } from '../config/constants';
|
import { ASSETS } from '../config/constants';
|
||||||
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
|
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
|
||||||
|
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get rank medal/trophy
|
* Get rank medal/trophy
|
||||||
@@ -207,14 +208,18 @@ export default function AgentCard({ agent, onClose, isClosing }) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginBottom: 4
|
marginBottom: 4
|
||||||
}}>
|
}}>
|
||||||
{modelInfo.logoPath ? (
|
{agent.modelName || modelInfo.logoPath ? (
|
||||||
<img
|
<LobeModelLogo
|
||||||
src={modelInfo.logoPath}
|
model={agent.modelName}
|
||||||
|
provider={agent.modelProvider}
|
||||||
|
fallbackSrc={modelInfo.logoPath}
|
||||||
alt={modelInfo.provider}
|
alt={modelInfo.provider}
|
||||||
|
size={36}
|
||||||
|
type="color"
|
||||||
|
shape="square"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
objectFit: 'contain'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { formatTime } from '../utils/formatters';
|
|||||||
import { MESSAGE_COLORS, getAgentColors, AGENTS, ASSETS } from '../config/constants';
|
import { MESSAGE_COLORS, getAgentColors, AGENTS, ASSETS } from '../config/constants';
|
||||||
import { getModelIcon } from '../utils/modelIcons';
|
import { getModelIcon } from '../utils/modelIcons';
|
||||||
import MarkdownModal from './MarkdownModal';
|
import MarkdownModal from './MarkdownModal';
|
||||||
|
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||||
|
|
||||||
const isAnalyst = (agentId, agentName) => {
|
const isAnalyst = (agentId, agentName) => {
|
||||||
if (agentId && agentId.includes('analyst')) return true;
|
if (agentId && agentId.includes('analyst')) return true;
|
||||||
@@ -167,11 +168,11 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
|||||||
// Get current selection display info
|
// Get current selection display info
|
||||||
const getCurrentSelectionInfo = () => {
|
const getCurrentSelectionInfo = () => {
|
||||||
if (selectedAgent === 'all') {
|
if (selectedAgent === 'all') {
|
||||||
return { label: '全部角色', modelInfo: null };
|
return { label: '全部角色', modelInfo: null, agentInfo: null };
|
||||||
}
|
}
|
||||||
const agentInfo = getAgentInfoByName(selectedAgent);
|
const agentInfo = getAgentInfoByName(selectedAgent);
|
||||||
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
|
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
|
||||||
return { label: selectedAgent, modelInfo };
|
return { label: selectedAgent, modelInfo, agentInfo };
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentSelection = getCurrentSelectionInfo();
|
const currentSelection = getCurrentSelectionInfo();
|
||||||
@@ -189,11 +190,16 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
|||||||
onBlur={() => setTimeout(() => setDropdownOpen(false), 200)}
|
onBlur={() => setTimeout(() => setDropdownOpen(false), 200)}
|
||||||
>
|
>
|
||||||
<div className="custom-select-value">
|
<div className="custom-select-value">
|
||||||
{currentSelection.modelInfo?.logoPath && (
|
{(currentSelection.agentInfo?.modelName || currentSelection.modelInfo?.logoPath) && (
|
||||||
<img
|
<LobeModelLogo
|
||||||
src={currentSelection.modelInfo.logoPath}
|
model={currentSelection.agentInfo?.modelName}
|
||||||
alt={currentSelection.modelInfo.provider}
|
provider={currentSelection.agentInfo?.modelProvider}
|
||||||
|
fallbackSrc={currentSelection.modelInfo?.logoPath}
|
||||||
|
alt={currentSelection.modelInfo?.provider}
|
||||||
|
size={18}
|
||||||
className="select-model-icon"
|
className="select-model-icon"
|
||||||
|
shape="square"
|
||||||
|
type="color"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span>{currentSelection.label}</span>
|
<span>{currentSelection.label}</span>
|
||||||
@@ -223,11 +229,16 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
|||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{modelInfo?.logoPath && (
|
{(agentInfo?.modelName || modelInfo?.logoPath) && (
|
||||||
<img
|
<LobeModelLogo
|
||||||
src={modelInfo.logoPath}
|
model={agentInfo?.modelName}
|
||||||
alt={modelInfo.provider}
|
provider={agentInfo?.modelProvider}
|
||||||
|
fallbackSrc={modelInfo?.logoPath}
|
||||||
|
alt={modelInfo?.provider}
|
||||||
|
size={18}
|
||||||
className="select-model-icon"
|
className="select-model-icon"
|
||||||
|
shape="square"
|
||||||
|
type="color"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span>{agent}</span>
|
<span>{agent}</span>
|
||||||
@@ -363,16 +374,16 @@ function ConferenceMessage({ message, getAgentModelInfo }) {
|
|||||||
return (
|
return (
|
||||||
<div className="conf-message-item">
|
<div className="conf-message-item">
|
||||||
<div className="conf-agent-name" style={{ color: agentColors.text, display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
|
<div className="conf-agent-name" style={{ color: agentColors.text, display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
|
||||||
{modelInfo.logoPath && (
|
{(agentModelData.modelName || modelInfo.logoPath) && (
|
||||||
<img
|
<LobeModelLogo
|
||||||
src={modelInfo.logoPath}
|
model={agentModelData.modelName}
|
||||||
|
provider={agentModelData.modelProvider}
|
||||||
|
fallbackSrc={modelInfo.logoPath}
|
||||||
alt={modelInfo.provider}
|
alt={modelInfo.provider}
|
||||||
style={{
|
size={20}
|
||||||
width: '20px',
|
shape="circle"
|
||||||
height: '20px',
|
type="color"
|
||||||
borderRadius: '50%',
|
style={{ borderRadius: '50%' }}
|
||||||
objectFit: 'contain'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{message.agent}
|
{message.agent}
|
||||||
@@ -591,16 +602,16 @@ function MessageItem({ message, itemId, isHighlighted, getAgentModelInfo }) {
|
|||||||
>
|
>
|
||||||
<div className="feed-item-header">
|
<div className="feed-item-header">
|
||||||
<span className="feed-item-title" style={{ color: colors.text, display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
|
<span className="feed-item-title" style={{ color: colors.text, display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
|
||||||
{modelInfo.logoPath && message.agent !== 'Memory' && (
|
{message.agent !== 'Memory' && (agentModelData.modelName || modelInfo.logoPath) && (
|
||||||
<img
|
<LobeModelLogo
|
||||||
src={modelInfo.logoPath}
|
model={agentModelData.modelName}
|
||||||
|
provider={agentModelData.modelProvider}
|
||||||
|
fallbackSrc={modelInfo.logoPath}
|
||||||
alt={modelInfo.provider}
|
alt={modelInfo.provider}
|
||||||
style={{
|
size={20}
|
||||||
width: '20px',
|
shape="circle"
|
||||||
height: '20px',
|
type="color"
|
||||||
borderRadius: '50%',
|
style={{ borderRadius: '50%' }}
|
||||||
objectFit: 'contain'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{title}
|
{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';
|
import { OpenClawStatus } from './OpenClawStatus';
|
||||||
|
|
||||||
export default function OpenClawView() {
|
export default function OpenClawView() {
|
||||||
return (
|
return <OpenClawStatus />;
|
||||||
<div style={{
|
|
||||||
height: '100%',
|
|
||||||
overflow: 'auto',
|
|
||||||
padding: '16px',
|
|
||||||
background: '#F3F4F6',
|
|
||||||
}}>
|
|
||||||
<OpenClawStatus />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'
|
|||||||
import { ASSETS, SCENE_NATIVE, AGENT_SEATS, AGENTS } from '../config/constants';
|
import { ASSETS, SCENE_NATIVE, AGENT_SEATS, AGENTS } from '../config/constants';
|
||||||
import AgentCard from './AgentCard';
|
import AgentCard from './AgentCard';
|
||||||
import { getModelIcon } from '../utils/modelIcons';
|
import { getModelIcon } from '../utils/modelIcons';
|
||||||
|
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook to load an image
|
* Custom hook to load an image
|
||||||
@@ -518,21 +519,23 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
|||||||
{medal}
|
{medal}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{modelInfo.logoPath && (
|
{(agentData?.modelName || modelInfo.logoPath) && (
|
||||||
<img
|
<LobeModelLogo
|
||||||
src={modelInfo.logoPath}
|
model={agentData?.modelName}
|
||||||
|
provider={agentData?.modelProvider}
|
||||||
|
fallbackSrc={modelInfo.logoPath}
|
||||||
alt={modelInfo.provider}
|
alt={modelInfo.provider}
|
||||||
|
size={25}
|
||||||
|
shape="circle"
|
||||||
|
type="color"
|
||||||
className="agent-model-badge"
|
className="agent-model-badge"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: -12,
|
top: -12,
|
||||||
right: -12,
|
right: -12,
|
||||||
width: 25,
|
|
||||||
height: 25,
|
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
border: '2px solid #ffffff',
|
border: '2px solid #ffffff',
|
||||||
background: '#ffffff',
|
background: '#ffffff',
|
||||||
objectFit: 'contain',
|
|
||||||
padding: 2,
|
padding: 2,
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||||
pointerEvents: 'none'
|
pointerEvents: 'none'
|
||||||
@@ -642,10 +645,15 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
|||||||
|
|
||||||
{/* Agent header with model icon */}
|
{/* Agent header with model icon */}
|
||||||
<div className="room-bubble-header">
|
<div className="room-bubble-header">
|
||||||
{modelInfo.logoPath && (
|
{(agentData?.modelName || modelInfo.logoPath) && (
|
||||||
<img
|
<LobeModelLogo
|
||||||
src={modelInfo.logoPath}
|
model={agentData?.modelName}
|
||||||
|
provider={agentData?.modelProvider}
|
||||||
|
fallbackSrc={modelInfo.logoPath}
|
||||||
alt={modelInfo.provider}
|
alt={modelInfo.provider}
|
||||||
|
size={18}
|
||||||
|
shape="circle"
|
||||||
|
type="color"
|
||||||
className="bubble-model-icon"
|
className="bubble-model-icon"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
|
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
|
||||||
|
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||||
|
|
||||||
export default function TraderView({
|
export default function TraderView({
|
||||||
agents,
|
agents,
|
||||||
@@ -249,13 +250,16 @@ export default function TraderView({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 10
|
gap: 10
|
||||||
}}>
|
}}>
|
||||||
{modelInfo.logoPath && (
|
<LobeModelLogo
|
||||||
<img
|
model={profile.model_name}
|
||||||
src={modelInfo.logoPath}
|
provider={profile.model_provider}
|
||||||
alt={modelInfo.provider}
|
fallbackSrc={modelInfo.logoPath}
|
||||||
style={{ width: 26, height: 26, borderRadius: 999 }}
|
alt={modelInfo.provider}
|
||||||
/>
|
size={26}
|
||||||
)}
|
shape="circle"
|
||||||
|
type="color"
|
||||||
|
style={{ borderRadius: 999 }}
|
||||||
|
/>
|
||||||
<div style={{ display: 'grid', gap: 2 }}>
|
<div style={{ display: 'grid', gap: 2 }}>
|
||||||
<div style={{ fontSize: 11, color: '#4B5563', fontWeight: 700 }}>模型</div>
|
<div style={{ fontSize: 11, color: '#4B5563', fontWeight: 700 }}>模型</div>
|
||||||
<div style={{ fontSize: 12, color: '#111111', fontWeight: 800 }}>
|
<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 requestCron = useCallback(() => {
|
||||||
const store = getStore();
|
const store = getStore();
|
||||||
const client = store.clientRef?.current;
|
const client = store.clientRef?.current;
|
||||||
@@ -91,13 +177,13 @@ export function useOpenClawPanel() {
|
|||||||
sendWithRetry({ current: client }, { type: "get_openclaw_agents_presence" });
|
sendWithRetry({ current: client }, { type: "get_openclaw_agents_presence" });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const requestSkills = useCallback(() => {
|
const requestSkills = useCallback((agentId = null) => {
|
||||||
const store = getStore();
|
const store = getStore();
|
||||||
const client = store.clientRef?.current;
|
const client = store.clientRef?.current;
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
store.setSkillsLoading(true);
|
store.setSkillsLoading(true);
|
||||||
store.setSkillsError(null);
|
store.setSkillsError(null);
|
||||||
sendWithRetry({ current: client }, { type: "get_openclaw_skills" });
|
sendWithRetry({ current: client }, { type: "get_openclaw_skills", agent_id: agentId });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const requestModels = useCallback(() => {
|
const requestModels = useCallback(() => {
|
||||||
@@ -239,6 +325,13 @@ export function useOpenClawPanel() {
|
|||||||
requestSessions,
|
requestSessions,
|
||||||
requestSessionDetail,
|
requestSessionDetail,
|
||||||
requestSessionHistory,
|
requestSessionHistory,
|
||||||
|
resolveSession,
|
||||||
|
createSession,
|
||||||
|
subscribeSession,
|
||||||
|
unsubscribeSession,
|
||||||
|
resetSession,
|
||||||
|
deleteSession,
|
||||||
|
sendSessionMessage,
|
||||||
requestCron,
|
requestCron,
|
||||||
requestApprovals,
|
requestApprovals,
|
||||||
requestAgents,
|
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.
|
* Custom hook for WebSocket connection lifecycle and event handling.
|
||||||
* Manages clientRef, connection, and ALL event handlers.
|
* Manages clientRef, connection, and ALL event handlers.
|
||||||
@@ -805,7 +1105,15 @@ export function useWebSocketConnection({
|
|||||||
useOpenClawStore.getState().setStatusLoading(false);
|
useOpenClawStore.getState().setStatusLoading(false);
|
||||||
},
|
},
|
||||||
openclaw_sessions_loaded: (e) => {
|
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);
|
useOpenClawStore.getState().setSessionsLoading(false);
|
||||||
},
|
},
|
||||||
openclaw_session_detail_loaded: (e) => {
|
openclaw_session_detail_loaded: (e) => {
|
||||||
@@ -813,7 +1121,120 @@ export function useWebSocketConnection({
|
|||||||
useOpenClawStore.getState().setSessionDetailLoading(false);
|
useOpenClawStore.getState().setSessionDetailLoading(false);
|
||||||
},
|
},
|
||||||
openclaw_session_history_loaded: (e) => {
|
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) => {
|
openclaw_cron_loaded: (e) => {
|
||||||
useOpenClawStore.getState().setOpenclawCronJobs(e.data || e);
|
useOpenClawStore.getState().setOpenclawCronJobs(e.data || e);
|
||||||
@@ -829,12 +1250,30 @@ export function useWebSocketConnection({
|
|||||||
if (d?.error) {
|
if (d?.error) {
|
||||||
useOpenClawStore.getState().setAgentsError(d.error);
|
useOpenClawStore.getState().setAgentsError(d.error);
|
||||||
} else {
|
} 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);
|
useOpenClawStore.getState().setAgentsError(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openclaw_agents_presence_loaded: (e) => {
|
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) => {
|
openclaw_skills_loaded: (e) => {
|
||||||
useOpenClawStore.getState().setSkillsLoading(false);
|
useOpenClawStore.getState().setSkillsLoading(false);
|
||||||
|
|||||||
@@ -7,10 +7,16 @@ export const useOpenClawStore = create(
|
|||||||
// Raw data
|
// Raw data
|
||||||
openclawStatus: null,
|
openclawStatus: null,
|
||||||
openclawSessions: [],
|
openclawSessions: [],
|
||||||
|
openclawSessionsDefaults: null,
|
||||||
openclawSessionDetail: null,
|
openclawSessionDetail: null,
|
||||||
openclawSessionHistory: [],
|
openclawSessionHistory: [],
|
||||||
openclawCronJobs: [],
|
openclawCronJobs: [],
|
||||||
openclawApprovals: [],
|
openclawApprovals: [],
|
||||||
|
openclawResolvedSessionKey: null,
|
||||||
|
openclawChatMessagesBySession: {},
|
||||||
|
openclawChatDraftBySession: {},
|
||||||
|
openclawChatSendingBySession: {},
|
||||||
|
openclawSessionSubscriptions: {},
|
||||||
|
|
||||||
// Loading states
|
// Loading states
|
||||||
isStatusLoading: false,
|
isStatusLoading: false,
|
||||||
@@ -18,6 +24,7 @@ export const useOpenClawStore = create(
|
|||||||
isSessionDetailLoading: false,
|
isSessionDetailLoading: false,
|
||||||
isCronLoading: false,
|
isCronLoading: false,
|
||||||
isApprovalsLoading: false,
|
isApprovalsLoading: false,
|
||||||
|
isChatSending: false,
|
||||||
|
|
||||||
// Error states
|
// Error states
|
||||||
statusError: null,
|
statusError: null,
|
||||||
@@ -25,6 +32,7 @@ export const useOpenClawStore = create(
|
|||||||
sessionDetailError: null,
|
sessionDetailError: null,
|
||||||
cronError: null,
|
cronError: null,
|
||||||
approvalsError: null,
|
approvalsError: null,
|
||||||
|
chatError: null,
|
||||||
|
|
||||||
// Agents state
|
// Agents state
|
||||||
agents: [],
|
agents: [],
|
||||||
@@ -119,11 +127,129 @@ export const useOpenClawStore = create(
|
|||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
setOpenclawStatus: (data) => set({ openclawStatus: data, statusError: null }),
|
setOpenclawStatus: (data) => set({ openclawStatus: data, statusError: null }),
|
||||||
setOpenclawSessions: (data) => set({ openclawSessions: data?.sessions || [], sessionsError: null }),
|
setOpenclawSessions: (data) => set({
|
||||||
setOpenclawSessionDetail: (data) => set({ openclawSessionDetail: data?.session || null, sessionDetailError: null }),
|
openclawSessions: data?.sessions || [],
|
||||||
setOpenclawSessionHistory: (data) => set({ openclawSessionHistory: data?.history || [], sessionDetailError: null }),
|
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 }),
|
setOpenclawCronJobs: (data) => set({ openclawCronJobs: data?.cron || [], cronError: null }),
|
||||||
setOpenclawApprovals: (data) => set({ openclawApprovals: data?.approvals || [], approvalsError: 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 }),
|
setSelectedSessionKey: (key) => set({ selectedSessionKey: key }),
|
||||||
|
|
||||||
@@ -138,6 +264,7 @@ export const useOpenClawStore = create(
|
|||||||
setSessionDetailError: (e) => set({ sessionDetailError: e }),
|
setSessionDetailError: (e) => set({ sessionDetailError: e }),
|
||||||
setCronError: (e) => set({ cronError: e }),
|
setCronError: (e) => set({ cronError: e }),
|
||||||
setApprovalsError: (e) => set({ approvalsError: e }),
|
setApprovalsError: (e) => set({ approvalsError: e }),
|
||||||
|
setChatError: (e) => set({ chatError: e }),
|
||||||
|
|
||||||
setAgents: (agents) => set({ agents }),
|
setAgents: (agents) => set({ agents }),
|
||||||
setAgentsLoading: (loading) => set({ agentsLoading: loading }),
|
setAgentsLoading: (loading) => set({ agentsLoading: loading }),
|
||||||
|
|||||||
@@ -282,7 +282,6 @@
|
|||||||
"trading_days_completed": 0,
|
"trading_days_completed": 0,
|
||||||
"server_mode": "live",
|
"server_mode": "live",
|
||||||
"is_backtest": false,
|
"is_backtest": false,
|
||||||
"is_mock_mode": false,
|
|
||||||
"data_sources": {
|
"data_sources": {
|
||||||
"preferred": [
|
"preferred": [
|
||||||
"yfinance",
|
"yfinance",
|
||||||
@@ -300,4 +299,4 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"last_saved": "2026-03-16T00:25:09.700631"
|
"last_saved": "2026-03-16T00:25:09.700631"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -517,11 +517,13 @@ class OpenClawWebSocketClient:
|
|||||||
params["agentId"] = agent_id
|
params["agentId"] = agent_id
|
||||||
if label:
|
if label:
|
||||||
params["label"] = label
|
params["label"] = label
|
||||||
|
if channel:
|
||||||
|
params["channel"] = channel
|
||||||
|
|
||||||
result = await self._send_request("sessions.resolve", params)
|
result = await self._send_request("sessions.resolve", params)
|
||||||
sessions = result.get("sessions", [])
|
key = result.get("key")
|
||||||
if sessions:
|
if isinstance(key, str) and key.strip():
|
||||||
return sessions[0].get("key")
|
return key.strip()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
@@ -549,12 +551,30 @@ class OpenClawWebSocketClient:
|
|||||||
if thinking:
|
if thinking:
|
||||||
params["thinking"] = thinking
|
params["thinking"] = thinking
|
||||||
|
|
||||||
# Use shorter timeout for send since it waits for agent response
|
previous_timeout_ms = self.timeout_ms
|
||||||
result = await self._send_request(
|
if timeout_ms is not None:
|
||||||
"sessions.send",
|
self.timeout_ms = timeout_ms
|
||||||
params,
|
try:
|
||||||
)
|
return await self._send_request("sessions.send", params)
|
||||||
return result
|
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:
|
async def subscribe(self, session_key: str) -> AsyncMessageIterator:
|
||||||
"""Subscribe to messages from a session.
|
"""Subscribe to messages from a session.
|
||||||
@@ -670,6 +690,11 @@ class AsyncMessageIterator:
|
|||||||
async def __anext__(self) -> MessageEvent:
|
async def __anext__(self) -> MessageEvent:
|
||||||
return await self._queue.get()
|
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
|
# Synchronous convenience functions
|
||||||
|
|||||||
Reference in New Issue
Block a user