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
package/dashboard/performance.py
DELETED
|
@@ -1,474 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
性能监控 - 真实 TPM/RPM 统计(逐条消息解析)
|
|
3
|
-
支持按分钟查看调用详情,便于分析调用瓶颈
|
|
4
|
-
"""
|
|
5
|
-
from fastapi import APIRouter
|
|
6
|
-
from typing import List, Dict, Any, Optional
|
|
7
|
-
import json
|
|
8
|
-
import re
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from datetime import datetime, timedelta, timezone
|
|
11
|
-
from zoneinfo import ZoneInfo
|
|
12
|
-
|
|
13
|
-
# 详情展示使用 Asia/Shanghai 时区
|
|
14
|
-
TZ_DISPLAY = ZoneInfo('Asia/Shanghai')
|
|
15
|
-
|
|
16
|
-
router = APIRouter()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _extract_trigger_text(msg: Dict) -> str:
|
|
20
|
-
"""从消息中提取触发内容(完整展示)"""
|
|
21
|
-
content = msg.get('content') or []
|
|
22
|
-
if isinstance(content, str):
|
|
23
|
-
return content.replace('\n', ' ')
|
|
24
|
-
if not isinstance(content, list):
|
|
25
|
-
return ''
|
|
26
|
-
for item in content:
|
|
27
|
-
if isinstance(item, dict):
|
|
28
|
-
if item.get('type') == 'text' and item.get('text'):
|
|
29
|
-
text = str(item['text'])
|
|
30
|
-
if '[Subagent Task]' in text:
|
|
31
|
-
m = re.search(r'\*\*任务[::]\s*(.+?)\*\*', text)
|
|
32
|
-
if m:
|
|
33
|
-
return f"子任务: {m.group(1).strip()}"
|
|
34
|
-
return text.replace('\n', ' ')
|
|
35
|
-
if item.get('type') == 'toolCall':
|
|
36
|
-
return f"工具调用: {item.get('name', '?')}"
|
|
37
|
-
return ''
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _extract_tool_call_detail(msg: Dict, tool_call_id: str) -> str:
|
|
41
|
-
"""从 assistant 消息的 content 中提取 toolCall 的 arguments 详情"""
|
|
42
|
-
content = msg.get('content') or []
|
|
43
|
-
if not isinstance(content, list):
|
|
44
|
-
return ''
|
|
45
|
-
for item in content:
|
|
46
|
-
if not isinstance(item, dict):
|
|
47
|
-
continue
|
|
48
|
-
if item.get('type') == 'toolCall' and item.get('id') == tool_call_id:
|
|
49
|
-
name = item.get('name', '')
|
|
50
|
-
args = item.get('arguments') or {}
|
|
51
|
-
if isinstance(args, str):
|
|
52
|
-
try:
|
|
53
|
-
args = json.loads(args)
|
|
54
|
-
except Exception:
|
|
55
|
-
args = {}
|
|
56
|
-
if not isinstance(args, dict):
|
|
57
|
-
args = {}
|
|
58
|
-
if name == 'exec' and args:
|
|
59
|
-
cmd = args.get('command', '')
|
|
60
|
-
if cmd:
|
|
61
|
-
return f"exec: {cmd}"
|
|
62
|
-
if name == 'read' and args:
|
|
63
|
-
path = args.get('path', '')
|
|
64
|
-
if path:
|
|
65
|
-
return f"read: {path}"
|
|
66
|
-
if name == 'write' and args:
|
|
67
|
-
path = args.get('path', '')
|
|
68
|
-
if path:
|
|
69
|
-
return f"write: {path}"
|
|
70
|
-
if name == 'process' and args:
|
|
71
|
-
action = args.get('action', '')
|
|
72
|
-
sid = args.get('sessionId', '')
|
|
73
|
-
if action and sid:
|
|
74
|
-
return f"process: {action} ({sid})"
|
|
75
|
-
if action:
|
|
76
|
-
return f"process: {action}"
|
|
77
|
-
if name == 'sessions_spawn' and args:
|
|
78
|
-
task = (args.get('task') or '').replace(chr(10), ' ')
|
|
79
|
-
agent = args.get('agentId', '')
|
|
80
|
-
if task and agent:
|
|
81
|
-
return f"sessions_spawn: {agent} - {task}"
|
|
82
|
-
if agent:
|
|
83
|
-
return f"sessions_spawn: {agent}"
|
|
84
|
-
# 其他工具:显示完整 arguments
|
|
85
|
-
if args:
|
|
86
|
-
try:
|
|
87
|
-
s = json.dumps(args, ensure_ascii=False)
|
|
88
|
-
return f"{name}: {s}"
|
|
89
|
-
except Exception:
|
|
90
|
-
pass
|
|
91
|
-
return f"工具: {name}"
|
|
92
|
-
return ''
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def parse_session_file_with_details(session_path: Path, agent_id: str) -> List[Dict]:
|
|
96
|
-
"""解析 session,返回带详情的 API 调用记录(assistant 消息)"""
|
|
97
|
-
records = []
|
|
98
|
-
id_to_msg = {}
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
with open(session_path, 'r', encoding='utf-8') as f:
|
|
102
|
-
for line in f:
|
|
103
|
-
try:
|
|
104
|
-
data = json.loads(line)
|
|
105
|
-
if data.get('type') != 'message':
|
|
106
|
-
continue
|
|
107
|
-
msg = data.get('message', {})
|
|
108
|
-
if not msg:
|
|
109
|
-
continue
|
|
110
|
-
|
|
111
|
-
msg_id = data.get('id', '')
|
|
112
|
-
id_to_msg[msg_id] = {'data': data, 'msg': msg}
|
|
113
|
-
|
|
114
|
-
if msg.get('role') != 'assistant':
|
|
115
|
-
continue
|
|
116
|
-
if 'usage' not in msg:
|
|
117
|
-
continue
|
|
118
|
-
|
|
119
|
-
try:
|
|
120
|
-
ts = datetime.fromisoformat(data['timestamp'].replace('Z', '+00:00'))
|
|
121
|
-
except Exception:
|
|
122
|
-
continue
|
|
123
|
-
|
|
124
|
-
usage = msg.get('usage', {})
|
|
125
|
-
tokens = usage.get('totalTokens', 0) or 0
|
|
126
|
-
model = msg.get('model', '')
|
|
127
|
-
|
|
128
|
-
trigger = ''
|
|
129
|
-
parent_id = data.get('parentId')
|
|
130
|
-
if parent_id and parent_id in id_to_msg:
|
|
131
|
-
parent = id_to_msg[parent_id]['msg']
|
|
132
|
-
parent_role = parent.get('role', '')
|
|
133
|
-
if parent_role == 'user':
|
|
134
|
-
trigger = _extract_trigger_text(parent)
|
|
135
|
-
elif parent_role == 'toolResult':
|
|
136
|
-
tool = parent.get('toolName', '') or (parent.get('details') or {}).get('tool', '?')
|
|
137
|
-
tool_call_id = parent.get('toolCallId', '')
|
|
138
|
-
# 从 toolResult 的 parent(发起调用的 assistant)获取 toolCall 详情
|
|
139
|
-
parent_data = id_to_msg.get(parent_id, {})
|
|
140
|
-
parent_of_tr = parent_data.get('data', {})
|
|
141
|
-
tr_parent_id = parent_of_tr.get('parentId', '')
|
|
142
|
-
if tool_call_id and tr_parent_id and tr_parent_id in id_to_msg:
|
|
143
|
-
detail = _extract_tool_call_detail(id_to_msg[tr_parent_id]['msg'], tool_call_id)
|
|
144
|
-
# 重要:这是 toolResult 触发的消息,即工具执行完成后的回传,不是工具调用本身
|
|
145
|
-
# 【完成回传】前缀醒目,因果顺序:派发 → 子Agent执行 → 完成回传
|
|
146
|
-
trigger = f"【完成回传】{detail}" if detail else f"【完成回传】工具: {tool}"
|
|
147
|
-
else:
|
|
148
|
-
trigger = f"【完成回传】工具: {tool}"
|
|
149
|
-
|
|
150
|
-
records.append({
|
|
151
|
-
'timestamp': ts,
|
|
152
|
-
'tokens': tokens,
|
|
153
|
-
'agentId': agent_id,
|
|
154
|
-
'sessionId': session_path.stem,
|
|
155
|
-
'model': model,
|
|
156
|
-
'trigger': trigger or '(用户输入)',
|
|
157
|
-
'inputTokens': usage.get('input', 0),
|
|
158
|
-
'outputTokens': usage.get('output', 0)
|
|
159
|
-
})
|
|
160
|
-
except Exception:
|
|
161
|
-
continue
|
|
162
|
-
return records
|
|
163
|
-
except Exception as e:
|
|
164
|
-
print(f"解析 session 详情失败 {session_path}: {e}")
|
|
165
|
-
return []
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def parse_session_file(session_path: Path) -> List[Dict]:
|
|
169
|
-
"""解析单个 session 文件,提取每条消息的 token 统计"""
|
|
170
|
-
messages = []
|
|
171
|
-
|
|
172
|
-
try:
|
|
173
|
-
with open(session_path, 'r', encoding='utf-8') as f:
|
|
174
|
-
for line in f:
|
|
175
|
-
try:
|
|
176
|
-
data = json.loads(line)
|
|
177
|
-
|
|
178
|
-
# 只处理有 usage 和 timestamp 的消息
|
|
179
|
-
if 'message' in data and 'usage' in data['message'] and 'timestamp' in data:
|
|
180
|
-
usage = data['message']['usage']
|
|
181
|
-
tokens = usage.get('totalTokens', 0) or 0
|
|
182
|
-
is_request = data.get('message', {}).get('role') == 'assistant'
|
|
183
|
-
|
|
184
|
-
try:
|
|
185
|
-
timestamp = datetime.fromisoformat(data['timestamp'].replace('Z', '+00:00'))
|
|
186
|
-
|
|
187
|
-
# 只统计最近1小时的消息
|
|
188
|
-
now = datetime.now(timezone.utc)
|
|
189
|
-
one_hour_ago = now - timedelta(hours=1)
|
|
190
|
-
|
|
191
|
-
if timestamp >= one_hour_ago:
|
|
192
|
-
messages.append({
|
|
193
|
-
'timestamp': timestamp,
|
|
194
|
-
'tokens': tokens,
|
|
195
|
-
'is_request': is_request
|
|
196
|
-
})
|
|
197
|
-
except:
|
|
198
|
-
pass
|
|
199
|
-
except:
|
|
200
|
-
continue
|
|
201
|
-
|
|
202
|
-
return messages
|
|
203
|
-
except Exception as e:
|
|
204
|
-
print(f"解析 session 文件失败 {session_path}: {e}")
|
|
205
|
-
return []
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@router.get("/performance")
|
|
209
|
-
async def get_performance_stats(range: str = "20m"):
|
|
210
|
-
"""获取性能统计
|
|
211
|
-
|
|
212
|
-
Args:
|
|
213
|
-
range: 时间范围 (20m, 1h)
|
|
214
|
-
"""
|
|
215
|
-
import time
|
|
216
|
-
start = time.perf_counter()
|
|
217
|
-
range_minutes = {
|
|
218
|
-
"20m": 20,
|
|
219
|
-
"1h": 60
|
|
220
|
-
}.get(range, 20)
|
|
221
|
-
|
|
222
|
-
stats = await get_real_stats(range_minutes)
|
|
223
|
-
# 接口处理耗时作为延迟参考(毫秒)
|
|
224
|
-
stats['current']['latency'] = int((time.perf_counter() - start) * 1000)
|
|
225
|
-
return stats
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
async def get_real_stats(range_minutes: int = 20) -> Dict:
|
|
229
|
-
"""获取真实的 TPM/RPM 统计
|
|
230
|
-
|
|
231
|
-
Args:
|
|
232
|
-
range_minutes: 时间范围(分钟)
|
|
233
|
-
"""
|
|
234
|
-
stats = {
|
|
235
|
-
'current': {
|
|
236
|
-
'tpm': 0,
|
|
237
|
-
'rpm': 0,
|
|
238
|
-
'latency': 0,
|
|
239
|
-
'errorRate': 0.0
|
|
240
|
-
},
|
|
241
|
-
'history': {
|
|
242
|
-
'tpm': [],
|
|
243
|
-
'rpm': [],
|
|
244
|
-
'timestamps': []
|
|
245
|
-
},
|
|
246
|
-
'total': {
|
|
247
|
-
'tokens': 0,
|
|
248
|
-
'requests': 0
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
# 读取所有 session 文件
|
|
253
|
-
from data.config_reader import get_openclaw_root
|
|
254
|
-
openclaw_path = get_openclaw_root()
|
|
255
|
-
agents_path = openclaw_path / 'agents'
|
|
256
|
-
|
|
257
|
-
if not agents_path.exists():
|
|
258
|
-
return stats
|
|
259
|
-
|
|
260
|
-
# 按分钟统计
|
|
261
|
-
minutes_stats = {}
|
|
262
|
-
|
|
263
|
-
# 扫描所有 agent 的 sessions
|
|
264
|
-
for agent_dir in agents_path.iterdir():
|
|
265
|
-
if not agent_dir.is_dir():
|
|
266
|
-
continue
|
|
267
|
-
|
|
268
|
-
sessions_path = agent_dir / 'sessions'
|
|
269
|
-
if not sessions_path.exists():
|
|
270
|
-
continue
|
|
271
|
-
|
|
272
|
-
# 扫描所有 .jsonl 文件
|
|
273
|
-
for session_file in sessions_path.glob('*.jsonl'):
|
|
274
|
-
# 跳过 lock 和 deleted 文件
|
|
275
|
-
if 'lock' in session_file.name or 'deleted' in session_file.name:
|
|
276
|
-
continue
|
|
277
|
-
|
|
278
|
-
# 解析 session 文件,获取所有消息
|
|
279
|
-
messages = parse_session_file(session_file)
|
|
280
|
-
|
|
281
|
-
# 按分钟逐条累加
|
|
282
|
-
for msg in messages:
|
|
283
|
-
minute_key = msg['timestamp'].strftime('%Y-%m-%d %H:%M')
|
|
284
|
-
|
|
285
|
-
if minute_key not in minutes_stats:
|
|
286
|
-
minutes_stats[minute_key] = {
|
|
287
|
-
'tokens': 0,
|
|
288
|
-
'requests': 0
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
minutes_stats[minute_key]['tokens'] += msg['tokens']
|
|
292
|
-
if msg['is_request']:
|
|
293
|
-
minutes_stats[minute_key]['requests'] += 1
|
|
294
|
-
|
|
295
|
-
# 排序并转换为列表
|
|
296
|
-
sorted_minutes = sorted(minutes_stats.items())
|
|
297
|
-
|
|
298
|
-
# 填充最近 range_minutes 分钟的数据(缺失的分钟补0)
|
|
299
|
-
timestamps = []
|
|
300
|
-
tpm_data = []
|
|
301
|
-
rpm_data = []
|
|
302
|
-
|
|
303
|
-
now = datetime.now(timezone.utc)
|
|
304
|
-
for i in range(range_minutes):
|
|
305
|
-
minute_time = now - timedelta(minutes=(range_minutes - i - 1))
|
|
306
|
-
minute_key = minute_time.strftime('%Y-%m-%d %H:%M')
|
|
307
|
-
|
|
308
|
-
# 发送 Unix 时间戳(毫秒),前端可正确转换为本地时区
|
|
309
|
-
timestamps.append(int(minute_time.timestamp() * 1000))
|
|
310
|
-
|
|
311
|
-
if minute_key in minutes_stats:
|
|
312
|
-
tpm_data.append(minutes_stats[minute_key]['tokens'])
|
|
313
|
-
rpm_data.append(minutes_stats[minute_key]['requests'])
|
|
314
|
-
else:
|
|
315
|
-
tpm_data.append(0)
|
|
316
|
-
rpm_data.append(0)
|
|
317
|
-
|
|
318
|
-
stats['history']['tpm'] = tpm_data
|
|
319
|
-
stats['history']['rpm'] = rpm_data
|
|
320
|
-
stats['history']['timestamps'] = timestamps
|
|
321
|
-
|
|
322
|
-
# 当前分钟的统计
|
|
323
|
-
current_minute = now.strftime('%Y-%m-%d %H:%M')
|
|
324
|
-
if current_minute in minutes_stats:
|
|
325
|
-
stats['current']['tpm'] = minutes_stats[current_minute]['tokens']
|
|
326
|
-
stats['current']['rpm'] = minutes_stats[current_minute]['requests']
|
|
327
|
-
else:
|
|
328
|
-
stats['current']['tpm'] = 0
|
|
329
|
-
stats['current']['rpm'] = 0
|
|
330
|
-
|
|
331
|
-
# 总计(基于历史数据)
|
|
332
|
-
total_tokens = sum([s['tokens'] for s in minutes_stats.values()])
|
|
333
|
-
total_requests = sum([s['requests'] for s in minutes_stats.values()])
|
|
334
|
-
|
|
335
|
-
stats['total']['tokens'] = total_tokens
|
|
336
|
-
stats['total']['requests'] = total_requests
|
|
337
|
-
|
|
338
|
-
return stats
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
async def get_minute_details(timestamp_ms: int) -> Dict[str, Any]:
|
|
342
|
-
"""获取指定分钟的调用详情,用于柱体点击钻取。时间展示使用 Asia/Shanghai 时区"""
|
|
343
|
-
try:
|
|
344
|
-
ts = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
|
|
345
|
-
ts_local = ts.astimezone(TZ_DISPLAY)
|
|
346
|
-
minute_key = ts_local.strftime('%Y-%m-%d %H:%M')
|
|
347
|
-
minute_start = ts.replace(second=0, microsecond=0)
|
|
348
|
-
minute_end = minute_start + timedelta(minutes=1)
|
|
349
|
-
|
|
350
|
-
from data.config_reader import get_openclaw_root
|
|
351
|
-
openclaw_path = get_openclaw_root()
|
|
352
|
-
agents_path = openclaw_path / 'agents'
|
|
353
|
-
if not agents_path.exists():
|
|
354
|
-
return {'minute': minute_key, 'calls': [], 'totalTokens': 0}
|
|
355
|
-
|
|
356
|
-
all_calls = []
|
|
357
|
-
for agent_dir in agents_path.iterdir():
|
|
358
|
-
if not agent_dir.is_dir():
|
|
359
|
-
continue
|
|
360
|
-
agent_id = agent_dir.name
|
|
361
|
-
sessions_path = agent_dir / 'sessions'
|
|
362
|
-
if not sessions_path.exists():
|
|
363
|
-
continue
|
|
364
|
-
|
|
365
|
-
for session_file in sessions_path.glob('*.jsonl'):
|
|
366
|
-
if 'lock' in session_file.name or 'deleted' in session_file.name:
|
|
367
|
-
continue
|
|
368
|
-
records = parse_session_file_with_details(session_file, agent_id)
|
|
369
|
-
for r in records:
|
|
370
|
-
if minute_start <= r['timestamp'] < minute_end:
|
|
371
|
-
# 转为 Asia/Shanghai 时区展示
|
|
372
|
-
r_ts = r['timestamp']
|
|
373
|
-
if r_ts.tzinfo is None:
|
|
374
|
-
r_ts = r_ts.replace(tzinfo=timezone.utc)
|
|
375
|
-
r_local = r_ts.astimezone(TZ_DISPLAY)
|
|
376
|
-
all_calls.append({
|
|
377
|
-
'agentId': r['agentId'],
|
|
378
|
-
'sessionId': r['sessionId'],
|
|
379
|
-
'model': r['model'],
|
|
380
|
-
'tokens': r['tokens'],
|
|
381
|
-
'trigger': r['trigger'],
|
|
382
|
-
'inputTokens': r.get('inputTokens', 0),
|
|
383
|
-
'outputTokens': r.get('outputTokens', 0),
|
|
384
|
-
'time': r_local.strftime('%H:%M:%S')
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
all_calls.sort(key=lambda x: x['time'])
|
|
388
|
-
total_tokens = sum(c['tokens'] for c in all_calls)
|
|
389
|
-
return {
|
|
390
|
-
'minute': minute_key,
|
|
391
|
-
'calls': all_calls,
|
|
392
|
-
'totalCalls': len(all_calls),
|
|
393
|
-
'totalTokens': total_tokens
|
|
394
|
-
}
|
|
395
|
-
except Exception as e:
|
|
396
|
-
print(f"获取分钟详情失败: {e}")
|
|
397
|
-
import traceback
|
|
398
|
-
traceback.print_exc()
|
|
399
|
-
return {'minute': '', 'calls': [], 'totalTokens': 0}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
@router.get("/performance/details")
|
|
403
|
-
async def get_performance_details(timestamp: int):
|
|
404
|
-
"""获取指定分钟的 TPM/RPM 调用详情(柱体点击钻取)
|
|
405
|
-
|
|
406
|
-
Args:
|
|
407
|
-
timestamp: 分钟起始的 Unix 毫秒时间戳
|
|
408
|
-
"""
|
|
409
|
-
return await get_minute_details(timestamp)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def _openclaw_path() -> Path:
|
|
413
|
-
from data.config_reader import get_openclaw_root
|
|
414
|
-
return get_openclaw_root()
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
@router.get("/tokens/analysis")
|
|
418
|
-
async def get_tokens_analysis():
|
|
419
|
-
"""
|
|
420
|
-
Token 分析视图:按 agent、按 session 汇总 usage
|
|
421
|
-
数据来源:sessions.json (inputTokens, outputTokens, cacheRead, cacheWrite)
|
|
422
|
-
"""
|
|
423
|
-
openclaw_path = _openclaw_path()
|
|
424
|
-
agents_path = openclaw_path / 'agents'
|
|
425
|
-
|
|
426
|
-
result = {"byAgent": {}, "total": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}}
|
|
427
|
-
|
|
428
|
-
if not agents_path.exists():
|
|
429
|
-
return result
|
|
430
|
-
|
|
431
|
-
for agent_dir in agents_path.iterdir():
|
|
432
|
-
if not agent_dir.is_dir():
|
|
433
|
-
continue
|
|
434
|
-
agent_id = agent_dir.name
|
|
435
|
-
sessions_index = agent_dir / 'sessions' / 'sessions.json'
|
|
436
|
-
if not sessions_index.exists():
|
|
437
|
-
continue
|
|
438
|
-
|
|
439
|
-
try:
|
|
440
|
-
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
441
|
-
data = json.load(f)
|
|
442
|
-
if not isinstance(data, dict):
|
|
443
|
-
continue
|
|
444
|
-
|
|
445
|
-
agent_total = {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "sessions": []}
|
|
446
|
-
for session_key, entry in data.items():
|
|
447
|
-
if not isinstance(entry, dict):
|
|
448
|
-
continue
|
|
449
|
-
inp = entry.get('inputTokens', 0) or 0
|
|
450
|
-
out = entry.get('outputTokens', 0) or 0
|
|
451
|
-
cr = entry.get('cacheRead', 0) or 0
|
|
452
|
-
cw = entry.get('cacheWrite', 0) or 0
|
|
453
|
-
agent_total["input"] += inp
|
|
454
|
-
agent_total["output"] += out
|
|
455
|
-
agent_total["cacheRead"] += cr
|
|
456
|
-
agent_total["cacheWrite"] += cw
|
|
457
|
-
agent_total["sessions"].append({
|
|
458
|
-
"sessionKey": session_key,
|
|
459
|
-
"input": inp,
|
|
460
|
-
"output": out,
|
|
461
|
-
"cacheRead": cr,
|
|
462
|
-
"cacheWrite": cw,
|
|
463
|
-
"total": inp + out,
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
result["byAgent"][agent_id] = agent_total
|
|
467
|
-
result["total"]["input"] += agent_total["input"]
|
|
468
|
-
result["total"]["output"] += agent_total["output"]
|
|
469
|
-
result["total"]["cacheRead"] += agent_total["cacheRead"]
|
|
470
|
-
result["total"]["cacheWrite"] += agent_total["cacheWrite"]
|
|
471
|
-
except Exception:
|
|
472
|
-
continue
|
|
473
|
-
|
|
474
|
-
return result
|