openclaw-agent-dashboard 1.0.39 → 1.0.40
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 +27 -11
- package/dashboard/api/fortify_routes.py +80 -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 +112 -0
- package/dashboard/core/error_handler.py +339 -0
- package/dashboard/core/fallback_manager.py +70 -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 +84 -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 +741 -0
- package/dashboard/utils/__init__.py +1 -0
- package/dashboard/utils/data_repair.py +210 -0
- package/dashboard/watchers/file_watcher.py +367 -77
- 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
|
@@ -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 []
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
时序数据读取器 - 将 session jsonl 解析为可视化时序步骤
|
|
3
3
|
"""
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
5
|
import os
|
|
7
6
|
from functools import lru_cache
|
|
@@ -92,7 +91,13 @@ class TimelineStep:
|
|
|
92
91
|
|
|
93
92
|
|
|
94
93
|
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id, agent_ids_equal
|
|
95
|
-
from data.session_reader import
|
|
94
|
+
from data.session_reader import (
|
|
95
|
+
normalize_sessions_index,
|
|
96
|
+
resolve_session_jsonl_path,
|
|
97
|
+
_load_sessions_index_file,
|
|
98
|
+
)
|
|
99
|
+
from data.subagent_reader import load_subagent_runs
|
|
100
|
+
from utils.data_repair import parse_session_jsonl_line
|
|
96
101
|
|
|
97
102
|
|
|
98
103
|
def _read_session_header_timestamp(path: Path) -> Optional[int]:
|
|
@@ -102,10 +107,10 @@ def _read_session_header_timestamp(path: Path) -> Optional[int]:
|
|
|
102
107
|
first = f.readline()
|
|
103
108
|
if not first.strip():
|
|
104
109
|
return None
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
return _parse_timestamp(
|
|
108
|
-
except (
|
|
110
|
+
envelope, _ = parse_session_jsonl_line(first.strip())
|
|
111
|
+
if envelope and envelope.get('type') == 'session':
|
|
112
|
+
return _parse_timestamp(envelope.get('timestamp', 0))
|
|
113
|
+
except (OSError, IOError):
|
|
109
114
|
pass
|
|
110
115
|
return None
|
|
111
116
|
|
|
@@ -260,17 +265,11 @@ def get_subagent_runs() -> Dict[str, List[Dict]]:
|
|
|
260
265
|
|
|
261
266
|
@lru_cache(maxsize=16)
|
|
262
267
|
def _get_subagent_runs_cached(mtime: float) -> Dict[str, List[Dict]]:
|
|
263
|
-
runs_file = get_openclaw_root() / "subagents" / "runs.json"
|
|
264
|
-
try:
|
|
265
|
-
with open(runs_file, 'r', encoding='utf-8') as f:
|
|
266
|
-
data = json.load(f)
|
|
267
|
-
except (json.JSONDecodeError, IOError, OSError):
|
|
268
|
-
return {}
|
|
269
268
|
runs_by_agent: Dict[str, List[Dict]] = {}
|
|
270
|
-
|
|
271
|
-
for run_id, run_info in runs.items():
|
|
269
|
+
for run_info in load_subagent_runs():
|
|
272
270
|
if not isinstance(run_info, dict):
|
|
273
271
|
continue
|
|
272
|
+
run_id = str(run_info.get('runId', ''))
|
|
274
273
|
child_key = run_info.get('childSessionKey', '')
|
|
275
274
|
if ':' in child_key:
|
|
276
275
|
parts = child_key.split(':')
|
|
@@ -297,9 +296,11 @@ def _get_requester_info_for_session(agent_id: str, session_key: Optional[str]) -
|
|
|
297
296
|
sessions_index = get_openclaw_root() / f"agents/{state_id}/sessions/sessions.json"
|
|
298
297
|
if sessions_index.exists():
|
|
299
298
|
try:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
299
|
+
index_data = _load_sessions_index_file(sessions_index)
|
|
300
|
+
if not index_data:
|
|
301
|
+
index_map = {}
|
|
302
|
+
else:
|
|
303
|
+
index_map = normalize_sessions_index(index_data)
|
|
303
304
|
if not session_key:
|
|
304
305
|
entries = list(index_map.items())
|
|
305
306
|
if entries:
|
|
@@ -328,14 +329,8 @@ def _get_requester_info_for_session(agent_id: str, session_key: Optional[str]) -
|
|
|
328
329
|
session_key = runs[0].get('childSessionKey')
|
|
329
330
|
if not session_key:
|
|
330
331
|
return {}
|
|
331
|
-
runs_file = get_openclaw_root() / "subagents" / "runs.json"
|
|
332
|
-
if not runs_file.exists():
|
|
333
|
-
return {}
|
|
334
332
|
try:
|
|
335
|
-
|
|
336
|
-
data = json.load(f)
|
|
337
|
-
runs = data.get('runs', {})
|
|
338
|
-
for run_id, run_info in runs.items():
|
|
333
|
+
for run_info in load_subagent_runs():
|
|
339
334
|
if not isinstance(run_info, dict):
|
|
340
335
|
continue
|
|
341
336
|
child_key = run_info.get('childSessionKey', '')
|
|
@@ -612,10 +607,10 @@ def resolve_agent_session_jsonl(
|
|
|
612
607
|
index_path = sessions_path / "sessions.json"
|
|
613
608
|
index_map: Dict[str, Dict[str, Any]] = {}
|
|
614
609
|
if index_path.exists():
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
610
|
+
raw = _load_sessions_index_file(index_path)
|
|
611
|
+
if raw:
|
|
612
|
+
index_map = normalize_sessions_index(raw)
|
|
613
|
+
else:
|
|
619
614
|
index_map = {}
|
|
620
615
|
|
|
621
616
|
prefix = f"agent:{state_id}:"
|
|
@@ -895,17 +890,13 @@ def _extract_subagent_steps_from_main_lines(
|
|
|
895
890
|
for line in lines:
|
|
896
891
|
if '"type":"message"' not in line and '"type": "message"' not in line:
|
|
897
892
|
continue
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
except json.JSONDecodeError:
|
|
901
|
-
continue
|
|
902
|
-
if data.get('type') != 'message':
|
|
893
|
+
envelope, msg = parse_session_jsonl_line(line)
|
|
894
|
+
if envelope is None or envelope.get('type') != 'message' or msg is None:
|
|
903
895
|
continue
|
|
904
|
-
msg = data.get('message', {})
|
|
905
896
|
role = msg.get('role')
|
|
906
897
|
if not role:
|
|
907
898
|
continue
|
|
908
|
-
timestamp = _parse_timestamp(msg.get('timestamp') or
|
|
899
|
+
timestamp = _parse_timestamp(msg.get('timestamp') or envelope.get('timestamp', 0))
|
|
909
900
|
duration = 0
|
|
910
901
|
if last_timestamp and timestamp:
|
|
911
902
|
duration = timestamp - last_timestamp
|
|
@@ -1112,21 +1103,19 @@ def _parse_session_lines(
|
|
|
1112
1103
|
sender_id = requester_info.get('senderId') if requester_info else None
|
|
1113
1104
|
sender_name = requester_info.get('senderName') if requester_info else None
|
|
1114
1105
|
for line in lines:
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
except json.JSONDecodeError:
|
|
1106
|
+
envelope, msg = parse_session_jsonl_line(line)
|
|
1107
|
+
if envelope is None:
|
|
1118
1108
|
continue
|
|
1119
|
-
msg_type =
|
|
1109
|
+
msg_type = envelope.get('type')
|
|
1120
1110
|
if msg_type == 'session':
|
|
1121
|
-
started_at = _parse_timestamp(
|
|
1111
|
+
started_at = _parse_timestamp(envelope.get('timestamp', 0))
|
|
1122
1112
|
continue
|
|
1123
|
-
if msg_type != 'message':
|
|
1113
|
+
if msg_type != 'message' or msg is None:
|
|
1124
1114
|
continue
|
|
1125
|
-
msg = data.get('message', {})
|
|
1126
1115
|
role = msg.get('role')
|
|
1127
1116
|
if not role:
|
|
1128
1117
|
continue
|
|
1129
|
-
timestamp = _parse_timestamp(msg.get('timestamp') or
|
|
1118
|
+
timestamp = _parse_timestamp(msg.get('timestamp') or envelope.get('timestamp', 0))
|
|
1130
1119
|
duration = 0
|
|
1131
1120
|
if last_timestamp and timestamp:
|
|
1132
1121
|
duration = timestamp - last_timestamp
|
|
@@ -1295,13 +1284,10 @@ def _line_index_of_first_user_message(path: Path) -> Optional[int]:
|
|
|
1295
1284
|
for i, line in enumerate(f):
|
|
1296
1285
|
if '"role"' not in line or 'user' not in line:
|
|
1297
1286
|
continue
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
except json.JSONDecodeError:
|
|
1301
|
-
continue
|
|
1302
|
-
if d.get('type') != 'message':
|
|
1287
|
+
env, msg = parse_session_jsonl_line(line)
|
|
1288
|
+
if env is None or env.get('type') != 'message' or msg is None:
|
|
1303
1289
|
continue
|
|
1304
|
-
if
|
|
1290
|
+
if msg.get('role') == 'user':
|
|
1305
1291
|
return i
|
|
1306
1292
|
except (OSError, IOError):
|
|
1307
1293
|
pass
|
package/dashboard/main.py
CHANGED
|
@@ -13,14 +13,35 @@ import asyncio
|
|
|
13
13
|
async def lifespan(app: FastAPI):
|
|
14
14
|
"""应用生命周期:启动时启动文件监听,关闭时停止"""
|
|
15
15
|
loop = asyncio.get_running_loop()
|
|
16
|
+
probe_stop = None
|
|
16
17
|
try:
|
|
17
18
|
from watchers.file_watcher import start_file_watcher
|
|
19
|
+
from core.config_fortify import get_fortify_config
|
|
20
|
+
|
|
18
21
|
start_file_watcher(loop)
|
|
22
|
+
cfg = get_fortify_config()
|
|
23
|
+
if cfg.cache_preload:
|
|
24
|
+
try:
|
|
25
|
+
from status.status_calculator import get_agents_with_status
|
|
26
|
+
|
|
27
|
+
get_agents_with_status()
|
|
28
|
+
except Exception as e:
|
|
29
|
+
from core.error_handler import record_error
|
|
30
|
+
|
|
31
|
+
record_error("unknown", str(e), "main:cache_preload", exc=e)
|
|
32
|
+
from status.cache_fp_probe import start_cache_fp_probe_background
|
|
33
|
+
|
|
34
|
+
probe_stop = start_cache_fp_probe_background()
|
|
19
35
|
except Exception as e:
|
|
20
|
-
|
|
36
|
+
from core.error_handler import record_error
|
|
37
|
+
|
|
38
|
+
record_error("unknown", str(e), "main:file_watcher_start", exc=e)
|
|
21
39
|
yield
|
|
22
40
|
try:
|
|
41
|
+
if probe_stop is not None:
|
|
42
|
+
probe_stop.set()
|
|
23
43
|
from watchers.file_watcher import stop_file_watcher
|
|
44
|
+
|
|
24
45
|
stop_file_watcher()
|
|
25
46
|
except Exception:
|
|
26
47
|
pass
|
|
@@ -47,11 +68,12 @@ app.add_middleware(
|
|
|
47
68
|
import sys
|
|
48
69
|
sys.path.append(str(Path(__file__).parent))
|
|
49
70
|
|
|
50
|
-
from api import agents, subagents, websocket, performance, collaboration, agents_config, errors, timeline, chains, agent_config_api, error_analysis, debug_paths, version
|
|
71
|
+
from api import agents, subagents, websocket, performance, collaboration, agents_config, errors, timeline, chains, agent_config_api, error_analysis, debug_paths, version, fortify_routes
|
|
51
72
|
|
|
52
73
|
# 注册 API 路由
|
|
53
74
|
app.include_router(agents.router, prefix="/api", tags=["agents"])
|
|
54
75
|
app.include_router(errors.router, prefix="/api", tags=["errors"])
|
|
76
|
+
app.include_router(fortify_routes.router, prefix="/api", tags=["fortify"])
|
|
55
77
|
app.include_router(agents_config.router, prefix="/api", tags=["agents-config"])
|
|
56
78
|
app.include_router(subagents.router, prefix="/api", tags=["subagents"])
|
|
57
79
|
app.include_router(websocket.router, tags=["websocket"])
|
|
@@ -7,7 +7,7 @@ from typing import Dict, Any, List
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
|
|
10
|
-
from data.session_reader import normalize_sessions_index
|
|
10
|
+
from data.session_reader import normalize_sessions_index, _load_sessions_index_file
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def get_agent_mechanisms(agent_id: str) -> Dict[str, Any]:
|
|
@@ -30,9 +30,8 @@ def get_agent_mechanisms(agent_id: str) -> Dict[str, Any]:
|
|
|
30
30
|
return result
|
|
31
31
|
|
|
32
32
|
try:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if not isinstance(data, dict):
|
|
33
|
+
data = _load_sessions_index_file(sessions_index)
|
|
34
|
+
if not data:
|
|
36
35
|
return result
|
|
37
36
|
|
|
38
37
|
# 取最新 session 的机制信息(兼容 sessions.json 顶层或 entries 嵌套)
|
|
@@ -129,6 +128,6 @@ def get_agent_mechanisms(agent_id: str) -> Dict[str, Any]:
|
|
|
129
128
|
|
|
130
129
|
def get_all_agents_mechanisms() -> List[Dict[str, Any]]:
|
|
131
130
|
"""获取所有 Agent 的机制使用情况"""
|
|
132
|
-
from .config_reader import get_agents_list
|
|
131
|
+
from data.config_reader import get_agents_list
|
|
133
132
|
agents = get_agents_list()
|
|
134
133
|
return [get_agent_mechanisms(a.get('id', '')) for a in agents if a.get('id')]
|
package/dashboard/mechanisms.py
CHANGED
|
@@ -14,14 +14,14 @@ router = APIRouter()
|
|
|
14
14
|
@router.get("/mechanisms")
|
|
15
15
|
async def get_mechanisms():
|
|
16
16
|
"""获取所有 Agent 的机制使用情况"""
|
|
17
|
-
from
|
|
17
|
+
from mechanism_reader import get_all_agents_mechanisms
|
|
18
18
|
return get_all_agents_mechanisms()
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@router.get("/mechanisms/{agent_id}")
|
|
22
22
|
async def get_agent_mechanisms(agent_id: str):
|
|
23
23
|
"""获取单个 Agent 的机制使用情况"""
|
|
24
|
-
from
|
|
24
|
+
from mechanism_reader import get_agent_mechanisms
|
|
25
25
|
from data.config_reader import get_agent_config
|
|
26
26
|
|
|
27
27
|
if not get_agent_config(agent_id):
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""可选后台线程:周期性对 StatusCache 做 mtime 双验证剔除(RISK-004)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
_LOG = logging.getLogger("openclaw.fortify.cache_probe")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def start_cache_fp_probe_background() -> Optional[threading.Event]:
|
|
12
|
+
"""
|
|
13
|
+
若 OPENCLAW_CACHE_FP_PROBE_INTERVAL > 0,启动守护线程并返回用于停止的 Event;
|
|
14
|
+
否则返回 None。
|
|
15
|
+
"""
|
|
16
|
+
from core.config_fortify import get_fortify_config
|
|
17
|
+
|
|
18
|
+
interval = get_fortify_config().cache_fp_probe_interval_sec
|
|
19
|
+
if interval <= 0:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
stop = threading.Event()
|
|
23
|
+
|
|
24
|
+
def loop() -> None:
|
|
25
|
+
from status.status_cache import get_cache
|
|
26
|
+
|
|
27
|
+
while not stop.is_set():
|
|
28
|
+
if stop.wait(timeout=interval):
|
|
29
|
+
break
|
|
30
|
+
try:
|
|
31
|
+
n = get_cache().invalidate_stale_fp_entries()
|
|
32
|
+
if n:
|
|
33
|
+
_LOG.info("cache_fp_probe removed %s stale cache entries", n)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
from core.error_handler import record_error
|
|
36
|
+
|
|
37
|
+
record_error("unknown", str(e), "cache_fp_probe", exc=e)
|
|
38
|
+
|
|
39
|
+
threading.Thread(target=loop, daemon=True, name="openclaw_cache_fp_probe").start()
|
|
40
|
+
return stop
|