openclaw-agent-dashboard 1.0.4

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 (111) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/README.md +302 -0
  3. package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +132 -0
  4. package/docs/RELEASE-LATEST.md +189 -0
  5. package/docs/RELEASE-MODEL-CONFIG.md +95 -0
  6. package/docs/release-guide.md +259 -0
  7. package/docs/release-operations-manual.md +167 -0
  8. package/docs/specs/tr3-install-system.md +580 -0
  9. package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
  10. package/frontend/index.html +12 -0
  11. package/frontend/package-lock.json +1240 -0
  12. package/frontend/package.json +19 -0
  13. package/frontend/src/App.vue +331 -0
  14. package/frontend/src/components/AgentCard.vue +796 -0
  15. package/frontend/src/components/AgentConfigPanel.vue +539 -0
  16. package/frontend/src/components/AgentDetailPanel.vue +738 -0
  17. package/frontend/src/components/ErrorAnalysisView.vue +546 -0
  18. package/frontend/src/components/ErrorCenterPanel.vue +844 -0
  19. package/frontend/src/components/PerformanceMonitor.vue +515 -0
  20. package/frontend/src/components/SettingsPanel.vue +236 -0
  21. package/frontend/src/components/TokenAnalysisPanel.vue +683 -0
  22. package/frontend/src/components/chain/ChainEdge.vue +85 -0
  23. package/frontend/src/components/chain/ChainNode.vue +166 -0
  24. package/frontend/src/components/chain/TaskChainView.vue +425 -0
  25. package/frontend/src/components/chain/index.ts +3 -0
  26. package/frontend/src/components/chain/types.ts +70 -0
  27. package/frontend/src/components/collaboration/CollaborationFlowSection.vue +1032 -0
  28. package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +113 -0
  29. package/frontend/src/components/performance/PerformancePanel.vue +119 -0
  30. package/frontend/src/components/performance/PerformanceSection.vue +1137 -0
  31. package/frontend/src/components/tasks/TaskStatusSection.vue +973 -0
  32. package/frontend/src/components/timeline/TimelineConnector.vue +31 -0
  33. package/frontend/src/components/timeline/TimelineRound.vue +135 -0
  34. package/frontend/src/components/timeline/TimelineStep.vue +691 -0
  35. package/frontend/src/components/timeline/TimelineToolLink.vue +109 -0
  36. package/frontend/src/components/timeline/TimelineView.vue +540 -0
  37. package/frontend/src/components/timeline/index.ts +5 -0
  38. package/frontend/src/components/timeline/types.ts +120 -0
  39. package/frontend/src/composables/index.ts +7 -0
  40. package/frontend/src/composables/useDebounce.ts +48 -0
  41. package/frontend/src/composables/useRealtime.ts +52 -0
  42. package/frontend/src/composables/useState.ts +52 -0
  43. package/frontend/src/composables/useThrottle.ts +46 -0
  44. package/frontend/src/composables/useVirtualScroll.ts +106 -0
  45. package/frontend/src/main.ts +4 -0
  46. package/frontend/src/managers/EventDispatcher.ts +127 -0
  47. package/frontend/src/managers/RealtimeDataManager.ts +293 -0
  48. package/frontend/src/managers/StateManager.ts +128 -0
  49. package/frontend/src/managers/index.ts +5 -0
  50. package/frontend/src/types/collaboration.ts +135 -0
  51. package/frontend/src/types/index.ts +20 -0
  52. package/frontend/src/types/performance.ts +105 -0
  53. package/frontend/src/types/task.ts +38 -0
  54. package/frontend/vite.config.ts +18 -0
  55. package/package.json +22 -0
  56. package/plugin/README.md +99 -0
  57. package/plugin/config.json.example +1 -0
  58. package/plugin/index.js +250 -0
  59. package/plugin/openclaw.plugin.json +17 -0
  60. package/plugin/package.json +21 -0
  61. package/scripts/build-plugin.js +67 -0
  62. package/scripts/bundle.sh +62 -0
  63. package/scripts/install-plugin.sh +162 -0
  64. package/scripts/install-python-deps.js +346 -0
  65. package/scripts/install-python-deps.sh +226 -0
  66. package/scripts/install.js +512 -0
  67. package/scripts/install.sh +367 -0
  68. package/scripts/lib/common.js +490 -0
  69. package/scripts/lib/common.sh +137 -0
  70. package/scripts/release-pack.sh +110 -0
  71. package/scripts/start.js +50 -0
  72. package/scripts/test_available_models.py +284 -0
  73. package/scripts/test_websocket_ping.py +44 -0
  74. package/src/backend/agents.py +73 -0
  75. package/src/backend/api/__init__.py +1 -0
  76. package/src/backend/api/agent_config_api.py +90 -0
  77. package/src/backend/api/agents.py +73 -0
  78. package/src/backend/api/agents_config.py +75 -0
  79. package/src/backend/api/chains.py +126 -0
  80. package/src/backend/api/collaboration.py +902 -0
  81. package/src/backend/api/debug_paths.py +39 -0
  82. package/src/backend/api/error_analysis.py +146 -0
  83. package/src/backend/api/errors.py +281 -0
  84. package/src/backend/api/performance.py +784 -0
  85. package/src/backend/api/subagents.py +770 -0
  86. package/src/backend/api/timeline.py +144 -0
  87. package/src/backend/api/websocket.py +251 -0
  88. package/src/backend/collaboration.py +405 -0
  89. package/src/backend/data/__init__.py +1 -0
  90. package/src/backend/data/agent_config_manager.py +270 -0
  91. package/src/backend/data/chain_reader.py +299 -0
  92. package/src/backend/data/config_reader.py +153 -0
  93. package/src/backend/data/error_analyzer.py +430 -0
  94. package/src/backend/data/session_reader.py +445 -0
  95. package/src/backend/data/subagent_reader.py +244 -0
  96. package/src/backend/data/task_history.py +118 -0
  97. package/src/backend/data/timeline_reader.py +981 -0
  98. package/src/backend/errors.py +63 -0
  99. package/src/backend/main.py +89 -0
  100. package/src/backend/mechanism_reader.py +131 -0
  101. package/src/backend/mechanisms.py +32 -0
  102. package/src/backend/performance.py +474 -0
  103. package/src/backend/requirements.txt +5 -0
  104. package/src/backend/session_reader.py +238 -0
  105. package/src/backend/status/__init__.py +1 -0
  106. package/src/backend/status/error_detector.py +122 -0
  107. package/src/backend/status/status_calculator.py +301 -0
  108. package/src/backend/status_calculator.py +121 -0
  109. package/src/backend/subagent_reader.py +229 -0
  110. package/src/backend/watchers/__init__.py +4 -0
  111. package/src/backend/watchers/file_watcher.py +159 -0
