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.
- package/dashboard/api/agent_config_api.py +28 -7
- package/dashboard/api/agents.py +48 -10
- package/dashboard/api/agents_config.py +5 -1
- package/dashboard/api/chains.py +25 -5
- package/dashboard/api/collaboration.py +10 -9
- package/dashboard/api/debug_paths.py +5 -1
- package/dashboard/api/error_analysis.py +29 -11
- package/dashboard/api/errors.py +37 -11
- package/dashboard/api/fortify_routes.py +108 -0
- package/dashboard/api/input_safety.py +60 -0
- package/dashboard/api/performance.py +73 -53
- package/dashboard/api/subagents.py +95 -99
- package/dashboard/api/timeline.py +24 -3
- package/dashboard/api/version.py +2 -0
- package/dashboard/api/websocket.py +9 -7
- package/dashboard/core/__init__.py +1 -0
- package/dashboard/core/config_fortify.py +125 -0
- package/dashboard/core/error_handler.py +488 -0
- package/dashboard/core/fallback_manager.py +81 -0
- package/dashboard/core/logging_config.py +217 -0
- package/dashboard/core/safe_api_error.py +76 -0
- package/dashboard/core/schemas/__init__.py +16 -0
- package/dashboard/core/schemas/base.py +43 -0
- package/dashboard/core/schemas/session_schema.py +40 -0
- package/dashboard/core/schemas/subagent_schema.py +23 -0
- package/dashboard/data/agent_config_manager.py +6 -4
- package/dashboard/data/chain_reader.py +16 -12
- package/dashboard/data/error_analyzer.py +15 -11
- package/dashboard/data/session_reader.py +268 -46
- package/dashboard/data/subagent_reader.py +74 -49
- package/dashboard/data/timeline_reader.py +35 -49
- package/dashboard/main.py +24 -2
- package/dashboard/mechanism_reader.py +4 -5
- package/dashboard/mechanisms.py +2 -2
- package/dashboard/pytest.ini +3 -0
- package/dashboard/requirements.txt +5 -0
- package/dashboard/status/cache_fp_probe.py +40 -0
- package/dashboard/status/status_cache.py +199 -72
- package/dashboard/status/status_calculator.py +50 -30
- package/dashboard/tests/conftest.py +87 -0
- package/dashboard/tests/test_api_contracts.py +372 -0
- package/dashboard/tests/test_bench_fortify.py +176 -0
- package/dashboard/tests/test_fortify.py +952 -0
- package/dashboard/utils/__init__.py +1 -0
- package/dashboard/utils/data_repair.py +210 -0
- package/dashboard/watchers/file_watcher.py +380 -77
- package/frontend-dist/assets/{index-cYIOn3Wq.css → index-BIZ2xHfw.css} +1 -1
- package/frontend-dist/assets/{index-DyRXGevD.js → index-Cnr0b02R.js} +1 -1
- package/frontend-dist/index.html +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dashboard/agents.py +0 -74
- package/dashboard/collaboration.py +0 -407
- package/dashboard/errors.py +0 -63
- package/dashboard/performance.py +0 -474
- package/dashboard/session_reader.py +0 -240
- package/dashboard/status_calculator.py +0 -121
- package/dashboard/subagent_reader.py +0 -232
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
会话读取器 - 读取 sessions/*.jsonl 和 sessions.json
|
|
3
3
|
"""
|
|
4
|
+
import hashlib
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
from pathlib import Path
|
|
@@ -8,9 +9,73 @@ from typing import List, Dict, Any, Optional
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
|
|
12
|
+
from utils.data_repair import parse_session_jsonl_line
|
|
11
13
|
|
|
12
14
|
_META_SESSION_INDEX_KEYS = frozenset({"entries", "version", "schema"})
|
|
13
15
|
|
|
16
|
+
# 校验报告:大文件仅哈希尾部,与 tail 读取策略一致;小文件全量哈希
|
|
17
|
+
_MAX_FULL_HASH_BYTES = 4 * 1024 * 1024
|
|
18
|
+
_TAIL_HASH_BYTES = 512 * 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compute_session_file_integrity(path: Path) -> Dict[str, Any]:
|
|
22
|
+
"""文件级完整性元数据:size、mtime、sha256(全文件或尾部窗口)。"""
|
|
23
|
+
try:
|
|
24
|
+
st = path.stat()
|
|
25
|
+
except OSError as e:
|
|
26
|
+
return {"path": str(path), "error": f"stat_failed:{e}"}
|
|
27
|
+
size = int(st.st_size)
|
|
28
|
+
mtime_ns = int(getattr(st, "st_mtime_ns", int(st.st_mtime * 1e9)))
|
|
29
|
+
out: Dict[str, Any] = {
|
|
30
|
+
"path": str(path.resolve()),
|
|
31
|
+
"size_bytes": size,
|
|
32
|
+
"mtime_ns": mtime_ns,
|
|
33
|
+
}
|
|
34
|
+
try:
|
|
35
|
+
if size == 0:
|
|
36
|
+
out["sha256"] = hashlib.sha256(b"").hexdigest()
|
|
37
|
+
out["hash_scope"] = "full"
|
|
38
|
+
return out
|
|
39
|
+
if size <= _MAX_FULL_HASH_BYTES:
|
|
40
|
+
with open(path, "rb") as f:
|
|
41
|
+
out["sha256"] = hashlib.sha256(f.read()).hexdigest()
|
|
42
|
+
out["hash_scope"] = "full"
|
|
43
|
+
else:
|
|
44
|
+
with open(path, "rb") as f:
|
|
45
|
+
f.seek(max(0, size - _TAIL_HASH_BYTES))
|
|
46
|
+
tail = f.read()
|
|
47
|
+
out["sha256"] = hashlib.sha256(tail).hexdigest()
|
|
48
|
+
out["hash_scope"] = "tail_512kb"
|
|
49
|
+
out["tail_hashed_bytes"] = len(tail)
|
|
50
|
+
except OSError as e:
|
|
51
|
+
out["error"] = f"read_failed:{e}"
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_validated_session_jsonl(agent_id: str, relative: str) -> Optional[Path]:
|
|
56
|
+
"""将相对路径解析为 agents/{id}/sessions 下的 .jsonl,禁止逃逸。"""
|
|
57
|
+
if not relative or not relative.strip():
|
|
58
|
+
return None
|
|
59
|
+
aid = normalize_openclaw_agent_id(agent_id)
|
|
60
|
+
base = get_openclaw_root() / "agents" / aid / "sessions"
|
|
61
|
+
if not base.is_dir():
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
base_r = base.resolve()
|
|
65
|
+
except OSError:
|
|
66
|
+
return None
|
|
67
|
+
rel = Path(relative.strip())
|
|
68
|
+
if rel.is_absolute() or ".." in rel.parts:
|
|
69
|
+
return None
|
|
70
|
+
try:
|
|
71
|
+
cand = (base_r / rel).resolve()
|
|
72
|
+
cand.relative_to(base_r)
|
|
73
|
+
except ValueError:
|
|
74
|
+
return None
|
|
75
|
+
if not cand.is_file() or cand.suffix.lower() != ".jsonl":
|
|
76
|
+
return None
|
|
77
|
+
return cand
|
|
78
|
+
|
|
14
79
|
|
|
15
80
|
def normalize_sessions_index(raw: Any) -> Dict[str, Dict[str, Any]]:
|
|
16
81
|
"""
|
|
@@ -68,6 +133,38 @@ def resolve_session_jsonl_path(sessions_dir: Path, entry: Dict[str, Any]) -> Opt
|
|
|
68
133
|
return None
|
|
69
134
|
|
|
70
135
|
|
|
136
|
+
def _load_sessions_index_file(sessions_index: Path) -> Optional[Dict[str, Any]]:
|
|
137
|
+
"""读取 sessions.json;可选 JSON Schema 校验(OPENCLAW_JSON_STRICT)。失败返回 None 并记录错误。"""
|
|
138
|
+
if not sessions_index.exists():
|
|
139
|
+
return None
|
|
140
|
+
try:
|
|
141
|
+
with open(sessions_index, "r", encoding="utf-8") as f:
|
|
142
|
+
data = json.load(f)
|
|
143
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
144
|
+
from core.error_handler import record_error
|
|
145
|
+
|
|
146
|
+
record_error("parsing-error", str(e), "sessions_index", exc=e)
|
|
147
|
+
return None
|
|
148
|
+
if not isinstance(data, dict):
|
|
149
|
+
from core.error_handler import record_error
|
|
150
|
+
|
|
151
|
+
record_error("validation-error", "sessions index root is not an object", "sessions_index")
|
|
152
|
+
return None
|
|
153
|
+
from core.config_fortify import get_fortify_config
|
|
154
|
+
from core.schemas.base import SchemaValidator
|
|
155
|
+
from core.schemas.session_schema import sessions_index_schema
|
|
156
|
+
|
|
157
|
+
cfg = get_fortify_config()
|
|
158
|
+
vr = SchemaValidator(sessions_index_schema, strict=cfg.json_strict).validate(data)
|
|
159
|
+
if not vr.is_valid:
|
|
160
|
+
from core.error_handler import record_error
|
|
161
|
+
|
|
162
|
+
record_error("validation-error", vr.error_message, "sessions_index")
|
|
163
|
+
if cfg.json_strict:
|
|
164
|
+
return None
|
|
165
|
+
return data
|
|
166
|
+
|
|
167
|
+
|
|
71
168
|
def get_agent_sessions_path(agent_id: str) -> Optional[Path]:
|
|
72
169
|
"""获取 Agent 的 sessions 目录"""
|
|
73
170
|
sessions_path = get_openclaw_root() / "agents" / normalize_openclaw_agent_id(agent_id) / "sessions"
|
|
@@ -125,12 +222,9 @@ def get_recent_messages(agent_id: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
|
125
222
|
line = line.strip()
|
|
126
223
|
if not line:
|
|
127
224
|
continue
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
messages.append(data.get('message', {}))
|
|
132
|
-
except json.JSONDecodeError:
|
|
133
|
-
continue
|
|
225
|
+
_, msg = parse_session_jsonl_line(line)
|
|
226
|
+
if msg is not None:
|
|
227
|
+
messages.append(msg)
|
|
134
228
|
# 必须取尾部:原先在扫描到 limit 条就 break,会拿到「窗口内较早」的消息而非最新,导致 tool/ thinking 误判
|
|
135
229
|
return messages[-limit:] if len(messages) > limit else messages
|
|
136
230
|
|
|
@@ -214,21 +308,17 @@ def get_session_updated_at(agent_id: str) -> int:
|
|
|
214
308
|
sessions_index = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
|
|
215
309
|
if not sessions_index.exists():
|
|
216
310
|
return 0
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
data = json.load(f)
|
|
221
|
-
if not isinstance(data, dict):
|
|
222
|
-
return 0
|
|
223
|
-
index_map = normalize_sessions_index(data)
|
|
224
|
-
max_ts = 0
|
|
225
|
-
for entry in index_map.values():
|
|
226
|
-
ts = entry.get('updatedAt') or entry.get('lastMessageAt') or 0
|
|
227
|
-
if isinstance(ts, (int, float)) and ts > max_ts:
|
|
228
|
-
max_ts = int(ts)
|
|
229
|
-
return max_ts
|
|
230
|
-
except (json.JSONDecodeError, IOError):
|
|
311
|
+
|
|
312
|
+
data = _load_sessions_index_file(sessions_index)
|
|
313
|
+
if not data:
|
|
231
314
|
return 0
|
|
315
|
+
index_map = normalize_sessions_index(data)
|
|
316
|
+
max_ts = 0
|
|
317
|
+
for entry in index_map.values():
|
|
318
|
+
ts = entry.get("updatedAt") or entry.get("lastMessageAt") or 0
|
|
319
|
+
if isinstance(ts, (int, float)) and ts > max_ts:
|
|
320
|
+
max_ts = int(ts)
|
|
321
|
+
return max_ts
|
|
232
322
|
|
|
233
323
|
|
|
234
324
|
def has_recent_session_activity(agent_id: str, minutes: int = 5) -> bool:
|
|
@@ -269,15 +359,12 @@ def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: i
|
|
|
269
359
|
session_file: Optional[Path] = None
|
|
270
360
|
sessions_path = get_openclaw_root() / "agents" / aid / "sessions"
|
|
271
361
|
if session_key:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
index_data = json.load(f)
|
|
362
|
+
index_data = _load_sessions_index_file(sessions_index)
|
|
363
|
+
if index_data:
|
|
275
364
|
index_map = normalize_sessions_index(index_data)
|
|
276
365
|
entry = index_map.get(session_key)
|
|
277
366
|
if entry:
|
|
278
367
|
session_file = resolve_session_jsonl_path(sessions_path, entry)
|
|
279
|
-
except (json.JSONDecodeError, IOError):
|
|
280
|
-
pass
|
|
281
368
|
|
|
282
369
|
if not session_file or not session_file.exists():
|
|
283
370
|
session_file = get_latest_session_file(agent_id)
|
|
@@ -290,19 +377,18 @@ def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: i
|
|
|
290
377
|
|
|
291
378
|
with open(session_file, 'r', encoding='utf-8') as f:
|
|
292
379
|
for line in f:
|
|
380
|
+
envelope, msg = parse_session_jsonl_line(line.strip())
|
|
381
|
+
if not envelope or envelope.get('type') != 'message' or msg is None:
|
|
382
|
+
continue
|
|
383
|
+
role = msg.get('role')
|
|
384
|
+
if not role:
|
|
385
|
+
continue
|
|
386
|
+
|
|
293
387
|
try:
|
|
294
|
-
data = json.loads(line.strip())
|
|
295
|
-
if data.get('type') != 'message':
|
|
296
|
-
continue
|
|
297
|
-
msg = data.get('message', {})
|
|
298
|
-
role = msg.get('role')
|
|
299
|
-
if not role:
|
|
300
|
-
continue
|
|
301
|
-
|
|
302
388
|
turn: Dict[str, Any] = {
|
|
303
389
|
'turnIndex': turn_index,
|
|
304
390
|
'role': role,
|
|
305
|
-
'timestamp': msg.get('timestamp') or
|
|
391
|
+
'timestamp': msg.get('timestamp') or envelope.get('timestamp'),
|
|
306
392
|
'content': [],
|
|
307
393
|
'usage': msg.get('usage'),
|
|
308
394
|
'stopReason': msg.get('stopReason'),
|
|
@@ -350,8 +436,8 @@ def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: i
|
|
|
350
436
|
|
|
351
437
|
turns.append(turn)
|
|
352
438
|
turn_index += 1
|
|
353
|
-
|
|
354
|
-
except (
|
|
439
|
+
|
|
440
|
+
except (KeyError, TypeError):
|
|
355
441
|
continue
|
|
356
442
|
|
|
357
443
|
return turns[-limit:] if len(turns) > limit else turns
|
|
@@ -471,17 +557,14 @@ def get_recent_messages_with_timestamp(agent_id: str, limit: int = 10) -> List[D
|
|
|
471
557
|
line = line.strip()
|
|
472
558
|
if not line:
|
|
473
559
|
continue
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if data.get('type') == 'message':
|
|
477
|
-
msg = data.get('message', {})
|
|
478
|
-
messages.append({
|
|
479
|
-
'message': msg,
|
|
480
|
-
'timestamp': msg.get('timestamp', 0),
|
|
481
|
-
'data_timestamp': data.get('timestamp', ''),
|
|
482
|
-
})
|
|
483
|
-
except json.JSONDecodeError:
|
|
560
|
+
env, msg = parse_session_jsonl_line(line)
|
|
561
|
+
if msg is None:
|
|
484
562
|
continue
|
|
563
|
+
messages.append({
|
|
564
|
+
'message': msg,
|
|
565
|
+
'timestamp': msg.get('timestamp', 0),
|
|
566
|
+
'data_timestamp': (env or {}).get('timestamp', ''),
|
|
567
|
+
})
|
|
485
568
|
|
|
486
569
|
return messages[-limit:] if len(messages) > limit else messages
|
|
487
570
|
|
|
@@ -545,3 +628,142 @@ def get_pending_tool_call_with_timestamp(agent_id: str) -> Optional[Dict[str, An
|
|
|
545
628
|
return tool_calls[last_id]
|
|
546
629
|
|
|
547
630
|
return None
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def get_session_validation_report(
|
|
634
|
+
agent_id: str,
|
|
635
|
+
*,
|
|
636
|
+
relative_session_file: Optional[str] = None,
|
|
637
|
+
auto_repair: Optional[bool] = None,
|
|
638
|
+
include_details: bool = False,
|
|
639
|
+
max_lines: int = 1000,
|
|
640
|
+
) -> Dict[str, Any]:
|
|
641
|
+
"""Validate session JSONL for an agent; used by GET /api/data/validate."""
|
|
642
|
+
from core.config_fortify import get_fortify_config
|
|
643
|
+
|
|
644
|
+
aid = normalize_openclaw_agent_id(agent_id)
|
|
645
|
+
sessions_dir = get_openclaw_root() / "agents" / aid / "sessions"
|
|
646
|
+
sessions_index_path = sessions_dir / "sessions.json"
|
|
647
|
+
cfg = get_fortify_config()
|
|
648
|
+
read_path_policy = {
|
|
649
|
+
"memory_auto_repair_default": cfg.auto_repair_json,
|
|
650
|
+
"disk_write_back_enabled": cfg.auto_repair_write_back,
|
|
651
|
+
"note": "本报告仅校验与统计;读路径不自动写盘,写回仅在显式修复工具中且受 OPENCLAW_AUTO_REPAIR_WB 控制。",
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
session_file: Optional[Path] = None
|
|
655
|
+
if relative_session_file and relative_session_file.strip():
|
|
656
|
+
session_file = resolve_validated_session_jsonl(agent_id, relative_session_file)
|
|
657
|
+
if not session_file:
|
|
658
|
+
return {
|
|
659
|
+
"agent_id": agent_id,
|
|
660
|
+
"validation_passed": False,
|
|
661
|
+
"sessions_index_path": str(sessions_index_path)
|
|
662
|
+
if sessions_index_path.exists()
|
|
663
|
+
else None,
|
|
664
|
+
"session_file": None,
|
|
665
|
+
"session_file_query": relative_session_file.strip(),
|
|
666
|
+
"file_integrity": None,
|
|
667
|
+
"read_path_policy": read_path_policy,
|
|
668
|
+
"total_lines": 0,
|
|
669
|
+
"valid_messages": 0,
|
|
670
|
+
"repaired_messages": 0,
|
|
671
|
+
"errors": [
|
|
672
|
+
{
|
|
673
|
+
"type": "invalid_session_file",
|
|
674
|
+
"message": "path not found, not .jsonl, or escapes sessions dir",
|
|
675
|
+
}
|
|
676
|
+
],
|
|
677
|
+
"repair_report": {
|
|
678
|
+
"repaired_count": 0,
|
|
679
|
+
"repair_success_rate": 1.0,
|
|
680
|
+
"failed_repairs": [],
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
else:
|
|
684
|
+
session_file = get_latest_session_file(agent_id)
|
|
685
|
+
|
|
686
|
+
if not session_file:
|
|
687
|
+
return {
|
|
688
|
+
"agent_id": agent_id,
|
|
689
|
+
"validation_passed": True,
|
|
690
|
+
"sessions_index_path": str(sessions_index_path)
|
|
691
|
+
if sessions_index_path.exists()
|
|
692
|
+
else None,
|
|
693
|
+
"session_file": None,
|
|
694
|
+
"session_file_query": None,
|
|
695
|
+
"file_integrity": None,
|
|
696
|
+
"read_path_policy": read_path_policy,
|
|
697
|
+
"total_lines": 0,
|
|
698
|
+
"valid_messages": 0,
|
|
699
|
+
"repaired_messages": 0,
|
|
700
|
+
"errors": [],
|
|
701
|
+
"repair_report": {"repaired_count": 0, "repair_success_rate": 1.0, "failed_repairs": []},
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
raw_lines = _read_tail_lines(session_file, max_lines)
|
|
705
|
+
errors: List[Dict[str, Any]] = []
|
|
706
|
+
valid_messages = 0
|
|
707
|
+
repaired_messages = 0
|
|
708
|
+
failed_repairs: List[Dict[str, Any]] = []
|
|
709
|
+
|
|
710
|
+
for i, line in enumerate(raw_lines):
|
|
711
|
+
stripped = line.strip()
|
|
712
|
+
if not stripped:
|
|
713
|
+
continue
|
|
714
|
+
env_probe, _ = parse_session_jsonl_line(
|
|
715
|
+
stripped, auto_repair=False, json_strict=False
|
|
716
|
+
)
|
|
717
|
+
if env_probe and env_probe.get("type") != "message":
|
|
718
|
+
continue
|
|
719
|
+
_env_strict, msg_strict = parse_session_jsonl_line(
|
|
720
|
+
stripped, auto_repair=False, json_strict=True
|
|
721
|
+
)
|
|
722
|
+
if msg_strict is not None:
|
|
723
|
+
valid_messages += 1
|
|
724
|
+
continue
|
|
725
|
+
_env_loose, msg_loose = parse_session_jsonl_line(
|
|
726
|
+
stripped,
|
|
727
|
+
auto_repair=auto_repair if auto_repair is not None else True,
|
|
728
|
+
json_strict=False,
|
|
729
|
+
)
|
|
730
|
+
if msg_loose is not None:
|
|
731
|
+
repaired_messages += 1
|
|
732
|
+
else:
|
|
733
|
+
failed_repairs.append({"line_index": i, "reason": "unparseable_or_invalid_schema"})
|
|
734
|
+
if include_details:
|
|
735
|
+
errors.append(
|
|
736
|
+
{
|
|
737
|
+
"type": "validation_failed",
|
|
738
|
+
"line_index_in_tail_window": i,
|
|
739
|
+
"reason": "unparseable_or_invalid_schema",
|
|
740
|
+
"sample": stripped[:200],
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
total_non_empty = sum(1 for ln in raw_lines if ln.strip())
|
|
745
|
+
validation_passed = len(failed_repairs) == 0
|
|
746
|
+
denom = repaired_messages + valid_messages
|
|
747
|
+
rate = 1.0 if denom == 0 else repaired_messages / max(denom, 1)
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
"agent_id": agent_id,
|
|
751
|
+
"validation_passed": validation_passed,
|
|
752
|
+
"sessions_index_path": str(sessions_index_path)
|
|
753
|
+
if sessions_index_path.exists()
|
|
754
|
+
else None,
|
|
755
|
+
"session_file": str(session_file.resolve()),
|
|
756
|
+
"session_file_query": relative_session_file.strip() if relative_session_file else None,
|
|
757
|
+
"file_integrity": compute_session_file_integrity(session_file),
|
|
758
|
+
"read_path_policy": read_path_policy,
|
|
759
|
+
"tail_lines_scanned": max_lines,
|
|
760
|
+
"total_lines": total_non_empty,
|
|
761
|
+
"valid_messages": valid_messages,
|
|
762
|
+
"repaired_messages": repaired_messages,
|
|
763
|
+
"errors": errors,
|
|
764
|
+
"repair_report": {
|
|
765
|
+
"repaired_count": repaired_messages,
|
|
766
|
+
"repair_success_rate": rate,
|
|
767
|
+
"failed_repairs": failed_repairs,
|
|
768
|
+
},
|
|
769
|
+
}
|
|
@@ -6,7 +6,16 @@ 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, normalize_openclaw_agent_id
|
|
9
|
-
from data.session_reader import
|
|
9
|
+
from data.session_reader import (
|
|
10
|
+
normalize_sessions_index,
|
|
11
|
+
resolve_session_jsonl_path,
|
|
12
|
+
_load_sessions_index_file,
|
|
13
|
+
)
|
|
14
|
+
from core.config_fortify import get_fortify_config
|
|
15
|
+
from core.error_handler import record_error
|
|
16
|
+
from core.schemas.base import SchemaValidator
|
|
17
|
+
from core.schemas.subagent_schema import subagent_runs_root_schema
|
|
18
|
+
from utils.data_repair import parse_session_jsonl_line
|
|
10
19
|
|
|
11
20
|
|
|
12
21
|
def load_subagent_runs() -> List[Dict[str, Any]]:
|
|
@@ -17,13 +26,35 @@ def load_subagent_runs() -> List[Dict[str, Any]]:
|
|
|
17
26
|
runs_path = get_openclaw_root() / "subagents" / "runs.json"
|
|
18
27
|
if not runs_path.exists():
|
|
19
28
|
return []
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
with open(runs_path, 'r', encoding='utf-8') as f:
|
|
32
|
+
data = json.load(f)
|
|
33
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
34
|
+
record_error("parsing-error", str(e), "subagent_runs")
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
cfg = get_fortify_config()
|
|
41
|
+
vr = SchemaValidator(subagent_runs_root_schema, strict=cfg.json_strict).validate(data)
|
|
42
|
+
if not vr.is_valid:
|
|
43
|
+
record_error("validation-error", vr.error_message, "subagent_runs")
|
|
44
|
+
if cfg.json_strict:
|
|
45
|
+
return []
|
|
46
|
+
|
|
24
47
|
runs = data.get('runs', {})
|
|
25
48
|
if isinstance(runs, dict):
|
|
26
|
-
|
|
49
|
+
out: List[Dict[str, Any]] = []
|
|
50
|
+
for run_id, rec in runs.items():
|
|
51
|
+
if not isinstance(rec, dict):
|
|
52
|
+
continue
|
|
53
|
+
merged = dict(rec)
|
|
54
|
+
if not merged.get('runId'):
|
|
55
|
+
merged['runId'] = run_id
|
|
56
|
+
out.append(merged)
|
|
57
|
+
return out
|
|
27
58
|
return runs if isinstance(runs, list) else []
|
|
28
59
|
|
|
29
60
|
|
|
@@ -118,8 +149,9 @@ def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) ->
|
|
|
118
149
|
return None
|
|
119
150
|
|
|
120
151
|
try:
|
|
121
|
-
|
|
122
|
-
|
|
152
|
+
index_data = _load_sessions_index_file(sessions_index)
|
|
153
|
+
if not index_data:
|
|
154
|
+
return None
|
|
123
155
|
index_map = normalize_sessions_index(index_data)
|
|
124
156
|
entry = index_map.get(child_session_key)
|
|
125
157
|
if not entry:
|
|
@@ -132,22 +164,18 @@ def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) ->
|
|
|
132
164
|
last_text = None
|
|
133
165
|
with open(session_path, 'r', encoding='utf-8') as f:
|
|
134
166
|
for line in f:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if data.get('type') != 'message':
|
|
138
|
-
continue
|
|
139
|
-
msg = data.get('message', {})
|
|
140
|
-
if msg.get('role') != 'assistant':
|
|
141
|
-
continue
|
|
142
|
-
content = msg.get('content', [])
|
|
143
|
-
for c in content:
|
|
144
|
-
if isinstance(c, dict) and c.get('type') == 'text':
|
|
145
|
-
text = c.get('text', '')
|
|
146
|
-
if text and text.strip():
|
|
147
|
-
last_text = text
|
|
148
|
-
break
|
|
149
|
-
except (json.JSONDecodeError, KeyError):
|
|
167
|
+
envelope, msg = parse_session_jsonl_line(line)
|
|
168
|
+
if not envelope or envelope.get('type') != 'message' or msg is None:
|
|
150
169
|
continue
|
|
170
|
+
if msg.get('role') != 'assistant':
|
|
171
|
+
continue
|
|
172
|
+
content = msg.get('content', [])
|
|
173
|
+
for c in content:
|
|
174
|
+
if isinstance(c, dict) and c.get('type') == 'text':
|
|
175
|
+
text = c.get('text', '')
|
|
176
|
+
if text and text.strip():
|
|
177
|
+
last_text = text
|
|
178
|
+
break
|
|
151
179
|
|
|
152
180
|
if not last_text or not last_text.strip():
|
|
153
181
|
return None
|
|
@@ -155,7 +183,7 @@ def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) ->
|
|
|
155
183
|
return last_text[:max_chars] + '\n\n...(输出已截断)'
|
|
156
184
|
return last_text
|
|
157
185
|
except Exception as e:
|
|
158
|
-
|
|
186
|
+
record_error("io-error", str(e), "subagent_reader:get_agent_output_for_run", exc=e)
|
|
159
187
|
return None
|
|
160
188
|
|
|
161
189
|
|
|
@@ -180,8 +208,9 @@ def get_agent_files_for_run(child_session_key: str) -> List[str]:
|
|
|
180
208
|
return []
|
|
181
209
|
|
|
182
210
|
try:
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
index_data = _load_sessions_index_file(sessions_index)
|
|
212
|
+
if not index_data:
|
|
213
|
+
return []
|
|
185
214
|
index_map = normalize_sessions_index(index_data)
|
|
186
215
|
entry = index_map.get(child_session_key)
|
|
187
216
|
if not entry:
|
|
@@ -196,31 +225,27 @@ def get_agent_files_for_run(child_session_key: str) -> List[str]:
|
|
|
196
225
|
|
|
197
226
|
with open(session_path, 'r', encoding='utf-8') as f:
|
|
198
227
|
for line in f:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
228
|
+
envelope, msg = parse_session_jsonl_line(line)
|
|
229
|
+
if not envelope or envelope.get('type') != 'message' or msg is None:
|
|
230
|
+
continue
|
|
231
|
+
if msg.get('role') != 'assistant':
|
|
232
|
+
continue
|
|
233
|
+
content = msg.get('content', [])
|
|
234
|
+
for c in content:
|
|
235
|
+
if not isinstance(c, dict) or c.get('type') != 'toolCall':
|
|
202
236
|
continue
|
|
203
|
-
|
|
204
|
-
if
|
|
237
|
+
name = c.get('name', '')
|
|
238
|
+
if name not in file_tools:
|
|
205
239
|
continue
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
args = c.get('arguments', {})
|
|
241
|
+
if isinstance(args, str):
|
|
242
|
+
try:
|
|
243
|
+
args = json.loads(args)
|
|
244
|
+
except json.JSONDecodeError:
|
|
209
245
|
continue
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
args = c.get('arguments', {})
|
|
214
|
-
if isinstance(args, str):
|
|
215
|
-
try:
|
|
216
|
-
args = json.loads(args)
|
|
217
|
-
except json.JSONDecodeError:
|
|
218
|
-
continue
|
|
219
|
-
path = args.get('path') or args.get('file_path')
|
|
220
|
-
if path and isinstance(path, str) and path.strip():
|
|
221
|
-
file_paths.append(path.strip())
|
|
222
|
-
except (json.JSONDecodeError, KeyError):
|
|
223
|
-
continue
|
|
246
|
+
path = args.get('path') or args.get('file_path')
|
|
247
|
+
if path and isinstance(path, str) and path.strip():
|
|
248
|
+
file_paths.append(path.strip())
|
|
224
249
|
|
|
225
250
|
# 去重并保持顺序
|
|
226
251
|
seen = set()
|
|
@@ -231,5 +256,5 @@ def get_agent_files_for_run(child_session_key: str) -> List[str]:
|
|
|
231
256
|
result.append(p)
|
|
232
257
|
return result
|
|
233
258
|
except Exception as e:
|
|
234
|
-
|
|
259
|
+
record_error("io-error", str(e), "subagent_reader:get_agent_files_for_run", exc=e)
|
|
235
260
|
return []
|