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,240 +0,0 @@
1
- """
2
- 会话读取器 - 读取 sessions/*.jsonl 和 sessions.json
3
- """
4
- import json
5
- import os
6
- from pathlib import Path
7
- from typing import List, Dict, Any, Optional
8
-
9
-
10
- from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
11
-
12
-
13
- def get_agent_sessions_path(agent_id: str) -> Optional[Path]:
14
- """获取 Agent 的 sessions 目录"""
15
- sessions_path = get_openclaw_root() / "agents" / normalize_openclaw_agent_id(agent_id) / "sessions"
16
- if not sessions_path.exists():
17
- return None
18
- return sessions_path
19
-
20
-
21
- def get_latest_session_file(agent_id: str) -> Optional[Path]:
22
- """获取最新的 session 文件"""
23
- sessions_path = get_agent_sessions_path(agent_id)
24
- if not sessions_path:
25
- return None
26
-
27
- jsonl_files = list(sessions_path.glob("*.jsonl"))
28
- if not jsonl_files:
29
- return None
30
-
31
- # 按修改时间排序,取最新的
32
- jsonl_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
33
- return jsonl_files[0]
34
-
35
-
36
- def get_recent_messages(agent_id: str, limit: int = 10) -> List[Dict[str, Any]]:
37
- """获取最近的会话消息"""
38
- session_file = get_latest_session_file(agent_id)
39
- if not session_file:
40
- return []
41
-
42
- messages = []
43
- with open(session_file, 'r', encoding='utf-8') as f:
44
- for line in f:
45
- try:
46
- data = json.loads(line.strip())
47
- if data.get('type') == 'message':
48
- messages.append(data.get('message', {}))
49
- except json.JSONDecodeError:
50
- continue
51
-
52
- # 只取最后 N 条
53
- return messages[-limit:]
54
-
55
-
56
- def has_recent_errors(agent_id: str, minutes: int = 5) -> bool:
57
- """检查最近是否有错误"""
58
- messages = get_recent_messages(agent_id, limit=50)
59
-
60
- import time
61
- cutoff_time = int(time.time() * 1000) - (minutes * 60 * 1000)
62
-
63
- for msg in messages:
64
- if msg.get('stopReason') == 'error':
65
- timestamp = msg.get('timestamp', 0)
66
- if timestamp > cutoff_time:
67
- return True
68
-
69
- return False
70
-
71
-
72
- def get_last_error(agent_id: str) -> Optional[Dict[str, Any]]:
73
- """获取最近的错误信息"""
74
- messages = get_recent_messages(agent_id, limit=100)
75
-
76
- for msg in reversed(messages):
77
- if msg.get('stopReason') == 'error':
78
- return {
79
- 'type': detect_error_type(msg.get('errorMessage', '')),
80
- 'message': msg.get('errorMessage', ''),
81
- 'timestamp': msg.get('timestamp', 0)
82
- }
83
-
84
- return None
85
-
86
-
87
- def detect_error_type(error_msg: str) -> str:
88
- """检测错误类型"""
89
- error_msg_lower = (error_msg or '').lower()
90
-
91
- if '429' in error_msg or 'rate limit' in error_msg_lower:
92
- return 'rate-limit'
93
- elif 'token' in error_msg_lower or 'context' in error_msg_lower:
94
- return 'token-limit'
95
- elif 'timeout' in error_msg_lower or '超时' in error_msg_lower:
96
- return 'timeout'
97
- elif '余额不足' in (error_msg or ''):
98
- return 'quota'
99
- else:
100
- return 'unknown'
101
-
102
-
103
- def get_session_updated_at(agent_id: str) -> int:
104
- """
105
- 获取 Agent 会话的最后更新时间(sessions.json 中 updatedAt 的最大值)
106
- 用于判断「最近 5 分钟是否有 session 活动」
107
- """
108
- aid = normalize_openclaw_agent_id(agent_id)
109
- sessions_index = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
110
- if not sessions_index.exists():
111
- return 0
112
-
113
- try:
114
- with open(sessions_index, 'r', encoding='utf-8') as f:
115
- data = json.load(f)
116
- if not isinstance(data, dict):
117
- return 0
118
- max_ts = 0
119
- for entry in data.values():
120
- if isinstance(entry, dict):
121
- ts = entry.get('updatedAt', 0)
122
- if isinstance(ts, (int, float)) and ts > max_ts:
123
- max_ts = int(ts)
124
- return max_ts
125
- except (json.JSONDecodeError, IOError):
126
- return 0
127
-
128
-
129
- def has_recent_session_activity(agent_id: str, minutes: int = 5) -> bool:
130
- """检查 Agent 最近 N 分钟内是否有 session 活动"""
131
- import time
132
- updated_at = get_session_updated_at(agent_id)
133
- if not updated_at:
134
- return False
135
- cutoff = int(time.time() * 1000) - (minutes * 60 * 1000)
136
- return updated_at > cutoff
137
-
138
-
139
- def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: int = 50) -> List[Dict[str, Any]]:
140
- """
141
- 解析 jsonl 获取会话轮次,每轮包含 user/assistant/toolResult 及 usage
142
- 返回格式: [{ turnIndex, role, content, usage?, toolCalls?, stopReason?, timestamp }]
143
- """
144
- aid = normalize_openclaw_agent_id(agent_id)
145
- sessions_index = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
146
- if not sessions_index.exists():
147
- return []
148
-
149
- session_file: Optional[Path] = None
150
- if session_key:
151
- try:
152
- with open(sessions_index, 'r', encoding='utf-8') as f:
153
- index_data = json.load(f)
154
- entry = index_data.get(session_key) if isinstance(index_data, dict) else None
155
- if entry and isinstance(entry, dict):
156
- sf = entry.get('sessionFile')
157
- sid = entry.get('sessionId')
158
- if sf:
159
- session_file = Path(sf)
160
- elif sid:
161
- session_file = get_openclaw_root() / "agents" / aid / "sessions" / f"{sid}.jsonl"
162
- except (json.JSONDecodeError, IOError):
163
- pass
164
-
165
- if not session_file or not session_file.exists():
166
- session_file = get_latest_session_file(agent_id)
167
-
168
- if not session_file:
169
- return []
170
-
171
- turns: List[Dict[str, Any]] = []
172
- turn_index = 0
173
-
174
- with open(session_file, 'r', encoding='utf-8') as f:
175
- for line in f:
176
- try:
177
- data = json.loads(line.strip())
178
- if data.get('type') != 'message':
179
- continue
180
- msg = data.get('message', {})
181
- role = msg.get('role')
182
- if not role:
183
- continue
184
-
185
- turn: Dict[str, Any] = {
186
- 'turnIndex': turn_index,
187
- 'role': role,
188
- 'timestamp': msg.get('timestamp') or data.get('timestamp'),
189
- 'content': [],
190
- 'usage': msg.get('usage'),
191
- 'stopReason': msg.get('stopReason'),
192
- 'toolCalls': [],
193
- 'toolName': None,
194
- }
195
-
196
- content = msg.get('content', [])
197
- if isinstance(content, str):
198
- content = [{'type': 'text', 'text': content}]
199
-
200
- for c in content:
201
- if not isinstance(c, dict):
202
- continue
203
- ct = c.get('type')
204
- if ct == 'text':
205
- if role == 'toolResult':
206
- turn['content'].append({
207
- 'type': 'toolResult',
208
- 'content': c.get('text', ''),
209
- 'status': msg.get('details', {}).get('status'),
210
- 'error': msg.get('details', {}).get('error'),
211
- })
212
- else:
213
- turn['content'].append({'type': 'text', 'text': c.get('text', '')})
214
- elif ct == 'thinking':
215
- turn['content'].append({'type': 'thinking', 'text': c.get('thinking', '')})
216
- elif ct == 'toolCall':
217
- turn['toolCalls'].append({
218
- 'name': c.get('name'),
219
- 'arguments': c.get('arguments'),
220
- 'id': c.get('id'),
221
- })
222
- turn['toolName'] = c.get('name')
223
-
224
- if role == 'toolResult':
225
- turn['toolName'] = msg.get('toolName')
226
- if not turn['content']:
227
- details = msg.get('details', {})
228
- turn['content'].append({
229
- 'type': 'toolResult',
230
- 'status': details.get('status'),
231
- 'error': details.get('error'),
232
- })
233
-
234
- turns.append(turn)
235
- turn_index += 1
236
-
237
- except (json.JSONDecodeError, KeyError):
238
- continue
239
-
240
- return turns[-limit:] if len(turns) > limit else turns
@@ -1,121 +0,0 @@
1
- """
2
- 状态计算器 - 计算 Agent 状态
3
- """
4
- import sys
5
- from pathlib import Path
6
- sys.path.append(str(Path(__file__).parent.parent))
7
-
8
- import time
9
- from typing import Literal
10
- from data.config_reader import get_agents_list, get_agent_config
11
- from data.subagent_reader import is_agent_working, get_agent_runs
12
- from data.session_reader import (
13
- has_recent_errors,
14
- get_last_error,
15
- has_recent_session_activity,
16
- )
17
-
18
-
19
- AgentStatus = Literal['idle', 'working', 'down']
20
-
21
-
22
- def calculate_agent_status(agent_id: str) -> AgentStatus:
23
- """
24
- 计算 Agent 状态(基于 runs.json + sessions.json)
25
-
26
- 优先级:
27
- 1. 异常 (down) - 最近5分钟有 stopReason=error
28
- 2. 工作中 (working) - 有活跃 subagent run 或 session 正在处理(最近5分钟有活动)
29
- 3. 空闲 (idle) - 无活跃 run 且最近5分钟无 session 活动
30
- """
31
- # 检查异常
32
- if has_recent_errors(agent_id, minutes=5):
33
- return 'down'
34
-
35
- # 检查工作中:subagent run 未结束,或 session 最近有活动
36
- if is_agent_working(agent_id):
37
- return 'working'
38
- if has_recent_session_activity(agent_id, minutes=2):
39
- return 'working'
40
-
41
- # 默认空闲
42
- return 'idle'
43
-
44
-
45
- def get_agents_with_status() -> list:
46
- """获取所有 Agent 及其状态"""
47
- agents = get_agents_list()
48
- result = []
49
-
50
- for agent in agents:
51
- agent_id = agent.get('id')
52
- status = calculate_agent_status(agent_id)
53
-
54
- # 获取当前任务
55
- current_task = get_current_task(agent_id)
56
-
57
- # 获取最后活跃时间
58
- last_active = get_last_active_time(agent_id)
59
-
60
- # 获取错误信息
61
- last_error = get_last_error(agent_id) if status == 'down' else None
62
-
63
- result.append({
64
- 'id': agent_id,
65
- 'name': agent.get('name'),
66
- 'role': agent.get('name'),
67
- 'status': status,
68
- 'currentTask': current_task,
69
- 'lastActiveAt': last_active,
70
- 'error': last_error
71
- })
72
-
73
- return result
74
-
75
-
76
- def get_current_task(agent_id: str) -> str:
77
- """获取 Agent 当前任务"""
78
- runs = get_agent_runs(agent_id, limit=1)
79
- if not runs:
80
- return ''
81
-
82
- run = runs[0]
83
- task = run.get('task', '')
84
-
85
- # 截取前60个字符(统一长度)
86
- if len(task) > 60:
87
- task = task[:57] + '...'
88
-
89
- return task
90
-
91
-
92
- def get_last_active_time(agent_id: str) -> int:
93
- """获取 Agent 最后活跃时间(runs 或 sessions.json updatedAt)"""
94
- from data.session_reader import get_session_updated_at
95
-
96
- runs = get_agent_runs(agent_id, limit=1)
97
- run_ts = 0
98
- if runs:
99
- run = runs[0]
100
- run_ts = run.get('endedAt') or run.get('startedAt', 0)
101
-
102
- session_ts = get_session_updated_at(agent_id)
103
- return max(run_ts, session_ts)
104
-
105
-
106
- def format_last_active(timestamp: int) -> str:
107
- """格式化最后活跃时间为相对时间"""
108
- if not timestamp:
109
- return '未知'
110
-
111
- now = int(time.time() * 1000)
112
- diff_seconds = (now - timestamp) / 1000
113
-
114
- if diff_seconds < 60:
115
- return f"{int(diff_seconds)}秒前"
116
- elif diff_seconds < 3600:
117
- return f"{int(diff_seconds / 60)}分钟前"
118
- elif diff_seconds < 86400:
119
- return f"{int(diff_seconds / 3600)}小时前"
120
- else:
121
- return f"{int(diff_seconds / 86400)}天前"
@@ -1,232 +0,0 @@
1
- """
2
- 子代理运行读取器 - 读取 subagents/runs.json
3
- """
4
- import json
5
- import os
6
- from pathlib import Path
7
- from typing import List, Dict, Any, Optional
8
-
9
- from data.config_reader import normalize_openclaw_agent_id
10
-
11
-
12
- def _openclaw_home() -> Path:
13
- """OpenClaw 根目录,优先使用 OPENCLAW_HOME 环境变量"""
14
- env = os.environ.get("OPENCLAW_HOME")
15
- if env:
16
- p = Path(env).expanduser()
17
- if p.exists():
18
- return p
19
- return Path.home() / ".openclaw"
20
-
21
-
22
- SUBAGENTS_RUNS_PATH = _openclaw_home() / "subagents" / "runs.json"
23
-
24
-
25
- def load_subagent_runs() -> List[Dict[str, Any]]:
26
- """加载子代理运行记录
27
-
28
- OpenClaw runs.json 格式: {"version": 2, "runs": { runId: record }}
29
- """
30
- if not SUBAGENTS_RUNS_PATH.exists():
31
- return []
32
-
33
- with open(SUBAGENTS_RUNS_PATH, 'r', encoding='utf-8') as f:
34
- data = json.load(f)
35
-
36
- runs = data.get('runs', {})
37
- if isinstance(runs, dict):
38
- return list(runs.values())
39
- return runs if isinstance(runs, list) else []
40
-
41
-
42
- def get_active_runs() -> List[Dict[str, Any]]:
43
- """获取活跃的运行(未结束)"""
44
- runs = load_subagent_runs()
45
- return [run for run in runs if run.get('endedAt') is None]
46
-
47
-
48
- def get_agent_runs(agent_id: str, limit: int = 10) -> List[Dict[str, Any]]:
49
- """获取指定 Agent 的运行记录"""
50
- runs = load_subagent_runs()
51
- agent_runs = []
52
- prefix = f"agent:{normalize_openclaw_agent_id(agent_id)}:"
53
- for run in runs:
54
- child_key = run.get('childSessionKey', '')
55
- if prefix in child_key:
56
- agent_runs.append(run)
57
-
58
- # 按开始时间倒序
59
- agent_runs.sort(key=lambda x: x.get('startedAt', 0), reverse=True)
60
- return agent_runs[:limit]
61
-
62
-
63
- def is_agent_working(agent_id: str) -> bool:
64
- """
65
- 判断 Agent 是否在工作中
66
- - 作为执行者:childSessionKey 包含 agent:{agent_id}:
67
- - 作为派发者:requesterSessionKey 包含 agent:{agent_id}:(主 Agent 等待子 Agent 完成)
68
- """
69
- active_runs = get_active_runs()
70
- prefix = f"agent:{normalize_openclaw_agent_id(agent_id)}:"
71
- for run in active_runs:
72
- child_key = run.get('childSessionKey', '')
73
- requester_key = run.get('requesterSessionKey', '')
74
- if prefix in child_key:
75
- return True
76
- if prefix in requester_key:
77
- return True
78
- return False
79
-
80
-
81
- def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) -> Optional[str]:
82
- """
83
- 从子 Agent 的 session 文件中提取最后一次 assistant 消息的文本输出。
84
- 用于任务成功时展示 Agent 的执行结果。
85
-
86
- Args:
87
- child_session_key: 格式 agent:<agentId>:subagent:<uuid>
88
- max_chars: 最大返回字符数,超出则截断
89
-
90
- Returns:
91
- Agent 的文本输出,若无法获取则返回 None
92
- """
93
- if not child_session_key or ':' not in child_session_key:
94
- return None
95
- parts = child_session_key.split(':')
96
- if len(parts) < 2 or parts[0] != 'agent':
97
- return None
98
- agent_id = normalize_openclaw_agent_id(parts[1])
99
-
100
- openclaw_path = _openclaw_home()
101
- sessions_index = openclaw_path / "agents" / agent_id / "sessions" / "sessions.json"
102
- if not sessions_index.exists():
103
- return None
104
-
105
- try:
106
- with open(sessions_index, 'r', encoding='utf-8') as f:
107
- index_data = json.load(f)
108
- entry = index_data.get(child_session_key)
109
- if not entry:
110
- return None
111
- session_file = entry.get('sessionFile')
112
- session_id = entry.get('sessionId')
113
- if not session_file and not session_id:
114
- return None
115
- if not session_file:
116
- sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
117
- session_file = str(sessions_dir / f"{session_id}.jsonl")
118
-
119
- session_path = Path(session_file)
120
- if not session_path.exists():
121
- return None
122
-
123
- last_text = None
124
- with open(session_path, 'r', encoding='utf-8') as f:
125
- for line in f:
126
- try:
127
- data = json.loads(line)
128
- if data.get('type') != 'message':
129
- continue
130
- msg = data.get('message', {})
131
- if msg.get('role') != 'assistant':
132
- continue
133
- content = msg.get('content', [])
134
- for c in content:
135
- if isinstance(c, dict) and c.get('type') == 'text':
136
- text = c.get('text', '')
137
- if text and text.strip():
138
- last_text = text
139
- break
140
- except (json.JSONDecodeError, KeyError):
141
- continue
142
-
143
- if not last_text or not last_text.strip():
144
- return None
145
- if len(last_text) > max_chars:
146
- return last_text[:max_chars] + '\n\n...(输出已截断)'
147
- return last_text
148
- except Exception as e:
149
- print(f"get_agent_output_for_run 失败: {e}")
150
- return None
151
-
152
-
153
- def get_agent_files_for_run(child_session_key: str) -> List[str]:
154
- """
155
- 从子 Agent 的 session 中提取本次任务生成/修改的文件路径。
156
- 解析 write、edit 等工具调用中的 path 参数。
157
-
158
- Returns:
159
- 去重后的文件路径列表
160
- """
161
- if not child_session_key or ':' not in child_session_key:
162
- return []
163
- parts = child_session_key.split(':')
164
- if len(parts) < 2 or parts[0] != 'agent':
165
- return []
166
- agent_id = normalize_openclaw_agent_id(parts[1])
167
-
168
- openclaw_path = _openclaw_home()
169
- sessions_index = openclaw_path / "agents" / agent_id / "sessions" / "sessions.json"
170
- if not sessions_index.exists():
171
- return []
172
-
173
- try:
174
- with open(sessions_index, 'r', encoding='utf-8') as f:
175
- index_data = json.load(f)
176
- entry = index_data.get(child_session_key)
177
- if not entry:
178
- return []
179
- session_file = entry.get('sessionFile')
180
- session_id = entry.get('sessionId')
181
- if not session_file and not session_id:
182
- return []
183
- if not session_file:
184
- sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
185
- session_file = str(sessions_dir / f"{session_id}.jsonl")
186
-
187
- session_path = Path(session_file)
188
- if not session_path.exists():
189
- return []
190
-
191
- file_paths: List[str] = []
192
- file_tools = ('write', 'edit')
193
-
194
- with open(session_path, 'r', encoding='utf-8') as f:
195
- for line in f:
196
- try:
197
- data = json.loads(line)
198
- if data.get('type') != 'message':
199
- continue
200
- msg = data.get('message', {})
201
- if msg.get('role') != 'assistant':
202
- continue
203
- content = msg.get('content', [])
204
- for c in content:
205
- if not isinstance(c, dict) or c.get('type') != 'toolCall':
206
- continue
207
- name = c.get('name', '')
208
- if name not in file_tools:
209
- continue
210
- args = c.get('arguments', {})
211
- if isinstance(args, str):
212
- try:
213
- args = json.loads(args)
214
- except json.JSONDecodeError:
215
- continue
216
- path = args.get('path') or args.get('file_path')
217
- if path and isinstance(path, str) and path.strip():
218
- file_paths.append(path.strip())
219
- except (json.JSONDecodeError, KeyError):
220
- continue
221
-
222
- # 去重并保持顺序
223
- seen = set()
224
- result = []
225
- for p in file_paths:
226
- if p not in seen:
227
- seen.add(p)
228
- result.append(p)
229
- return result
230
- except Exception as e:
231
- print(f"get_agent_files_for_run 失败: {e}")
232
- return []