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 CHANGED
@@ -32,7 +32,7 @@ npm run deploy
32
32
  | `npm run deploy` | 打包 + 安装到 OpenClaw(首次安装或升级) |
33
33
  | `npm run upgrade` | 拉取最新代码 + 部署(推荐用于升级) |
34
34
  | `npm run pack` | 仅打包插件,不安装(开发调试用) |
35
- | `npm run bundle` | 生成可分发的压缩包(给同事用) |
35
+ | `npm run bundle` | 生成可分发的压缩包(离线分发) |
36
36
 
37
37
  ### 升级插件
38
38
 
@@ -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
- for session_key, entry in data.items():
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
- entry = index_data.get(child_session_key)
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
- session_file = entry.get('sessionFile')
276
- session_id = entry.get('sessionId')
277
- if not session_file and not session_id:
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
- entry = index_data.get(child_session_key)
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
- session_file = entry.get('sessionFile')
372
- session_id = entry.get('sessionId')
373
- if not session_file and not session_id:
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
- entry = index_data.get(child_session_key)
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
- session_file = entry.get('sessionFile')
538
- session_id = entry.get('sessionId')
539
- if not session_file and not session_id:
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]] = []
@@ -10,7 +10,7 @@ from typing import Optional
10
10
  # 导入版本信息读取器
11
11
  from data.version_info_reader import get_version_reader
12
12
 
13
- router = APIRouter(prefix="/api", tags=["version"])
13
+ router = APIRouter()
14
14
 
15
15
 
16
16
  class VersionInfo(BaseModel):
@@ -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.get('entries', {})
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 data.values():
144
- if isinstance(entry, dict):
145
- ts = entry.get('updatedAt', 0)
146
- if isinstance(ts, (int, float)) and ts > max_ts:
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
- entry = index_data.get(session_key) if isinstance(index_data, dict) else None
178
- if entry and isinstance(entry, dict):
179
- sf = entry.get('sessionFile')
180
- sid = entry.get('sessionId')
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
- entry = index_data.get(child_session_key)
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
- session_file = entry.get('sessionFile')
124
- session_id = entry.get('sessionId')
125
- if not session_file and not session_id:
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
- entry = index_data.get(child_session_key)
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
- session_file = entry.get('sessionFile')
192
- session_id = entry.get('sessionId')
193
- if not session_file and not session_id:
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 = [(k, v) for k, v in index_data.items() if isinstance(v, dict)]
239
+ entries = list(index_map.items())
238
240
  if entries:
239
- entries.sort(key=lambda x: x[1].get('updatedAt', 0), reverse=True)
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 = index_data.get(session_key)
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
- entry = index_data.get(session_key) if isinstance(index_data, dict) else None
424
- if entry and isinstance(entry, dict):
425
- sf = entry.get('sessionFile')
426
- sid = entry.get('sessionId')
427
- if sf:
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
- for session_key, entry in data.items():
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', {})