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,240 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
会话读取器 - 读取 sessions/*.jsonl 和 sessions.json
|
|
3
|
-
"""
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import List, Dict, Any, Optional
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_agent_sessions_path(agent_id: str) -> Optional[Path]:
|
|
14
|
-
"""获取 Agent 的 sessions 目录"""
|
|
15
|
-
sessions_path = get_openclaw_root() / "agents" / normalize_openclaw_agent_id(agent_id) / "sessions"
|
|
16
|
-
if not sessions_path.exists():
|
|
17
|
-
return None
|
|
18
|
-
return sessions_path
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def get_latest_session_file(agent_id: str) -> Optional[Path]:
|
|
22
|
-
"""获取最新的 session 文件"""
|
|
23
|
-
sessions_path = get_agent_sessions_path(agent_id)
|
|
24
|
-
if not sessions_path:
|
|
25
|
-
return None
|
|
26
|
-
|
|
27
|
-
jsonl_files = list(sessions_path.glob("*.jsonl"))
|
|
28
|
-
if not jsonl_files:
|
|
29
|
-
return None
|
|
30
|
-
|
|
31
|
-
# 按修改时间排序,取最新的
|
|
32
|
-
jsonl_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
|
33
|
-
return jsonl_files[0]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def get_recent_messages(agent_id: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
37
|
-
"""获取最近的会话消息"""
|
|
38
|
-
session_file = get_latest_session_file(agent_id)
|
|
39
|
-
if not session_file:
|
|
40
|
-
return []
|
|
41
|
-
|
|
42
|
-
messages = []
|
|
43
|
-
with open(session_file, 'r', encoding='utf-8') as f:
|
|
44
|
-
for line in f:
|
|
45
|
-
try:
|
|
46
|
-
data = json.loads(line.strip())
|
|
47
|
-
if data.get('type') == 'message':
|
|
48
|
-
messages.append(data.get('message', {}))
|
|
49
|
-
except json.JSONDecodeError:
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
# 只取最后 N 条
|
|
53
|
-
return messages[-limit:]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def has_recent_errors(agent_id: str, minutes: int = 5) -> bool:
|
|
57
|
-
"""检查最近是否有错误"""
|
|
58
|
-
messages = get_recent_messages(agent_id, limit=50)
|
|
59
|
-
|
|
60
|
-
import time
|
|
61
|
-
cutoff_time = int(time.time() * 1000) - (minutes * 60 * 1000)
|
|
62
|
-
|
|
63
|
-
for msg in messages:
|
|
64
|
-
if msg.get('stopReason') == 'error':
|
|
65
|
-
timestamp = msg.get('timestamp', 0)
|
|
66
|
-
if timestamp > cutoff_time:
|
|
67
|
-
return True
|
|
68
|
-
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def get_last_error(agent_id: str) -> Optional[Dict[str, Any]]:
|
|
73
|
-
"""获取最近的错误信息"""
|
|
74
|
-
messages = get_recent_messages(agent_id, limit=100)
|
|
75
|
-
|
|
76
|
-
for msg in reversed(messages):
|
|
77
|
-
if msg.get('stopReason') == 'error':
|
|
78
|
-
return {
|
|
79
|
-
'type': detect_error_type(msg.get('errorMessage', '')),
|
|
80
|
-
'message': msg.get('errorMessage', ''),
|
|
81
|
-
'timestamp': msg.get('timestamp', 0)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return None
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def detect_error_type(error_msg: str) -> str:
|
|
88
|
-
"""检测错误类型"""
|
|
89
|
-
error_msg_lower = (error_msg or '').lower()
|
|
90
|
-
|
|
91
|
-
if '429' in error_msg or 'rate limit' in error_msg_lower:
|
|
92
|
-
return 'rate-limit'
|
|
93
|
-
elif 'token' in error_msg_lower or 'context' in error_msg_lower:
|
|
94
|
-
return 'token-limit'
|
|
95
|
-
elif 'timeout' in error_msg_lower or '超时' in error_msg_lower:
|
|
96
|
-
return 'timeout'
|
|
97
|
-
elif '余额不足' in (error_msg or ''):
|
|
98
|
-
return 'quota'
|
|
99
|
-
else:
|
|
100
|
-
return 'unknown'
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def get_session_updated_at(agent_id: str) -> int:
|
|
104
|
-
"""
|
|
105
|
-
获取 Agent 会话的最后更新时间(sessions.json 中 updatedAt 的最大值)
|
|
106
|
-
用于判断「最近 5 分钟是否有 session 活动」
|
|
107
|
-
"""
|
|
108
|
-
aid = normalize_openclaw_agent_id(agent_id)
|
|
109
|
-
sessions_index = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
|
|
110
|
-
if not sessions_index.exists():
|
|
111
|
-
return 0
|
|
112
|
-
|
|
113
|
-
try:
|
|
114
|
-
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
115
|
-
data = json.load(f)
|
|
116
|
-
if not isinstance(data, dict):
|
|
117
|
-
return 0
|
|
118
|
-
max_ts = 0
|
|
119
|
-
for entry in data.values():
|
|
120
|
-
if isinstance(entry, dict):
|
|
121
|
-
ts = entry.get('updatedAt', 0)
|
|
122
|
-
if isinstance(ts, (int, float)) and ts > max_ts:
|
|
123
|
-
max_ts = int(ts)
|
|
124
|
-
return max_ts
|
|
125
|
-
except (json.JSONDecodeError, IOError):
|
|
126
|
-
return 0
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def has_recent_session_activity(agent_id: str, minutes: int = 5) -> bool:
|
|
130
|
-
"""检查 Agent 最近 N 分钟内是否有 session 活动"""
|
|
131
|
-
import time
|
|
132
|
-
updated_at = get_session_updated_at(agent_id)
|
|
133
|
-
if not updated_at:
|
|
134
|
-
return False
|
|
135
|
-
cutoff = int(time.time() * 1000) - (minutes * 60 * 1000)
|
|
136
|
-
return updated_at > cutoff
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: int = 50) -> List[Dict[str, Any]]:
|
|
140
|
-
"""
|
|
141
|
-
解析 jsonl 获取会话轮次,每轮包含 user/assistant/toolResult 及 usage
|
|
142
|
-
返回格式: [{ turnIndex, role, content, usage?, toolCalls?, stopReason?, timestamp }]
|
|
143
|
-
"""
|
|
144
|
-
aid = normalize_openclaw_agent_id(agent_id)
|
|
145
|
-
sessions_index = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
|
|
146
|
-
if not sessions_index.exists():
|
|
147
|
-
return []
|
|
148
|
-
|
|
149
|
-
session_file: Optional[Path] = None
|
|
150
|
-
if session_key:
|
|
151
|
-
try:
|
|
152
|
-
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
153
|
-
index_data = json.load(f)
|
|
154
|
-
entry = index_data.get(session_key) if isinstance(index_data, dict) else None
|
|
155
|
-
if entry and isinstance(entry, dict):
|
|
156
|
-
sf = entry.get('sessionFile')
|
|
157
|
-
sid = entry.get('sessionId')
|
|
158
|
-
if sf:
|
|
159
|
-
session_file = Path(sf)
|
|
160
|
-
elif sid:
|
|
161
|
-
session_file = get_openclaw_root() / "agents" / aid / "sessions" / f"{sid}.jsonl"
|
|
162
|
-
except (json.JSONDecodeError, IOError):
|
|
163
|
-
pass
|
|
164
|
-
|
|
165
|
-
if not session_file or not session_file.exists():
|
|
166
|
-
session_file = get_latest_session_file(agent_id)
|
|
167
|
-
|
|
168
|
-
if not session_file:
|
|
169
|
-
return []
|
|
170
|
-
|
|
171
|
-
turns: List[Dict[str, Any]] = []
|
|
172
|
-
turn_index = 0
|
|
173
|
-
|
|
174
|
-
with open(session_file, 'r', encoding='utf-8') as f:
|
|
175
|
-
for line in f:
|
|
176
|
-
try:
|
|
177
|
-
data = json.loads(line.strip())
|
|
178
|
-
if data.get('type') != 'message':
|
|
179
|
-
continue
|
|
180
|
-
msg = data.get('message', {})
|
|
181
|
-
role = msg.get('role')
|
|
182
|
-
if not role:
|
|
183
|
-
continue
|
|
184
|
-
|
|
185
|
-
turn: Dict[str, Any] = {
|
|
186
|
-
'turnIndex': turn_index,
|
|
187
|
-
'role': role,
|
|
188
|
-
'timestamp': msg.get('timestamp') or data.get('timestamp'),
|
|
189
|
-
'content': [],
|
|
190
|
-
'usage': msg.get('usage'),
|
|
191
|
-
'stopReason': msg.get('stopReason'),
|
|
192
|
-
'toolCalls': [],
|
|
193
|
-
'toolName': None,
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
content = msg.get('content', [])
|
|
197
|
-
if isinstance(content, str):
|
|
198
|
-
content = [{'type': 'text', 'text': content}]
|
|
199
|
-
|
|
200
|
-
for c in content:
|
|
201
|
-
if not isinstance(c, dict):
|
|
202
|
-
continue
|
|
203
|
-
ct = c.get('type')
|
|
204
|
-
if ct == 'text':
|
|
205
|
-
if role == 'toolResult':
|
|
206
|
-
turn['content'].append({
|
|
207
|
-
'type': 'toolResult',
|
|
208
|
-
'content': c.get('text', ''),
|
|
209
|
-
'status': msg.get('details', {}).get('status'),
|
|
210
|
-
'error': msg.get('details', {}).get('error'),
|
|
211
|
-
})
|
|
212
|
-
else:
|
|
213
|
-
turn['content'].append({'type': 'text', 'text': c.get('text', '')})
|
|
214
|
-
elif ct == 'thinking':
|
|
215
|
-
turn['content'].append({'type': 'thinking', 'text': c.get('thinking', '')})
|
|
216
|
-
elif ct == 'toolCall':
|
|
217
|
-
turn['toolCalls'].append({
|
|
218
|
-
'name': c.get('name'),
|
|
219
|
-
'arguments': c.get('arguments'),
|
|
220
|
-
'id': c.get('id'),
|
|
221
|
-
})
|
|
222
|
-
turn['toolName'] = c.get('name')
|
|
223
|
-
|
|
224
|
-
if role == 'toolResult':
|
|
225
|
-
turn['toolName'] = msg.get('toolName')
|
|
226
|
-
if not turn['content']:
|
|
227
|
-
details = msg.get('details', {})
|
|
228
|
-
turn['content'].append({
|
|
229
|
-
'type': 'toolResult',
|
|
230
|
-
'status': details.get('status'),
|
|
231
|
-
'error': details.get('error'),
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
turns.append(turn)
|
|
235
|
-
turn_index += 1
|
|
236
|
-
|
|
237
|
-
except (json.JSONDecodeError, KeyError):
|
|
238
|
-
continue
|
|
239
|
-
|
|
240
|
-
return turns[-limit:] if len(turns) > limit else turns
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
状态计算器 - 计算 Agent 状态
|
|
3
|
-
"""
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
sys.path.append(str(Path(__file__).parent.parent))
|
|
7
|
-
|
|
8
|
-
import time
|
|
9
|
-
from typing import Literal
|
|
10
|
-
from data.config_reader import get_agents_list, get_agent_config
|
|
11
|
-
from data.subagent_reader import is_agent_working, get_agent_runs
|
|
12
|
-
from data.session_reader import (
|
|
13
|
-
has_recent_errors,
|
|
14
|
-
get_last_error,
|
|
15
|
-
has_recent_session_activity,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
AgentStatus = Literal['idle', 'working', 'down']
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def calculate_agent_status(agent_id: str) -> AgentStatus:
|
|
23
|
-
"""
|
|
24
|
-
计算 Agent 状态(基于 runs.json + sessions.json)
|
|
25
|
-
|
|
26
|
-
优先级:
|
|
27
|
-
1. 异常 (down) - 最近5分钟有 stopReason=error
|
|
28
|
-
2. 工作中 (working) - 有活跃 subagent run 或 session 正在处理(最近5分钟有活动)
|
|
29
|
-
3. 空闲 (idle) - 无活跃 run 且最近5分钟无 session 活动
|
|
30
|
-
"""
|
|
31
|
-
# 检查异常
|
|
32
|
-
if has_recent_errors(agent_id, minutes=5):
|
|
33
|
-
return 'down'
|
|
34
|
-
|
|
35
|
-
# 检查工作中:subagent run 未结束,或 session 最近有活动
|
|
36
|
-
if is_agent_working(agent_id):
|
|
37
|
-
return 'working'
|
|
38
|
-
if has_recent_session_activity(agent_id, minutes=2):
|
|
39
|
-
return 'working'
|
|
40
|
-
|
|
41
|
-
# 默认空闲
|
|
42
|
-
return 'idle'
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def get_agents_with_status() -> list:
|
|
46
|
-
"""获取所有 Agent 及其状态"""
|
|
47
|
-
agents = get_agents_list()
|
|
48
|
-
result = []
|
|
49
|
-
|
|
50
|
-
for agent in agents:
|
|
51
|
-
agent_id = agent.get('id')
|
|
52
|
-
status = calculate_agent_status(agent_id)
|
|
53
|
-
|
|
54
|
-
# 获取当前任务
|
|
55
|
-
current_task = get_current_task(agent_id)
|
|
56
|
-
|
|
57
|
-
# 获取最后活跃时间
|
|
58
|
-
last_active = get_last_active_time(agent_id)
|
|
59
|
-
|
|
60
|
-
# 获取错误信息
|
|
61
|
-
last_error = get_last_error(agent_id) if status == 'down' else None
|
|
62
|
-
|
|
63
|
-
result.append({
|
|
64
|
-
'id': agent_id,
|
|
65
|
-
'name': agent.get('name'),
|
|
66
|
-
'role': agent.get('name'),
|
|
67
|
-
'status': status,
|
|
68
|
-
'currentTask': current_task,
|
|
69
|
-
'lastActiveAt': last_active,
|
|
70
|
-
'error': last_error
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
return result
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def get_current_task(agent_id: str) -> str:
|
|
77
|
-
"""获取 Agent 当前任务"""
|
|
78
|
-
runs = get_agent_runs(agent_id, limit=1)
|
|
79
|
-
if not runs:
|
|
80
|
-
return ''
|
|
81
|
-
|
|
82
|
-
run = runs[0]
|
|
83
|
-
task = run.get('task', '')
|
|
84
|
-
|
|
85
|
-
# 截取前60个字符(统一长度)
|
|
86
|
-
if len(task) > 60:
|
|
87
|
-
task = task[:57] + '...'
|
|
88
|
-
|
|
89
|
-
return task
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def get_last_active_time(agent_id: str) -> int:
|
|
93
|
-
"""获取 Agent 最后活跃时间(runs 或 sessions.json updatedAt)"""
|
|
94
|
-
from data.session_reader import get_session_updated_at
|
|
95
|
-
|
|
96
|
-
runs = get_agent_runs(agent_id, limit=1)
|
|
97
|
-
run_ts = 0
|
|
98
|
-
if runs:
|
|
99
|
-
run = runs[0]
|
|
100
|
-
run_ts = run.get('endedAt') or run.get('startedAt', 0)
|
|
101
|
-
|
|
102
|
-
session_ts = get_session_updated_at(agent_id)
|
|
103
|
-
return max(run_ts, session_ts)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def format_last_active(timestamp: int) -> str:
|
|
107
|
-
"""格式化最后活跃时间为相对时间"""
|
|
108
|
-
if not timestamp:
|
|
109
|
-
return '未知'
|
|
110
|
-
|
|
111
|
-
now = int(time.time() * 1000)
|
|
112
|
-
diff_seconds = (now - timestamp) / 1000
|
|
113
|
-
|
|
114
|
-
if diff_seconds < 60:
|
|
115
|
-
return f"{int(diff_seconds)}秒前"
|
|
116
|
-
elif diff_seconds < 3600:
|
|
117
|
-
return f"{int(diff_seconds / 60)}分钟前"
|
|
118
|
-
elif diff_seconds < 86400:
|
|
119
|
-
return f"{int(diff_seconds / 3600)}小时前"
|
|
120
|
-
else:
|
|
121
|
-
return f"{int(diff_seconds / 86400)}天前"
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
子代理运行读取器 - 读取 subagents/runs.json
|
|
3
|
-
"""
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import List, Dict, Any, Optional
|
|
8
|
-
|
|
9
|
-
from data.config_reader import normalize_openclaw_agent_id
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _openclaw_home() -> Path:
|
|
13
|
-
"""OpenClaw 根目录,优先使用 OPENCLAW_HOME 环境变量"""
|
|
14
|
-
env = os.environ.get("OPENCLAW_HOME")
|
|
15
|
-
if env:
|
|
16
|
-
p = Path(env).expanduser()
|
|
17
|
-
if p.exists():
|
|
18
|
-
return p
|
|
19
|
-
return Path.home() / ".openclaw"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
SUBAGENTS_RUNS_PATH = _openclaw_home() / "subagents" / "runs.json"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def load_subagent_runs() -> List[Dict[str, Any]]:
|
|
26
|
-
"""加载子代理运行记录
|
|
27
|
-
|
|
28
|
-
OpenClaw runs.json 格式: {"version": 2, "runs": { runId: record }}
|
|
29
|
-
"""
|
|
30
|
-
if not SUBAGENTS_RUNS_PATH.exists():
|
|
31
|
-
return []
|
|
32
|
-
|
|
33
|
-
with open(SUBAGENTS_RUNS_PATH, 'r', encoding='utf-8') as f:
|
|
34
|
-
data = json.load(f)
|
|
35
|
-
|
|
36
|
-
runs = data.get('runs', {})
|
|
37
|
-
if isinstance(runs, dict):
|
|
38
|
-
return list(runs.values())
|
|
39
|
-
return runs if isinstance(runs, list) else []
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def get_active_runs() -> List[Dict[str, Any]]:
|
|
43
|
-
"""获取活跃的运行(未结束)"""
|
|
44
|
-
runs = load_subagent_runs()
|
|
45
|
-
return [run for run in runs if run.get('endedAt') is None]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def get_agent_runs(agent_id: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
49
|
-
"""获取指定 Agent 的运行记录"""
|
|
50
|
-
runs = load_subagent_runs()
|
|
51
|
-
agent_runs = []
|
|
52
|
-
prefix = f"agent:{normalize_openclaw_agent_id(agent_id)}:"
|
|
53
|
-
for run in runs:
|
|
54
|
-
child_key = run.get('childSessionKey', '')
|
|
55
|
-
if prefix in child_key:
|
|
56
|
-
agent_runs.append(run)
|
|
57
|
-
|
|
58
|
-
# 按开始时间倒序
|
|
59
|
-
agent_runs.sort(key=lambda x: x.get('startedAt', 0), reverse=True)
|
|
60
|
-
return agent_runs[:limit]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def is_agent_working(agent_id: str) -> bool:
|
|
64
|
-
"""
|
|
65
|
-
判断 Agent 是否在工作中
|
|
66
|
-
- 作为执行者:childSessionKey 包含 agent:{agent_id}:
|
|
67
|
-
- 作为派发者:requesterSessionKey 包含 agent:{agent_id}:(主 Agent 等待子 Agent 完成)
|
|
68
|
-
"""
|
|
69
|
-
active_runs = get_active_runs()
|
|
70
|
-
prefix = f"agent:{normalize_openclaw_agent_id(agent_id)}:"
|
|
71
|
-
for run in active_runs:
|
|
72
|
-
child_key = run.get('childSessionKey', '')
|
|
73
|
-
requester_key = run.get('requesterSessionKey', '')
|
|
74
|
-
if prefix in child_key:
|
|
75
|
-
return True
|
|
76
|
-
if prefix in requester_key:
|
|
77
|
-
return True
|
|
78
|
-
return False
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) -> Optional[str]:
|
|
82
|
-
"""
|
|
83
|
-
从子 Agent 的 session 文件中提取最后一次 assistant 消息的文本输出。
|
|
84
|
-
用于任务成功时展示 Agent 的执行结果。
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
child_session_key: 格式 agent:<agentId>:subagent:<uuid>
|
|
88
|
-
max_chars: 最大返回字符数,超出则截断
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
Agent 的文本输出,若无法获取则返回 None
|
|
92
|
-
"""
|
|
93
|
-
if not child_session_key or ':' not in child_session_key:
|
|
94
|
-
return None
|
|
95
|
-
parts = child_session_key.split(':')
|
|
96
|
-
if len(parts) < 2 or parts[0] != 'agent':
|
|
97
|
-
return None
|
|
98
|
-
agent_id = normalize_openclaw_agent_id(parts[1])
|
|
99
|
-
|
|
100
|
-
openclaw_path = _openclaw_home()
|
|
101
|
-
sessions_index = openclaw_path / "agents" / agent_id / "sessions" / "sessions.json"
|
|
102
|
-
if not sessions_index.exists():
|
|
103
|
-
return None
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
107
|
-
index_data = json.load(f)
|
|
108
|
-
entry = index_data.get(child_session_key)
|
|
109
|
-
if not entry:
|
|
110
|
-
return None
|
|
111
|
-
session_file = entry.get('sessionFile')
|
|
112
|
-
session_id = entry.get('sessionId')
|
|
113
|
-
if not session_file and not session_id:
|
|
114
|
-
return None
|
|
115
|
-
if not session_file:
|
|
116
|
-
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
117
|
-
session_file = str(sessions_dir / f"{session_id}.jsonl")
|
|
118
|
-
|
|
119
|
-
session_path = Path(session_file)
|
|
120
|
-
if not session_path.exists():
|
|
121
|
-
return None
|
|
122
|
-
|
|
123
|
-
last_text = None
|
|
124
|
-
with open(session_path, 'r', encoding='utf-8') as f:
|
|
125
|
-
for line in f:
|
|
126
|
-
try:
|
|
127
|
-
data = json.loads(line)
|
|
128
|
-
if data.get('type') != 'message':
|
|
129
|
-
continue
|
|
130
|
-
msg = data.get('message', {})
|
|
131
|
-
if msg.get('role') != 'assistant':
|
|
132
|
-
continue
|
|
133
|
-
content = msg.get('content', [])
|
|
134
|
-
for c in content:
|
|
135
|
-
if isinstance(c, dict) and c.get('type') == 'text':
|
|
136
|
-
text = c.get('text', '')
|
|
137
|
-
if text and text.strip():
|
|
138
|
-
last_text = text
|
|
139
|
-
break
|
|
140
|
-
except (json.JSONDecodeError, KeyError):
|
|
141
|
-
continue
|
|
142
|
-
|
|
143
|
-
if not last_text or not last_text.strip():
|
|
144
|
-
return None
|
|
145
|
-
if len(last_text) > max_chars:
|
|
146
|
-
return last_text[:max_chars] + '\n\n...(输出已截断)'
|
|
147
|
-
return last_text
|
|
148
|
-
except Exception as e:
|
|
149
|
-
print(f"get_agent_output_for_run 失败: {e}")
|
|
150
|
-
return None
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def get_agent_files_for_run(child_session_key: str) -> List[str]:
|
|
154
|
-
"""
|
|
155
|
-
从子 Agent 的 session 中提取本次任务生成/修改的文件路径。
|
|
156
|
-
解析 write、edit 等工具调用中的 path 参数。
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
去重后的文件路径列表
|
|
160
|
-
"""
|
|
161
|
-
if not child_session_key or ':' not in child_session_key:
|
|
162
|
-
return []
|
|
163
|
-
parts = child_session_key.split(':')
|
|
164
|
-
if len(parts) < 2 or parts[0] != 'agent':
|
|
165
|
-
return []
|
|
166
|
-
agent_id = normalize_openclaw_agent_id(parts[1])
|
|
167
|
-
|
|
168
|
-
openclaw_path = _openclaw_home()
|
|
169
|
-
sessions_index = openclaw_path / "agents" / agent_id / "sessions" / "sessions.json"
|
|
170
|
-
if not sessions_index.exists():
|
|
171
|
-
return []
|
|
172
|
-
|
|
173
|
-
try:
|
|
174
|
-
with open(sessions_index, 'r', encoding='utf-8') as f:
|
|
175
|
-
index_data = json.load(f)
|
|
176
|
-
entry = index_data.get(child_session_key)
|
|
177
|
-
if not entry:
|
|
178
|
-
return []
|
|
179
|
-
session_file = entry.get('sessionFile')
|
|
180
|
-
session_id = entry.get('sessionId')
|
|
181
|
-
if not session_file and not session_id:
|
|
182
|
-
return []
|
|
183
|
-
if not session_file:
|
|
184
|
-
sessions_dir = openclaw_path / "agents" / agent_id / "sessions"
|
|
185
|
-
session_file = str(sessions_dir / f"{session_id}.jsonl")
|
|
186
|
-
|
|
187
|
-
session_path = Path(session_file)
|
|
188
|
-
if not session_path.exists():
|
|
189
|
-
return []
|
|
190
|
-
|
|
191
|
-
file_paths: List[str] = []
|
|
192
|
-
file_tools = ('write', 'edit')
|
|
193
|
-
|
|
194
|
-
with open(session_path, 'r', encoding='utf-8') as f:
|
|
195
|
-
for line in f:
|
|
196
|
-
try:
|
|
197
|
-
data = json.loads(line)
|
|
198
|
-
if data.get('type') != 'message':
|
|
199
|
-
continue
|
|
200
|
-
msg = data.get('message', {})
|
|
201
|
-
if msg.get('role') != 'assistant':
|
|
202
|
-
continue
|
|
203
|
-
content = msg.get('content', [])
|
|
204
|
-
for c in content:
|
|
205
|
-
if not isinstance(c, dict) or c.get('type') != 'toolCall':
|
|
206
|
-
continue
|
|
207
|
-
name = c.get('name', '')
|
|
208
|
-
if name not in file_tools:
|
|
209
|
-
continue
|
|
210
|
-
args = c.get('arguments', {})
|
|
211
|
-
if isinstance(args, str):
|
|
212
|
-
try:
|
|
213
|
-
args = json.loads(args)
|
|
214
|
-
except json.JSONDecodeError:
|
|
215
|
-
continue
|
|
216
|
-
path = args.get('path') or args.get('file_path')
|
|
217
|
-
if path and isinstance(path, str) and path.strip():
|
|
218
|
-
file_paths.append(path.strip())
|
|
219
|
-
except (json.JSONDecodeError, KeyError):
|
|
220
|
-
continue
|
|
221
|
-
|
|
222
|
-
# 去重并保持顺序
|
|
223
|
-
seen = set()
|
|
224
|
-
result = []
|
|
225
|
-
for p in file_paths:
|
|
226
|
-
if p not in seen:
|
|
227
|
-
seen.add(p)
|
|
228
|
-
result.append(p)
|
|
229
|
-
return result
|
|
230
|
-
except Exception as e:
|
|
231
|
-
print(f"get_agent_files_for_run 失败: {e}")
|
|
232
|
-
return []
|