@@ -0,0 +1,144 @@
1
+ """
2
+ Timeline API 路由 - 实时执行时序图
3
+ """
4
+ from fastapi import APIRouter, Query, HTTPException
5
+ from pydantic import BaseModel
6
+ from typing import Optional, List, Dict, Any
7
+ import sys
8
+ from pathlib import Path
9
+ sys.path.append(str(Path(__file__).parent.parent))
10
+
11
+ from data.timeline_reader import get_timeline_steps, StepType, StepStatus
12
+ from data.config_reader import get_agents_list
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ class TimelineStats(BaseModel):
18
+ totalDuration: int
19
+ totalInputTokens: int
20
+ totalOutputTokens: int
21
+ toolCallCount: int
22
+ stepCount: int
23
+
24
+
25
+ class LLMRound(BaseModel):
26
+ id: str
27
+ index: int
28
+ trigger: str
29
+ triggerBy: Optional[str] = None
30
+ stepIds: List[str] = []
31
+ duration: int = 0
32
+ tokens: Optional[Dict[str, int]] = None
33
+
34
+
35
+ class TimelineResponse(BaseModel):
36
+ sessionId: Optional[str] = None
37
+ agentId: str
38
+ agentName: Optional[str] = None
39
+ model: Optional[str] = None
40
+ startedAt: Optional[int] = None
41
+ status: str
42
+ steps: List[Dict[str, Any]]
43
+ stats: TimelineStats
44
+ message: Optional[str] = None
45
+ # LLM 轮次分组
46
+ rounds: Optional[List[LLMRound]] = None
47
+ roundMode: Optional[bool] = None
48
+
49
+
50
+ @router.get("/timeline/{agent_id}", response_model=TimelineResponse)
51
+ async def get_timeline(
52
+ agent_id: str,
53
+ session_key: Optional[str] = Query(None, description="指定 session key,默认最新"),
54
+ limit: int = Query(100, ge=1, le=500, description="步骤数量限制")
55
+ ):
56
+ """
57
+ 获取 Agent 会话的完整时序数据
58
+
59
+ 返回用户与 Agent 的交互时序,包括:
60
+ - 用户消息
61
+ - Agent 思考过程 (thinking)
62
+ - 工具调用及结果
63
+ - 错误信息
64
+ """
65
+ # 验证 agent_id
66
+ agents = get_agents_list()
67
+ agent_info = None
68
+ for a in agents:
69
+ if a.get('id') == agent_id:
70
+ agent_info = a
71
+ break
72
+
73
+ if not agent_info:
74
+ raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
75
+
76
+ # 获取时序数据
77
+ result = get_timeline_steps(agent_id, session_key, limit)
78
+
79
+ # 补充 Agent 信息
80
+ result['agentName'] = agent_info.get('name', agent_id)
81
+
82
+ # 处理 model 字段:可能是字符串或字典
83
+ model_info = agent_info.get('model', 'unknown')
84
+ if isinstance(model_info, dict):
85
+ result['model'] = model_info.get('primary', 'unknown')
86
+ else:
87
+ result['model'] = model_info
88
+
89
+ return result
90
+
91
+
92
+ @router.get("/timeline/{agent_id}/steps")
93
+ async def get_timeline_steps_only(
94
+ agent_id: str,
95
+ session_key: Optional[str] = Query(None),
96
+ limit: int = Query(50, ge=1, le=200),
97
+ step_type: Optional[str] = Query(None, description="过滤步骤类型: user, thinking, toolCall, toolResult, error")
98
+ ):
99
+ """
100
+ 获取时序步骤(简化版,只返回步骤列表)
101
+
102
+ 可按步骤类型过滤
103
+ """
104
+ agents = get_agents_list()
105
+ if not any(a.get('id') == agent_id for a in agents):
106
+ raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
107
+
108
+ result = get_timeline_steps(agent_id, session_key, limit)
109
+ steps = result.get('steps', [])
110
+
111
+ # 类型过滤
112
+ if step_type:
113
+ steps = [s for s in steps if s.get('type') == step_type]
114
+
115
+ return {"steps": steps, "count": len(steps)}
116
+
117
+
118
+ @router.get("/timeline/{agent_id}/summary")
119
+ async def get_timeline_summary(agent_id: str, session_key: Optional[str] = Query(None)):
120
+ """
121
+ 获取时序摘要统计
122
+
123
+ 快速查看会话概览,不返回详细步骤
124
+ """
125
+ agents = get_agents_list()
126
+ if not any(a.get('id') == agent_id for a in agents):
127
+ raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
128
+
129
+ result = get_timeline_steps(agent_id, session_key, limit=10) # 只需基本信息
130
+
131
+ # 统计各类型步骤数量
132
+ steps = result.get('steps', [])
133
+ type_counts = {}
134
+ for step in steps:
135
+ t = step.get('type', 'unknown')
136
+ type_counts[t] = type_counts.get(t, 0) + 1
137
+
138
+ return {
139
+ "agentId": agent_id,
140
+ "sessionId": result.get('sessionId'),
141
+ "status": result.get('status'),
142
+ "stats": result.get('stats'),
143
+ "typeCounts": type_counts
144
+ }
@@ -0,0 +1,251 @@
1
+ """
2
+ WebSocket 路由
3
+ """
4
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
5
+ from typing import Set
6
+ import json
7
+ import asyncio
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.append(str(Path(__file__).parent.parent))
12
+
13
+ router = APIRouter()
14
+
15
+ # 活跃的 WebSocket 连接
16
+ active_connections: Set[WebSocket] = set()
17
+
18
+ # 周期性推送间隔(秒)
19
+ BROADCAST_INTERVAL_SEC = 3
20
+ _broadcast_task: asyncio.Task | None = None
21
+
22
+
23
+ async def _periodic_broadcast_loop():
24
+ """周期性广播完整状态,确保无文件变更时也有更新"""
25
+ while True:
26
+ await asyncio.sleep(BROADCAST_INTERVAL_SEC)
27
+ if active_connections:
28
+ await broadcast_full_state()
29
+
30
+
31
+ def _ensure_broadcast_task():
32
+ """有连接时启动周期性推送"""
33
+ global _broadcast_task
34
+ if active_connections and (_broadcast_task is None or _broadcast_task.done()):
35
+ _broadcast_task = asyncio.create_task(_periodic_broadcast_loop())
36
+
37
+
38
+ def _cancel_broadcast_task():
39
+ """无连接时停止周期性推送"""
40
+ global _broadcast_task
41
+ if not active_connections and _broadcast_task and not _broadcast_task.done():
42
+ _broadcast_task.cancel()
43
+ _broadcast_task = None
44
+
45
+
46
+ @router.websocket("/ws")
47
+ async def websocket_endpoint(websocket: WebSocket):
48
+ """WebSocket 端点"""
49
+ await websocket.accept()
50
+ active_connections.add(websocket)
51
+ _ensure_broadcast_task()
52
+
53
+ try:
54
+ # 发送初始状态
55
+ await send_initial_state(websocket)
56
+
57
+ # 保持连接
58
+ while True:
59
+ # 心跳检测(同时支持纯文本 ping 和 JSON 格式)
60
+ data = await websocket.receive_text()
61
+
62
+ is_ping = False
63
+ try:
64
+ msg = json.loads(data)
65
+ if isinstance(msg, dict) and msg.get('type') == 'ping':
66
+ is_ping = True
67
+ except json.JSONDecodeError:
68
+ if data == 'ping':
69
+ is_ping = True
70
+
71
+ if is_ping:
72
+ await websocket.send_json({'type': 'pong', 'timestamp': int(asyncio.get_event_loop().time() * 1000)})
73
+ except WebSocketDisconnect:
74
+ active_connections.discard(websocket)
75
+ _cancel_broadcast_task()
76
+
77
+
78
+ async def send_initial_state(websocket: WebSocket):
79
+ """发送初始状态(含 collaboration,避免前端协作流程空白)"""
80
+ try:
81
+ from .agents import get_agents as get_agents_list
82
+ from .subagents import get_subagents, get_tasks
83
+ from .api_status import get_api_status_list
84
+ from status.status_calculator import format_last_active
85
+
86
+ agents = await get_agents_list()
87
+ subagents = await get_subagents()
88
+ api_status = await get_api_status_list()
89
+
90
+ for agent in agents:
91
+ if agent.get("lastActiveAt"):
92
+ agent["lastActiveFormatted"] = format_last_active(agent["lastActiveAt"])
93
+
94
+ data = {
95
+ 'agents': agents,
96
+ 'subagents': subagents,
97
+ 'apiStatus': api_status,
98
+ }
99
+ # collaboration/tasks/performance 单独获取,失败不影响主数据
100
+ try:
101
+ from .collaboration import get_collaboration
102
+ collab = await get_collaboration()
103
+ data['collaboration'] = collab.model_dump() if hasattr(collab, "model_dump") else collab
104
+ except Exception as e:
105
+ print(f"[WebSocket] collaboration 获取失败: {e}")
106
+ try:
107
+ tasks_result = await get_tasks()
108
+ data['tasks'] = tasks_result.get("tasks", []) if isinstance(tasks_result, dict) else []
109
+ except Exception as e:
110
+ print(f"[WebSocket] tasks 获取失败: {e}")
111
+ try:
112
+ from .performance import get_real_stats
113
+ data['performance'] = await get_real_stats()
114
+ except Exception as e:
115
+ print(f"[WebSocket] performance 获取失败: {e}")
116
+ try:
117
+ from .workflow import list_workflows
118
+ data['workflows'] = await list_workflows()
119
+ except Exception as e:
120
+ print(f"[WebSocket] workflows 获取失败: {e}")
121
+
122
+ await websocket.send_json({'type': 'full_state', 'data': data})
123
+ except Exception as e:
124
+ print(f"发送初始状态失败: {e}")
125
+
126
+
127
+ async def broadcast_agent_update(agent_id: str, status: str):
128
+ """广播 Agent 状态更新"""
129
+ if not active_connections:
130
+ return
131
+
132
+ message = {
133
+ 'type': 'agent_update',
134
+ 'data': {
135
+ 'agentId': agent_id,
136
+ 'status': status,
137
+ 'timestamp': int(asyncio.get_event_loop().time() * 1000)
138
+ }
139
+ }
140
+
141
+ await broadcast_message(message)
142
+
143
+
144
+ async def broadcast_subagent_update(run_id: str, agent_id: str, outcome: str):
145
+ """广播子代理状态更新"""
146
+ if not active_connections:
147
+ return
148
+
149
+ message = {
150
+ 'type': 'subagent_update',
151
+ 'data': {
152
+ 'runId': run_id,
153
+ 'agentId': agent_id,
154
+ 'outcome': outcome,
155
+ 'timestamp': int(asyncio.get_event_loop().time() * 1000)
156
+ }
157
+ }
158
+
159
+ await broadcast_message(message)
160
+
161
+
162
+ async def broadcast_api_status(provider: str, model: str, status: str):
163
+ """广播 API 状态更新"""
164
+ if not active_connections:
165
+ return
166
+
167
+ message = {
168
+ 'type': 'api_status_update',
169
+ 'data': {
170
+ 'provider': provider,
171
+ 'model': model,
172
+ 'status': status,
173
+ 'timestamp': int(asyncio.get_event_loop().time() * 1000)
174
+ }
175
+ }
176
+
177
+ await broadcast_message(message)
178
+
179
+
180
+ async def broadcast_message(message: dict):
181
+ """广播消息到所有连接"""
182
+ disconnected = set()
183
+
184
+ for connection in active_connections:
185
+ try:
186
+ await connection.send_json(message)
187
+ except:
188
+ disconnected.add(connection)
189
+
190
+ # 清理断开的连接
191
+ for connection in disconnected:
192
+ active_connections.discard(connection)
193
+ if not active_connections:
194
+ _cancel_broadcast_task()
195
+
196
+
197
+ async def broadcast_full_state():
198
+ """文件变更时广播完整状态(agents、subagents、apiStatus、collaboration、tasks、performance)"""
199
+ if not active_connections:
200
+ return
201
+ try:
202
+ from .agents import get_agents as get_agents_list
203
+ from .subagents import get_subagents
204
+ from .api_status import get_api_status_list
205
+ from .collaboration import get_collaboration
206
+ from .performance import get_real_stats
207
+ from .workflow import list_workflows
208
+
209
+ agents = await get_agents_list()
210
+ subagents = await get_subagents()
211
+ api_status = await get_api_status_list()
212
+ collaboration = await get_collaboration()
213
+ performance = await get_real_stats()
214
+ workflows = await list_workflows()
215
+
216
+ # tasks 来自 subagents 的 get_tasks
217
+ from .subagents import get_tasks
218
+ tasks_result = await get_tasks()
219
+ tasks = tasks_result.get("tasks", []) if isinstance(tasks_result, dict) else []
220
+
221
+ # 格式化 agents 的 lastActiveFormatted
222
+ from status.status_calculator import format_last_active
223
+ for agent in agents:
224
+ if agent.get("lastActiveAt"):
225
+ agent["lastActiveFormatted"] = format_last_active(agent["lastActiveAt"])
226
+
227
+ await broadcast_message({
228
+ "type": "full_state",
229
+ "data": {
230
+ "agents": agents,
231
+ "subagents": subagents,
232
+ "apiStatus": api_status,
233
+ "collaboration": collaboration.model_dump() if hasattr(collaboration, "model_dump") else collaboration,
234
+ "tasks": tasks,
235
+ "performance": performance,
236
+ "workflows": workflows,
237
+ },
238
+ })
239
+ except Exception as e:
240
+ print(f"[WebSocket] broadcast_full_state 失败: {e}")
241
+
242
+
243
+ def get_active_connections_count() -> int:
244
+ """获取活跃连接数"""
245
+ return len(active_connections)
246
+
247
+
248
+ @router.get("/connections")
249
+ async def get_connections():
250
+ """获取活跃连接数"""
251
+ return {"count": get_active_connections_count()}