openclaw-agent-dashboard 1.0.39 → 1.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dashboard/api/agent_config_api.py +28 -7
  2. package/dashboard/api/agents.py +48 -10
  3. package/dashboard/api/agents_config.py +5 -1
  4. package/dashboard/api/chains.py +25 -5
  5. package/dashboard/api/collaboration.py +10 -9
  6. package/dashboard/api/debug_paths.py +5 -1
  7. package/dashboard/api/error_analysis.py +29 -11
  8. package/dashboard/api/errors.py +37 -11
  9. package/dashboard/api/fortify_routes.py +108 -0
  10. package/dashboard/api/input_safety.py +60 -0
  11. package/dashboard/api/performance.py +73 -53
  12. package/dashboard/api/subagents.py +95 -99
  13. package/dashboard/api/timeline.py +24 -3
  14. package/dashboard/api/version.py +2 -0
  15. package/dashboard/api/websocket.py +9 -7
  16. package/dashboard/core/__init__.py +1 -0
  17. package/dashboard/core/config_fortify.py +125 -0
  18. package/dashboard/core/error_handler.py +488 -0
  19. package/dashboard/core/fallback_manager.py +81 -0
  20. package/dashboard/core/logging_config.py +217 -0
  21. package/dashboard/core/safe_api_error.py +76 -0
  22. package/dashboard/core/schemas/__init__.py +16 -0
  23. package/dashboard/core/schemas/base.py +43 -0
  24. package/dashboard/core/schemas/session_schema.py +40 -0
  25. package/dashboard/core/schemas/subagent_schema.py +23 -0
  26. package/dashboard/data/agent_config_manager.py +6 -4
  27. package/dashboard/data/chain_reader.py +16 -12
  28. package/dashboard/data/error_analyzer.py +15 -11
  29. package/dashboard/data/session_reader.py +268 -46
  30. package/dashboard/data/subagent_reader.py +74 -49
  31. package/dashboard/data/timeline_reader.py +35 -49
  32. package/dashboard/main.py +24 -2
  33. package/dashboard/mechanism_reader.py +4 -5
  34. package/dashboard/mechanisms.py +2 -2
  35. package/dashboard/pytest.ini +3 -0
  36. package/dashboard/requirements.txt +5 -0
  37. package/dashboard/status/cache_fp_probe.py +40 -0
  38. package/dashboard/status/status_cache.py +199 -72
  39. package/dashboard/status/status_calculator.py +50 -30
  40. package/dashboard/tests/conftest.py +87 -0
  41. package/dashboard/tests/test_api_contracts.py +372 -0
  42. package/dashboard/tests/test_bench_fortify.py +176 -0
  43. package/dashboard/tests/test_fortify.py +952 -0
  44. package/dashboard/utils/__init__.py +1 -0
  45. package/dashboard/utils/data_repair.py +210 -0
  46. package/dashboard/watchers/file_watcher.py +380 -77
  47. package/frontend-dist/assets/{index-cYIOn3Wq.css → index-BIZ2xHfw.css} +1 -1
  48. package/frontend-dist/assets/{index-DyRXGevD.js → index-Cnr0b02R.js} +1 -1
  49. package/frontend-dist/index.html +2 -2
  50. package/openclaw.plugin.json +1 -1
  51. package/package.json +1 -1
  52. package/dashboard/agents.py +0 -74
  53. package/dashboard/collaboration.py +0 -407
  54. package/dashboard/errors.py +0 -63
  55. package/dashboard/performance.py +0 -474
  56. package/dashboard/session_reader.py +0 -240
  57. package/dashboard/status_calculator.py +0 -121
  58. package/dashboard/subagent_reader.py +0 -232
