openclaw-agent-dashboard 1.0.17 → 1.0.18
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.
- package/README.md +1 -1
- package/dashboard/api/performance.py +4 -1
- package/dashboard/api/subagents.py +16 -33
- package/dashboard/api/version.py +1 -1
- package/dashboard/data/agent_config_manager.py +2 -1
- package/dashboard/data/session_reader.py +68 -13
- package/dashboard/data/subagent_reader.py +11 -22
- package/dashboard/data/timeline_reader.py +13 -12
- package/dashboard/mechanism_reader.py +4 -2
- package/frontend-dist/assets/{index-Dz4mgXTK.css → index-CVOZbajP.css} +1 -1
- package/frontend-dist/assets/{index-XJCqLMJe.js → index-CeOfyP-t.js} +1 -1
- package/frontend-dist/index.html +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ from pathlib import Path
|
|
|
10
10
|
from datetime import datetime, timedelta, timezone
|
|
11
11
|
from zoneinfo import ZoneInfo
|
|
12
12
|
|
|
13
|
+
from data.session_reader import normalize_sessions_index
|
|
14
|
+
|
|
13
15
|
# 详情展示使用 Asia/Shanghai 时区
|
|
14
16
|
TZ_DISPLAY = ZoneInfo('Asia/Shanghai')
|
|
15
17
|
|
|
@@ -710,7 +712,8 @@ async def get_tokens_analysis(range: str = "all"):
|
|
|
710
712
|
continue
|
|
711
713
|
|
|
712
714
|
agent_total = {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}
|
|
713
|
-
|
|
715
|
+
index_map = normalize_sessions_index(data)
|
|
716
|
+
for session_key, entry in index_map.items():
|
|
714
717
|
if not isinstance(entry, dict):
|
|
715
718
|
continue
|
|
716
719
|
inp = entry.get('inputTokens', 0) or 0
|
|
@@ -17,6 +17,7 @@ from data.subagent_reader import (
|
|
|
17
17
|
get_agent_files_for_run
|
|
18
18
|
)
|
|
19
19
|
from data.task_history import merge_with_history
|
|
20
|
+
from data.session_reader import normalize_sessions_index, resolve_session_jsonl_path
|
|
20
21
|
import time
|
|
21
22
|
|
|
22
23
|
router = APIRouter()
|
|
@@ -269,19 +270,13 @@ def _get_session_message_count(child_session_key: str) -> int:
|
|
|
269
270
|
|
|
270
271
|
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
271
272
|
index_data = json.load(f)
|
|
272
|
-
|
|
273
|
+
index_map = normalize_sessions_index(index_data)
|
|
274
|
+
entry = index_map.get(child_session_key)
|
|
273
275
|
if not entry:
|
|
274
276
|
return 0
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if not
|
|
278
|
-
return 0
|
|
279
|
-
if not session_file:
|
|
280
|
-
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
281
|
-
session_file = str(sessions_dir / f"{session_id}.jsonl")
|
|
282
|
-
|
|
283
|
-
session_path = Path(session_file)
|
|
284
|
-
if not session_path.exists():
|
|
277
|
+
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
278
|
+
session_path = resolve_session_jsonl_path(sessions_dir, entry)
|
|
279
|
+
if not session_path:
|
|
285
280
|
return 0
|
|
286
281
|
|
|
287
282
|
count = 0
|
|
@@ -365,19 +360,13 @@ def _extract_subtasks_from_session(child_session_key: str) -> List[Dict[str, Any
|
|
|
365
360
|
|
|
366
361
|
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
367
362
|
index_data = json.load(f)
|
|
368
|
-
|
|
363
|
+
index_map = normalize_sessions_index(index_data)
|
|
364
|
+
entry = index_map.get(child_session_key)
|
|
369
365
|
if not entry:
|
|
370
366
|
return []
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if not
|
|
374
|
-
return []
|
|
375
|
-
if not session_file:
|
|
376
|
-
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
377
|
-
session_file = str(sessions_dir / f"{session_id}.jsonl")
|
|
378
|
-
|
|
379
|
-
session_path = Path(session_file)
|
|
380
|
-
if not session_path.exists():
|
|
367
|
+
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
368
|
+
session_path = resolve_session_jsonl_path(sessions_dir, entry)
|
|
369
|
+
if not session_path:
|
|
381
370
|
return []
|
|
382
371
|
|
|
383
372
|
subtasks: List[Dict[str, Any]] = []
|
|
@@ -531,19 +520,13 @@ def _extract_timeline_from_session(child_session_key: str) -> List[Dict[str, Any
|
|
|
531
520
|
|
|
532
521
|
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
533
522
|
index_data = json.load(f)
|
|
534
|
-
|
|
523
|
+
index_map = normalize_sessions_index(index_data)
|
|
524
|
+
entry = index_map.get(child_session_key)
|
|
535
525
|
if not entry:
|
|
536
526
|
return []
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if not
|
|
540
|
-
return []
|
|
541
|
-
if not session_file:
|
|
542
|
-
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
543
|
-
session_file = str(sessions_dir / f"{session_id}.jsonl")
|
|
544
|
-
|
|
545
|
-
session_path = Path(session_file)
|
|
546
|
-
if not session_path.exists():
|
|
527
|
+
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
528
|
+
session_path = resolve_session_jsonl_path(sessions_dir, entry)
|
|
529
|
+
if not session_path:
|
|
547
530
|
return []
|
|
548
531
|
|
|
549
532
|
timeline: List[Dict[str, Any]] = []
|
package/dashboard/api/version.py
CHANGED
|
@@ -8,6 +8,7 @@ import shutil
|
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
|
|
10
10
|
from data.config_reader import get_openclaw_root
|
|
11
|
+
from data.session_reader import normalize_sessions_index
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def _backup_config() -> Optional[Path]:
|
|
@@ -230,7 +231,7 @@ def get_agent_full_info(agent_id: str) -> Dict[str, Any]:
|
|
|
230
231
|
try:
|
|
231
232
|
with open(session_file, 'r', encoding='utf-8') as f:
|
|
232
233
|
sessions_data = json.load(f)
|
|
233
|
-
entries = sessions_data
|
|
234
|
+
entries = normalize_sessions_index(sessions_data)
|
|
234
235
|
if entries:
|
|
235
236
|
latest = max(entries.values(), key=lambda e: e.get('lastMessageAt', 0))
|
|
236
237
|
last_active = latest.get('lastMessageAt')
|
|
@@ -9,6 +9,64 @@ from typing import List, Dict, Any, Optional
|
|
|
9
9
|
|
|
10
10
|
from data.config_reader import get_openclaw_root
|
|
11
11
|
|
|
12
|
+
_META_SESSION_INDEX_KEYS = frozenset({"entries", "version", "schema"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_sessions_index(raw: Any) -> Dict[str, Dict[str, Any]]:
|
|
16
|
+
"""
|
|
17
|
+
统一 sessions.json 结构(跨平台兼容 OpenClaw 不同版本):
|
|
18
|
+
- 常见嵌套:{"entries": {"agent:...": {...}}}
|
|
19
|
+
- 扁平:{"agent:...": {...}}
|
|
20
|
+
"""
|
|
21
|
+
if not isinstance(raw, dict):
|
|
22
|
+
return {}
|
|
23
|
+
out: Dict[str, Dict[str, Any]] = {}
|
|
24
|
+
inner = raw.get("entries")
|
|
25
|
+
if isinstance(inner, dict):
|
|
26
|
+
for k, v in inner.items():
|
|
27
|
+
if isinstance(v, dict):
|
|
28
|
+
out[str(k)] = v
|
|
29
|
+
for k, v in raw.items():
|
|
30
|
+
if k in _META_SESSION_INDEX_KEYS or not isinstance(v, dict):
|
|
31
|
+
continue
|
|
32
|
+
out.setdefault(str(k), v)
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_session_jsonl_path(sessions_dir: Path, entry: Dict[str, Any]) -> Optional[Path]:
|
|
37
|
+
"""
|
|
38
|
+
由 sessions.json 单条记录解析真实 .jsonl 路径。
|
|
39
|
+
兼容:绝对路径、相对 sessions 目录、仅文件名(避免 Windows 下 cwd 非 sessions 导致找不到文件)。
|
|
40
|
+
"""
|
|
41
|
+
sf = entry.get("sessionFile")
|
|
42
|
+
sid = entry.get("sessionId")
|
|
43
|
+
try:
|
|
44
|
+
sessions_dir = sessions_dir.resolve()
|
|
45
|
+
except OSError:
|
|
46
|
+
sessions_dir = sessions_dir
|
|
47
|
+
|
|
48
|
+
if sf:
|
|
49
|
+
p = Path(str(sf))
|
|
50
|
+
try:
|
|
51
|
+
if p.is_file():
|
|
52
|
+
return p.resolve()
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
for cand in (sessions_dir / sf, sessions_dir / p.name):
|
|
56
|
+
try:
|
|
57
|
+
if cand.is_file():
|
|
58
|
+
return cand.resolve()
|
|
59
|
+
except OSError:
|
|
60
|
+
continue
|
|
61
|
+
if sid:
|
|
62
|
+
cand = sessions_dir / f"{sid}.jsonl"
|
|
63
|
+
try:
|
|
64
|
+
if cand.is_file():
|
|
65
|
+
return cand.resolve()
|
|
66
|
+
except OSError:
|
|
67
|
+
pass
|
|
68
|
+
return None
|
|
69
|
+
|
|
12
70
|
|
|
13
71
|
def get_agent_sessions_path(agent_id: str) -> Optional[Path]:
|
|
14
72
|
"""获取 Agent 的 sessions 目录"""
|
|
@@ -139,12 +197,12 @@ def get_session_updated_at(agent_id: str) -> int:
|
|
|
139
197
|
data = json.load(f)
|
|
140
198
|
if not isinstance(data, dict):
|
|
141
199
|
return 0
|
|
200
|
+
index_map = normalize_sessions_index(data)
|
|
142
201
|
max_ts = 0
|
|
143
|
-
for entry in
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
max_ts = int(ts)
|
|
202
|
+
for entry in index_map.values():
|
|
203
|
+
ts = entry.get('updatedAt') or entry.get('lastMessageAt') or 0
|
|
204
|
+
if isinstance(ts, (int, float)) and ts > max_ts:
|
|
205
|
+
max_ts = int(ts)
|
|
148
206
|
return max_ts
|
|
149
207
|
except (json.JSONDecodeError, IOError):
|
|
150
208
|
return 0
|
|
@@ -170,18 +228,15 @@ def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: i
|
|
|
170
228
|
return []
|
|
171
229
|
|
|
172
230
|
session_file: Optional[Path] = None
|
|
231
|
+
sessions_path = get_openclaw_root() / "agents" / agent_id / "sessions"
|
|
173
232
|
if session_key:
|
|
174
233
|
try:
|
|
175
234
|
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
176
235
|
index_data = json.load(f)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if sf:
|
|
182
|
-
session_file = Path(sf)
|
|
183
|
-
elif sid:
|
|
184
|
-
session_file = get_openclaw_root() / "agents" / agent_id / "sessions" / f"{sid}.jsonl"
|
|
236
|
+
index_map = normalize_sessions_index(index_data)
|
|
237
|
+
entry = index_map.get(session_key)
|
|
238
|
+
if entry:
|
|
239
|
+
session_file = resolve_session_jsonl_path(sessions_path, entry)
|
|
185
240
|
except (json.JSONDecodeError, IOError):
|
|
186
241
|
pass
|
|
187
242
|
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import List, Dict, Any, Optional
|
|
7
7
|
|
|
8
8
|
from data.config_reader import get_openclaw_root
|
|
9
|
+
from data.session_reader import normalize_sessions_index, resolve_session_jsonl_path
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def load_subagent_runs() -> List[Dict[str, Any]]:
|
|
@@ -117,19 +118,13 @@ def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) ->
|
|
|
117
118
|
try:
|
|
118
119
|
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
119
120
|
index_data = json.load(f)
|
|
120
|
-
|
|
121
|
+
index_map = normalize_sessions_index(index_data)
|
|
122
|
+
entry = index_map.get(child_session_key)
|
|
121
123
|
if not entry:
|
|
122
124
|
return None
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if not
|
|
126
|
-
return None
|
|
127
|
-
if not session_file:
|
|
128
|
-
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
129
|
-
session_file = str(sessions_dir / f"{session_id}.jsonl")
|
|
130
|
-
|
|
131
|
-
session_path = Path(session_file)
|
|
132
|
-
if not session_path.exists():
|
|
125
|
+
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
126
|
+
session_path = resolve_session_jsonl_path(sessions_dir, entry)
|
|
127
|
+
if not session_path:
|
|
133
128
|
return None
|
|
134
129
|
|
|
135
130
|
last_text = None
|
|
@@ -185,19 +180,13 @@ def get_agent_files_for_run(child_session_key: str) -> List[str]:
|
|
|
185
180
|
try:
|
|
186
181
|
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
187
182
|
index_data = json.load(f)
|
|
188
|
-
|
|
183
|
+
index_map = normalize_sessions_index(index_data)
|
|
184
|
+
entry = index_map.get(child_session_key)
|
|
189
185
|
if not entry:
|
|
190
186
|
return []
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if not
|
|
194
|
-
return []
|
|
195
|
-
if not session_file:
|
|
196
|
-
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
197
|
-
session_file = str(sessions_dir / f"{session_id}.jsonl")
|
|
198
|
-
|
|
199
|
-
session_path = Path(session_file)
|
|
200
|
-
if not session_path.exists():
|
|
187
|
+
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
188
|
+
session_path = resolve_session_jsonl_path(sessions_dir, entry)
|
|
189
|
+
if not session_path:
|
|
201
190
|
return []
|
|
202
191
|
|
|
203
192
|
file_paths: List[str] = []
|
|
@@ -83,6 +83,7 @@ class TimelineStep:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
from data.config_reader import get_openclaw_root
|
|
86
|
+
from data.session_reader import normalize_sessions_index, resolve_session_jsonl_path
|
|
86
87
|
|
|
87
88
|
|
|
88
89
|
# 子 Agent 回传消息的特征
|
|
@@ -233,13 +234,17 @@ def _get_requester_info_for_session(agent_id: str, session_key: Optional[str]) -
|
|
|
233
234
|
try:
|
|
234
235
|
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
235
236
|
index_data = json.load(f)
|
|
237
|
+
index_map = normalize_sessions_index(index_data)
|
|
236
238
|
if not session_key:
|
|
237
|
-
entries =
|
|
239
|
+
entries = list(index_map.items())
|
|
238
240
|
if entries:
|
|
239
|
-
entries.sort(
|
|
241
|
+
entries.sort(
|
|
242
|
+
key=lambda x: (x[1].get('updatedAt') or x[1].get('lastMessageAt') or 0),
|
|
243
|
+
reverse=True,
|
|
244
|
+
)
|
|
240
245
|
session_key = entries[0][0]
|
|
241
246
|
if session_key:
|
|
242
|
-
entry =
|
|
247
|
+
entry = index_map.get(session_key)
|
|
243
248
|
if isinstance(entry, dict):
|
|
244
249
|
spawned_by = entry.get('spawnedBy', '')
|
|
245
250
|
if spawned_by and ':' in spawned_by:
|
|
@@ -420,15 +425,11 @@ def get_timeline_steps(
|
|
|
420
425
|
try:
|
|
421
426
|
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
422
427
|
index_data = json.load(f)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
session_file = Path(sf)
|
|
429
|
-
elif sid:
|
|
430
|
-
session_file = sessions_path / f"{sid}.jsonl"
|
|
431
|
-
session_id = sid or session_key
|
|
428
|
+
index_map = normalize_sessions_index(index_data)
|
|
429
|
+
entry = index_map.get(session_key)
|
|
430
|
+
if entry:
|
|
431
|
+
session_file = resolve_session_jsonl_path(sessions_path, entry)
|
|
432
|
+
session_id = entry.get('sessionId') or session_key
|
|
432
433
|
except (json.JSONDecodeError, IOError):
|
|
433
434
|
pass
|
|
434
435
|
else:
|
|
@@ -7,6 +7,7 @@ from typing import Dict, Any, List
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
from data.config_reader import get_openclaw_root
|
|
10
|
+
from data.session_reader import normalize_sessions_index
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def get_agent_mechanisms(agent_id: str) -> Dict[str, Any]:
|
|
@@ -33,8 +34,9 @@ def get_agent_mechanisms(agent_id: str) -> Dict[str, Any]:
|
|
|
33
34
|
if not isinstance(data, dict):
|
|
34
35
|
return result
|
|
35
36
|
|
|
36
|
-
# 取最新 session
|
|
37
|
-
|
|
37
|
+
# 取最新 session 的机制信息(兼容 sessions.json 顶层或 entries 嵌套)
|
|
38
|
+
index_map = normalize_sessions_index(data)
|
|
39
|
+
for session_key, entry in index_map.items():
|
|
38
40
|
if not isinstance(entry, dict):
|
|
39
41
|
continue
|
|
40
42
|
report = entry.get('systemPromptReport', {})
|