@@ -1,74 +0,0 @@
1
- """
2
- Agent API 路由
3
- """
4
- from fastapi import APIRouter
5
- from pydantic import BaseModel
6
- from typing import List, Optional
7
- import sys
8
- from pathlib import Path
9
- sys.path.append(str(Path(__file__).parent.parent))
10
-
11
- from status.status_calculator import (
12
- get_agents_with_status,
13
- format_last_active
14
- )
15
-
16
- router = APIRouter()
17
-
18
-
19
- class AgentStatus(BaseModel):
20
- id: str
21
- name: str
22
- role: str
23
- status: str # idle/working/down
24
- currentTask: Optional[str] = None
25
- lastActiveAt: Optional[int] = None
26
- lastActiveFormatted: Optional[str] = None
27
- error: Optional[dict] = None
28
-
29
-
30
- @router.get("/agents", response_model=List[AgentStatus])
31
- async def get_agents():
32
- """获取所有 Agent 列表及状态"""
33
- agents = get_agents_with_status()
34
-
35
- # 格式化最后活跃时间
36
- for agent in agents:
37
- if agent.get('lastActiveAt'):
38
- agent['lastActiveFormatted'] = format_last_active(agent['lastActiveAt'])
39
-
40
- return agents
41
-
42
-
43
- @router.get("/agents/{agent_id}", response_model=AgentStatus)
44
- async def get_agent(agent_id: str):
45
- """获取单个 Agent 详情"""
46
- agents = get_agents_with_status()
47
-
48
- from data.config_reader import agent_ids_equal
49
-
50
- for agent in agents:
51
- if agent_ids_equal(agent['id'], agent_id):
52
- if agent.get('lastActiveAt'):
53
- agent['lastActiveFormatted'] = format_last_active(agent['lastActiveAt'])
54
- return agent
55
-
56
- from fastapi import HTTPException
57
- raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
58
-
59
-
60
- @router.get("/agents/{agent_id}/output")
61
- async def get_agent_output(agent_id: str, limit: int = 50):
62
- """
63
- 获取 Agent 最近会话详情:每轮 user/assistant/toolResult 及 usage
64
- 用于调试视图展示
65
- """
66
- from data.session_reader import get_session_turns
67
- from data.config_reader import get_agent_config
68
-
69
- if not get_agent_config(agent_id):
70
- from fastapi import HTTPException
71
- raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
72
-
73
- turns = get_session_turns(agent_id, limit=limit)
74
- return {"agentId": agent_id, "turns": turns}
@@ -1,407 +0,0 @@
1
- """
2
- 协作流程 API 路由
3
- 符合 PRD: 展示老 K 与所有子 Agents 之间的连接关系,任务从老 K 流向子 Agents
4
- 扩展: Agent 模型配置、模型节点、最近调用(光球展示)
5
- """
6
- from fastapi import APIRouter
7
- from pydantic import BaseModel
8
- from typing import List, Optional, Dict, Any
9
- import sys
10
- from pathlib import Path
11
- from datetime import datetime, timedelta, timezone
12
- from zoneinfo import ZoneInfo
13
-
14
- TZ_DISPLAY = ZoneInfo('Asia/Shanghai')
15
-
16
- sys.path.append(str(Path(__file__).parent.parent))
17
-
18
- router = APIRouter()
19
-
20
-
21
- class CollaborationNode(BaseModel):
22
- id: str
23
- type: str # agent/task/tool/model
24
- name: str
25
- status: str # idle/working/error (Agent 状态: 空闲/工作中/异常)
26
- timestamp: Optional[int] = None
27
- metadata: Optional[Dict[str, Any]] = None
28
-
29
-
30
- class CollaborationEdge(BaseModel):
31
- id: str
32
- source: str
33
- target: str
34
- type: str # delegates/calls/returns/error/model
35
- label: Optional[str] = None
36
-
37
-
38
- class ModelCall(BaseModel):
39
- id: str
40
- agentId: str
41
- model: str # provider/model
42
- sessionId: str
43
- trigger: str
44
- tokens: int
45
- timestamp: int # ms
46
- time: str # HH:MM:SS
47
-
48
-
49
- class CollaborationFlow(BaseModel):
50
- nodes: List[CollaborationNode]
51
- edges: List[CollaborationEdge]
52
- activePath: List[str]
53
- lastUpdate: int
54
- mainAgentId: Optional[str] = None # 主 Agent ID,前端用于布局与样式
55
- agentModels: Optional[Dict[str, Dict[str, Any]]] = None
56
- models: Optional[List[str]] = None
57
- recentCalls: Optional[List[ModelCall]] = None
58
-
59
-
60
- def _parse_agent_id(session_key: str) -> str:
61
- """从 sessionKey (childSessionKey 或 requesterSessionKey) 解析 agentId"""
62
- parts = (session_key or '').split(':')
63
- if len(parts) >= 2 and parts[0] == 'agent':
64
- return parts[1]
65
- return ''
66
-
67
-
68
- def _get_recent_model_calls(minutes: int = 30) -> List[Dict]:
69
- """获取最近 N 分钟的模型调用记录(用于光球展示)"""
70
- from api.performance import parse_session_file_with_details
71
-
72
- records = []
73
- from data.config_reader import get_openclaw_root
74
- openclaw_path = get_openclaw_root()
75
- agents_path = openclaw_path / 'agents'
76
- if not agents_path.exists():
77
- return []
78
-
79
- now = datetime.now(timezone.utc)
80
- since = now - timedelta(minutes=minutes)
81
-
82
- for agent_dir in agents_path.iterdir():
83
- if not agent_dir.is_dir():
84
- continue
85
- agent_id = agent_dir.name
86
- sessions_path = agent_dir / 'sessions'
87
- if not sessions_path.exists():
88
- continue
89
- for session_file in sessions_path.glob('*.jsonl'):
90
- if 'lock' in session_file.name or 'deleted' in session_file.name:
91
- continue
92
- for r in parse_session_file_with_details(session_file, agent_id):
93
- if r['timestamp'] >= since:
94
- ts = r['timestamp']
95
- if ts.tzinfo is None:
96
- ts = ts.replace(tzinfo=timezone.utc)
97
- ts_local = ts.astimezone(TZ_DISPLAY)
98
- records.append({
99
- 'agentId': agent_id,
100
- 'model': r.get('model', ''),
101
- 'sessionId': r['sessionId'],
102
- 'trigger': r.get('trigger', ''),
103
- 'tokens': r.get('tokens', 0),
104
- 'timestamp': int(ts.timestamp() * 1000),
105
- 'time': ts_local.strftime('%H:%M:%S')
106
- })
107
- records.sort(key=lambda x: x['timestamp'])
108
- return records[-100:] # 最多 100 条
109
-
110
-
111
- @router.get("/collaboration", response_model=CollaborationFlow)
112
- async def get_collaboration():
113
- """获取协作流程数据 - 主 Agent 与子 Agents 的拓扑关系,含模型配置与最近调用"""
114
- from data.config_reader import (
115
- get_agents_list, get_agent_models, get_models_configured_by_agents,
116
- get_model_display_name, get_main_agent_id, agent_ids_equal,
117
- )
118
- from data.subagent_reader import get_active_runs
119
- from status.status_calculator import calculate_agent_status
120
-
121
- nodes = []
122
- edges = []
123
- active_path = []
124
- agent_models: Dict[str, Dict[str, Any]] = {}
125
- models_list: List[str] = []
126
- recent_calls: List[Dict] = []
127
-
128
- main_agent_id = 'main'
129
- try:
130
- main_agent_id = get_main_agent_id()
131
- agents_list = get_agents_list()
132
- active_runs = get_active_runs()
133
-
134
- main_agent_id = get_main_agent_id()
135
- main_agent_config = next(
136
- (a for a in agents_list if agent_ids_equal(a.get('id'), main_agent_id)), None
137
- )
138
- sub_agents_config = [a for a in agents_list if not agent_ids_equal(a.get('id'), main_agent_id)]
139
-
140
- all_agents = [a for a in agents_list if a.get('id')]
141
- for agent in all_agents:
142
- aid = agent.get('id', '')
143
- agent_models[aid] = get_agent_models(aid)
144
- models_list = get_models_configured_by_agents()
145
- recent_calls = _get_recent_model_calls(30)
146
-
147
- main_display_name = (main_agent_config.get('name') if main_agent_config else None) or "主 Agent"
148
- main_status = "working" if active_runs else "idle"
149
- main_agent = CollaborationNode(
150
- id=main_agent_id,
151
- type="agent",
152
- name=main_display_name,
153
- status=main_status,
154
- timestamp=int(__import__('time').time() * 1000),
155
- metadata=agent_models.get(main_agent_id)
156
- )
157
- nodes.append(main_agent)
158
-
159
- for agent in sub_agents_config:
160
- agent_id = agent.get('id', '')
161
- agent_name = agent.get('name', agent_id)
162
-
163
- status = calculate_agent_status(agent_id)
164
- if status == 'down':
165
- status = 'error'
166
- elif status == 'working':
167
- status = 'working'
168
- else:
169
- status = 'idle'
170
-
171
- sub_node = CollaborationNode(
172
- id=agent_id,
173
- type="agent",
174
- name=agent_name,
175
- status=status,
176
- timestamp=None,
177
- metadata=agent_models.get(agent_id)
178
- )
179
- nodes.append(sub_node)
180
-
181
- edges.append(CollaborationEdge(
182
- id=f"edge-{main_agent_id}-{agent_id}",
183
- source=main_agent_id,
184
- target=agent_id,
185
- type="delegates",
186
- label="委托"
187
- ))
188
-
189
- # 3. 模型节点(右侧)
190
- for i, model_id in enumerate(models_list):
191
- model_node = CollaborationNode(
192
- id=f"model-{model_id.replace('/', '-')}",
193
- type="model",
194
- name=get_model_display_name(model_id),
195
- status="idle",
196
- timestamp=None,
197
- metadata={"modelId": model_id}
198
- )
199
- nodes.append(model_node)
200
-
201
- # 4. Agent -> Model 边(配置了该模型的 Agent)
202
- for agent in all_agents:
203
- aid = agent.get('id', '')
204
- cfg = agent_models.get(aid, {})
205
- primary = cfg.get('primary', '')
206
- fallbacks = cfg.get('fallbacks', [])
207
- all_models = [primary] + [f for f in fallbacks if f != primary]
208
- for mid in all_models:
209
- if mid and mid in models_list:
210
- mid_safe = mid.replace('/', '-')
211
- edges.append(CollaborationEdge(
212
- id=f"edge-{aid}-model-{mid_safe}",
213
- source=aid,
214
- target=f"model-{mid_safe}",
215
- type="model",
216
- label=mid
217
- ))
218
-
219
- # 5. 活跃任务与 spawn 链:requesterSessionKey -> childSessionKey -> task
220
- for run in active_runs[:10]:
221
- child_key = run.get('childSessionKey', '')
222
- requester_key = run.get('requesterSessionKey', '')
223
- agent_id = _parse_agent_id(child_key)
224
- requester_id = _parse_agent_id(requester_key)
225
- if not agent_id:
226
- continue
227
-
228
- task_name = run.get('task', 'Unknown Task')
229
- first_line = task_name.split('\n')[0].strip() if task_name else 'Unknown Task'
230
- task_name = first_line if first_line else task_name
231
-
232
- task_id = f"task-{run.get('runId', agent_id)}"
233
- task_node = CollaborationNode(
234
- id=task_id,
235
- type="task",
236
- name=task_name,
237
- status="working",
238
- timestamp=run.get('startedAt')
239
- )
240
- nodes.append(task_node)
241
-
242
- edges.append(CollaborationEdge(
243
- id=f"edge-{agent_id}-{task_id}",
244
- source=agent_id,
245
- target=task_id,
246
- type="calls",
247
- label="执行"
248
- ))
249
- # Spawn 链:主 Agent 派发 -> 子 Agent 执行
250
- if requester_id and requester_id != agent_id:
251
- edges.append(CollaborationEdge(
252
- id=f"edge-spawn-{requester_id}-{task_id}",
253
- source=requester_id,
254
- target=task_id,
255
- type="delegates",
256
- label="派发"
257
- ))
258
-
259
- active_path.extend([main_agent_id, agent_id, task_id])
260
-
261
- except Exception as e:
262
- print(f"Error building collaboration data: {e}")
263
- import traceback
264
- traceback.print_exc()
265
-
266
- if not nodes:
267
- try:
268
- main_agent_id = get_main_agent_id()
269
- except Exception:
270
- main_agent_id = 'main'
271
- nodes = [
272
- CollaborationNode(id=main_agent_id, type="agent", name="主 Agent", status="idle"),
273
- ]
274
-
275
- # 构建 recentCalls 带 id
276
- model_calls = [
277
- ModelCall(
278
- id=f"call-{i}",
279
- agentId=r["agentId"],
280
- model=r.get("model", ""),
281
- sessionId=r.get("sessionId", ""),
282
- trigger=r.get("trigger", ""),
283
- tokens=r.get("tokens", 0),
284
- timestamp=r.get("timestamp", 0),
285
- time=r.get("time", "")
286
- )
287
- for i, r in enumerate(recent_calls)
288
- ]
289
-
290
- return CollaborationFlow(
291
- nodes=nodes,
292
- edges=edges,
293
- activePath=list(set(active_path)),
294
- lastUpdate=int(__import__('time').time() * 1000),
295
- mainAgentId=main_agent_id,
296
- agentModels=agent_models,
297
- models=models_list,
298
- recentCalls=model_calls
299
- )
300
-
301
-
302
- class CollaborationDynamic(BaseModel):
303
- """仅动态数据:状态、小球、任务节点,不包含静态拓扑"""
304
- activePath: List[str]
305
- recentCalls: List[ModelCall]
306
- agentStatuses: Dict[str, str] # agentId -> idle/working/error
307
- taskNodes: List[CollaborationNode]
308
- taskEdges: List[CollaborationEdge]
309
- mainAgentId: str
310
- lastUpdate: int
311
-
312
-
313
- @router.get("/collaboration/dynamic", response_model=CollaborationDynamic)
314
- async def get_collaboration_dynamic():
315
- """轻量接口:仅返回状态、小球、任务等动态数据,用于静默刷新,不触发整体重载"""
316
- from data.config_reader import get_agents_list, get_main_agent_id
317
- from data.subagent_reader import get_active_runs
318
- from status.status_calculator import calculate_agent_status
319
-
320
- active_path = []
321
- agent_statuses: Dict[str, str] = {}
322
- task_nodes: List[CollaborationNode] = []
323
- task_edges: List[CollaborationEdge] = []
324
- main_agent_id = 'main'
325
-
326
- try:
327
- main_agent_id = get_main_agent_id()
328
- agents_list = get_agents_list()
329
- active_runs = get_active_runs()
330
-
331
- for agent in agents_list:
332
- aid = agent.get('id', '')
333
- if not aid:
334
- continue
335
- status = calculate_agent_status(aid)
336
- if status == 'down':
337
- status = 'error'
338
- elif status == 'working':
339
- status = 'working'
340
- else:
341
- status = 'idle'
342
- agent_statuses[aid] = status
343
-
344
- main_status = "working" if active_runs else "idle"
345
- agent_statuses[main_agent_id] = main_status
346
-
347
- for run in active_runs[:10]:
348
- child_key = run.get('childSessionKey', '')
349
- requester_key = run.get('requesterSessionKey', '')
350
- agent_id = _parse_agent_id(child_key)
351
- requester_id = _parse_agent_id(requester_key)
352
- if not agent_id:
353
- continue
354
- task_name = run.get('task', 'Unknown Task')
355
- first_line = task_name.split('\n')[0].strip() if task_name else 'Unknown Task'
356
- task_name = first_line if first_line else task_name
357
- task_id = f"task-{run.get('runId', agent_id)}"
358
- task_nodes.append(CollaborationNode(
359
- id=task_id,
360
- type="task",
361
- name=task_name,
362
- status="working",
363
- timestamp=run.get('startedAt')
364
- ))
365
- task_edges.append(CollaborationEdge(
366
- id=f"edge-{agent_id}-{task_id}",
367
- source=agent_id,
368
- target=task_id,
369
- type="calls",
370
- label="执行"
371
- ))
372
- if requester_id and requester_id != agent_id:
373
- task_edges.append(CollaborationEdge(
374
- id=f"edge-spawn-{requester_id}-{task_id}",
375
- source=requester_id,
376
- target=task_id,
377
- type="delegates",
378
- label="派发"
379
- ))
380
- active_path.extend([main_agent_id, agent_id, task_id])
381
- except Exception as e:
382
- print(f"Error building collaboration dynamic: {e}")
383
-
384
- recent_calls_raw = _get_recent_model_calls(30)
385
- model_calls = [
386
- ModelCall(
387
- id=f"call-{i}",
388
- agentId=r["agentId"],
389
- model=r.get("model", ""),
390
- sessionId=r.get("sessionId", ""),
391
- trigger=r.get("trigger", ""),
392
- tokens=r.get("tokens", 0),
393
- timestamp=r.get("timestamp", 0),
394
- time=r.get("time", "")
395
- )
396
- for i, r in enumerate(recent_calls_raw)
397
- ]
398
-
399
- return CollaborationDynamic(
400
- activePath=list(set(active_path)),
401
- recentCalls=model_calls,
402
- agentStatuses=agent_statuses,
403
- taskNodes=task_nodes,
404
- taskEdges=task_edges,
405
- mainAgentId=main_agent_id,
406
- lastUpdate=int(__import__('time').time() * 1000),
407
- )
@@ -1,63 +0,0 @@
1
- """
2
- 错误中心 API - 聚合 stopReason=error 与 model-failures.log
3
- """
4
- from fastapi import APIRouter
5
- from typing import List, Dict, Any
6
- import sys
7
- from pathlib import Path
8
-
9
- sys.path.append(str(Path(__file__).parent.parent))
10
-
11
- router = APIRouter()
12
-
13
-
14
- @router.get("/errors")
15
- async def get_errors(limit: int = 50):
16
- """
17
- 获取错误中心数据:聚合各 Agent 的 session 错误与 model-failures.log
18
- """
19
- from data.session_reader import get_recent_messages
20
- from data.config_reader import get_agents_list
21
- from status.error_detector import parse_failure_log
22
-
23
- result = {"sessionErrors": [], "modelFailures": []}
24
-
25
- # 1. Session 错误:遍历各 Agent 的 jsonl,找 stopReason=error
26
- agents = get_agents_list()
27
- for agent in agents:
28
- agent_id = agent.get('id', '')
29
- if not agent_id:
30
- continue
31
- messages = get_recent_messages(agent_id, limit=100)
32
- for msg in messages:
33
- if msg.get('stopReason') != 'error':
34
- continue
35
- err_type = 'unknown'
36
- err_msg = msg.get('errorMessage', '') or ''
37
- if '429' in err_msg or 'rate limit' in err_msg.lower():
38
- err_type = 'rate-limit'
39
- elif 'token' in err_msg.lower() or 'context' in err_msg.lower():
40
- err_type = 'token-limit'
41
- elif 'timeout' in err_msg.lower() or '超时' in err_msg:
42
- err_type = 'timeout'
43
- result["sessionErrors"].append({
44
- "agentId": agent_id,
45
- "type": err_type,
46
- "message": err_msg[:200] if err_msg else "(无详情)",
47
- "timestamp": msg.get('timestamp', 0),
48
- })
49
-
50
- result["sessionErrors"].sort(key=lambda x: x["timestamp"], reverse=True)
51
- result["sessionErrors"] = result["sessionErrors"][:limit]
52
-
53
- # 2. Model failures log
54
- failures = parse_failure_log()
55
- for f in failures[:limit]:
56
- result["modelFailures"].append({
57
- "model": f.get("model", ""),
58
- "errorType": f.get("error_type", ""),
59
- "message": (f.get("message", "") or "")[:200],
60
- "timestamp": f.get("timestamp", 0),
61
- })
62
-
63
- return result