openclaw-agent-dashboard 1.0.36 → 1.0.38
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/agents.py +6 -5
- package/dashboard/api/agents.py +6 -5
- package/dashboard/api/collaboration.py +76 -52
- package/dashboard/api/subagents.py +12 -9
- package/dashboard/api/timeline.py +4 -13
- package/dashboard/collaboration.py +5 -3
- package/dashboard/data/agent_config_manager.py +6 -5
- package/dashboard/data/chain_reader.py +3 -1
- package/dashboard/data/config_reader.py +42 -3
- package/dashboard/data/error_analyzer.py +5 -3
- package/dashboard/data/session_reader.py +28 -19
- package/dashboard/data/subagent_reader.py +10 -8
- package/dashboard/data/timeline_reader.py +17 -14
- package/dashboard/mechanism_reader.py +3 -2
- package/dashboard/mechanisms.py +3 -4
- package/dashboard/session_reader.py +7 -5
- package/dashboard/status/status_calculator.py +2 -2
- package/dashboard/subagent_reader.py +9 -6
- package/frontend-dist/assets/{index-Drb7GQ4m.js → index-BQRg8Gvl.js} +7 -7
- package/frontend-dist/assets/{index-DoK53sgy.css → index-CghUi6Ys.css} +1 -1
- package/frontend-dist/index.html +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dashboard/agents.py
CHANGED
|
@@ -45,8 +45,10 @@ async def get_agent(agent_id: str):
|
|
|
45
45
|
"""获取单个 Agent 详情"""
|
|
46
46
|
agents = get_agents_with_status()
|
|
47
47
|
|
|
48
|
+
from data.config_reader import agent_ids_equal
|
|
49
|
+
|
|
48
50
|
for agent in agents:
|
|
49
|
-
if agent['id']
|
|
51
|
+
if agent_ids_equal(agent['id'], agent_id):
|
|
50
52
|
if agent.get('lastActiveAt'):
|
|
51
53
|
agent['lastActiveFormatted'] = format_last_active(agent['lastActiveAt'])
|
|
52
54
|
return agent
|
|
@@ -62,10 +64,9 @@ async def get_agent_output(agent_id: str, limit: int = 50):
|
|
|
62
64
|
用于调试视图展示
|
|
63
65
|
"""
|
|
64
66
|
from data.session_reader import get_session_turns
|
|
65
|
-
from data.config_reader import
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if not any(a.get('id') == agent_id for a in agents):
|
|
67
|
+
from data.config_reader import get_agent_config
|
|
68
|
+
|
|
69
|
+
if not get_agent_config(agent_id):
|
|
69
70
|
from fastapi import HTTPException
|
|
70
71
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
71
72
|
|
package/dashboard/api/agents.py
CHANGED
|
@@ -45,8 +45,10 @@ async def get_agent(agent_id: str):
|
|
|
45
45
|
"""获取单个 Agent 详情"""
|
|
46
46
|
agents = get_agents_with_status()
|
|
47
47
|
|
|
48
|
+
from data.config_reader import agent_ids_equal
|
|
49
|
+
|
|
48
50
|
for agent in agents:
|
|
49
|
-
if agent['id']
|
|
51
|
+
if agent_ids_equal(agent['id'], agent_id):
|
|
50
52
|
if agent.get('lastActiveAt'):
|
|
51
53
|
agent['lastActiveFormatted'] = format_last_active(agent['lastActiveAt'])
|
|
52
54
|
return agent
|
|
@@ -62,10 +64,9 @@ async def get_agent_output(agent_id: str, limit: int = 50):
|
|
|
62
64
|
用于调试视图展示
|
|
63
65
|
"""
|
|
64
66
|
from data.session_reader import get_session_turns
|
|
65
|
-
from data.config_reader import
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if not any(a.get('id') == agent_id for a in agents):
|
|
67
|
+
from data.config_reader import get_agent_config
|
|
68
|
+
|
|
69
|
+
if not get_agent_config(agent_id):
|
|
69
70
|
from fastapi import HTTPException
|
|
70
71
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
71
72
|
|
|
@@ -73,6 +73,23 @@ class ActiveTask(BaseModel):
|
|
|
73
73
|
featureId: Optional[str] = None # FEATURE_ID(如果有)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _main_agent_status_for_collaboration(main_agent_id: str) -> str:
|
|
77
|
+
"""
|
|
78
|
+
协作流程里 PM 卡片状态:与 delegate 边是否激活同源——只认 subagents/runs.json 的活跃 run
|
|
79
|
+
(is_agent_working)+ 近期会话错误;不使用 calculate_agent_status 里的主会话 solo 启发式
|
|
80
|
+
(thinking / 未完成 tool / sessions 短窗),避免「连线已变实线仍显示工作中」。
|
|
81
|
+
全局 /api/agents 仍用 calculate_agent_status,可继续反映「主会话自己在跑」的语义。
|
|
82
|
+
"""
|
|
83
|
+
from data.session_reader import has_recent_errors
|
|
84
|
+
from data.subagent_reader import is_agent_working
|
|
85
|
+
|
|
86
|
+
if has_recent_errors(main_agent_id, minutes=5):
|
|
87
|
+
return 'error'
|
|
88
|
+
if is_agent_working(main_agent_id):
|
|
89
|
+
return 'working'
|
|
90
|
+
return 'idle'
|
|
91
|
+
|
|
92
|
+
|
|
76
93
|
def _extract_feature_id(task_name: str) -> Optional[str]:
|
|
77
94
|
"""从任务名称中提取 FEATURE_ID"""
|
|
78
95
|
if not task_name:
|
|
@@ -107,16 +124,21 @@ def _build_agent_active_tasks(
|
|
|
107
124
|
...
|
|
108
125
|
}
|
|
109
126
|
"""
|
|
127
|
+
from data.config_reader import agent_ids_equal, canonical_agent_id_from_config
|
|
128
|
+
|
|
110
129
|
agent_active_tasks: Dict[str, List[Dict[str, Any]]] = {}
|
|
111
130
|
|
|
112
131
|
for run in active_runs:
|
|
113
132
|
child_key = run.get('childSessionKey', '')
|
|
114
133
|
requester_key = run.get('requesterSessionKey', '')
|
|
115
134
|
|
|
116
|
-
# 解析执行者 Agent
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
# 解析执行者 Agent(会话 key 内为小写,映射到配置中的原始 id)
|
|
136
|
+
raw_child = _parse_agent_id(child_key)
|
|
137
|
+
raw_requester = _parse_agent_id(requester_key)
|
|
138
|
+
child_agent_id = canonical_agent_id_from_config(raw_child) if raw_child else ''
|
|
139
|
+
requester_agent_id = (
|
|
140
|
+
canonical_agent_id_from_config(raw_requester) if raw_requester else ''
|
|
141
|
+
)
|
|
120
142
|
|
|
121
143
|
if not child_agent_id:
|
|
122
144
|
continue
|
|
@@ -141,9 +163,17 @@ def _build_agent_active_tasks(
|
|
|
141
163
|
}
|
|
142
164
|
|
|
143
165
|
# 如果有派发者,添加 childAgentId(用于主 Agent 显示任务流向)
|
|
144
|
-
if requester_agent_id and requester_agent_id
|
|
166
|
+
if requester_agent_id and not agent_ids_equal(requester_agent_id, child_agent_id):
|
|
145
167
|
task_item['childAgentId'] = child_agent_id
|
|
146
168
|
|
|
169
|
+
# 自派:派发者与执行者为同一 Agent(如 PM 派给自己)时,同一 run 只记一条,否则会显示「2 个并行任务」
|
|
170
|
+
if requester_agent_id and agent_ids_equal(requester_agent_id, child_agent_id):
|
|
171
|
+
if child_agent_id not in agent_active_tasks:
|
|
172
|
+
agent_active_tasks[child_agent_id] = []
|
|
173
|
+
self_task = {k: v for k, v in task_item.items() if k != 'childAgentId'}
|
|
174
|
+
agent_active_tasks[child_agent_id].append(self_task)
|
|
175
|
+
continue
|
|
176
|
+
|
|
147
177
|
# 1. 添加到派发者(如果派发者是某个已知 agent)
|
|
148
178
|
if requester_agent_id:
|
|
149
179
|
if requester_agent_id not in agent_active_tasks:
|
|
@@ -192,8 +222,6 @@ class CollaborationFlow(BaseModel):
|
|
|
192
222
|
depths: Optional[Dict[str, int]] = None # agentId -> 层级深度 (0=主, 1=子, 2=孙...)
|
|
193
223
|
# 多任务并行展示:每个 Agent 的活跃任务列表
|
|
194
224
|
agentActiveTasks: Optional[Dict[str, List[ActiveTask]]] = None
|
|
195
|
-
# 多任务并行展示:每个 Agent 的活跃任务列表
|
|
196
|
-
agentActiveTasks: Optional[Dict[str, List[ActiveTask]]] = None
|
|
197
225
|
|
|
198
226
|
|
|
199
227
|
def _parse_agent_id(session_key: str) -> str:
|
|
@@ -335,30 +363,11 @@ def _enrich_main_agent_active_tasks_if_needed(
|
|
|
335
363
|
main_agent_id: str,
|
|
336
364
|
) -> Dict[str, List[Dict[str, Any]]]:
|
|
337
365
|
"""
|
|
338
|
-
|
|
339
|
-
|
|
366
|
+
PM 协作状态已与 runs 对齐;不再在「无 subagent run」时用 calculate_agent_status
|
|
367
|
+
伪造一条主会话任务,否则会出现徽章已空闲、卡片仍显示「当前任务」的矛盾。
|
|
368
|
+
主会话 solo 执行中的语义由 /api/agents 等入口展示。
|
|
340
369
|
"""
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if calculate_agent_status(main_agent_id) != 'working':
|
|
344
|
-
return agent_active_tasks
|
|
345
|
-
if agent_active_tasks.get(main_agent_id):
|
|
346
|
-
return agent_active_tasks
|
|
347
|
-
hint = get_current_task(main_agent_id)
|
|
348
|
-
name = _clean_task_name(hint) if hint else ''
|
|
349
|
-
if not name:
|
|
350
|
-
return agent_active_tasks
|
|
351
|
-
merged = dict(agent_active_tasks)
|
|
352
|
-
merged[main_agent_id] = [
|
|
353
|
-
{
|
|
354
|
-
'id': 'task-main-session',
|
|
355
|
-
'name': name,
|
|
356
|
-
'status': 'working',
|
|
357
|
-
'timestamp': None,
|
|
358
|
-
'featureId': None,
|
|
359
|
-
}
|
|
360
|
-
]
|
|
361
|
-
return merged
|
|
370
|
+
return agent_active_tasks
|
|
362
371
|
|
|
363
372
|
|
|
364
373
|
def _get_agent_error_info(agent_id: str) -> Optional[Dict[str, Any]]:
|
|
@@ -420,13 +429,15 @@ def _analyze_stuck_reason(agent_id: str, idle_seconds: int) -> Dict[str, Any]:
|
|
|
420
429
|
}
|
|
421
430
|
"""
|
|
422
431
|
from data.subagent_reader import get_active_runs
|
|
432
|
+
from data.config_reader import normalize_openclaw_agent_id
|
|
423
433
|
|
|
424
434
|
# 检查是否在等待子 agent
|
|
425
435
|
active_runs = get_active_runs()
|
|
436
|
+
prefix = f"agent:{normalize_openclaw_agent_id(agent_id)}:"
|
|
426
437
|
for run in active_runs:
|
|
427
438
|
requester_key = run.get('requesterSessionKey', '')
|
|
428
439
|
# 如果这个 agent 是 requester,说明它在等待子 agent
|
|
429
|
-
if
|
|
440
|
+
if prefix in requester_key:
|
|
430
441
|
child_key = run.get('childSessionKey', '')
|
|
431
442
|
if child_key and ':' in child_key:
|
|
432
443
|
parts = child_key.split(':')
|
|
@@ -533,7 +544,8 @@ async def get_collaboration():
|
|
|
533
544
|
"""获取协作流程数据 - 主 Agent 与子 Agents 的拓扑关系,含模型配置与最近调用"""
|
|
534
545
|
from data.config_reader import (
|
|
535
546
|
get_agents_list, get_agent_models, get_models_configured_by_agents,
|
|
536
|
-
get_model_display_name, get_main_agent_id
|
|
547
|
+
get_model_display_name, get_main_agent_id,
|
|
548
|
+
agent_ids_equal, normalize_openclaw_agent_id, canonical_agent_id_from_config,
|
|
537
549
|
)
|
|
538
550
|
from data.subagent_reader import get_active_runs
|
|
539
551
|
from status.status_calculator import calculate_agent_status, get_current_task
|
|
@@ -553,8 +565,10 @@ async def get_collaboration():
|
|
|
553
565
|
active_runs = get_active_runs()
|
|
554
566
|
|
|
555
567
|
main_agent_id = get_main_agent_id()
|
|
556
|
-
main_agent_config = next(
|
|
557
|
-
|
|
568
|
+
main_agent_config = next(
|
|
569
|
+
(a for a in agents_list if agent_ids_equal(a.get('id'), main_agent_id)), None
|
|
570
|
+
)
|
|
571
|
+
sub_agents_config = [a for a in agents_list if not agent_ids_equal(a.get('id'), main_agent_id)]
|
|
558
572
|
|
|
559
573
|
all_agents = [a for a in agents_list if a.get('id')]
|
|
560
574
|
for agent in all_agents:
|
|
@@ -564,14 +578,8 @@ async def get_collaboration():
|
|
|
564
578
|
recent_calls = _get_recent_model_calls(30)
|
|
565
579
|
|
|
566
580
|
main_display_name = (main_agent_config.get('name') if main_agent_config else None) or "主 Agent"
|
|
567
|
-
#
|
|
568
|
-
|
|
569
|
-
if main_raw == 'down':
|
|
570
|
-
main_status = 'error'
|
|
571
|
-
elif main_raw == 'working':
|
|
572
|
-
main_status = 'working'
|
|
573
|
-
else:
|
|
574
|
-
main_status = 'idle'
|
|
581
|
+
# PM:与连线 activePath 一致,仅用 runs 活跃 + 会话错误(见 _main_agent_status_for_collaboration)
|
|
582
|
+
main_status = _main_agent_status_for_collaboration(main_agent_id)
|
|
575
583
|
|
|
576
584
|
# 获取主 agent 的当前任务和错误信息
|
|
577
585
|
main_current_task = ''
|
|
@@ -579,9 +587,10 @@ async def get_collaboration():
|
|
|
579
587
|
main_stuck = None
|
|
580
588
|
if active_runs:
|
|
581
589
|
# 找到主 agent 作为 requester 的任务
|
|
590
|
+
main_prefix = f"agent:{normalize_openclaw_agent_id(main_agent_id)}:"
|
|
582
591
|
for run in active_runs:
|
|
583
592
|
requester_key = run.get('requesterSessionKey', '')
|
|
584
|
-
if
|
|
593
|
+
if main_prefix in requester_key:
|
|
585
594
|
main_current_task = _clean_task_name(run.get('task', ''))
|
|
586
595
|
break
|
|
587
596
|
if not main_current_task and active_runs:
|
|
@@ -622,9 +631,10 @@ async def get_collaboration():
|
|
|
622
631
|
|
|
623
632
|
# 获取子 agent 的当前任务
|
|
624
633
|
current_task = ''
|
|
634
|
+
child_prefix = f"agent:{normalize_openclaw_agent_id(agent_id)}:"
|
|
625
635
|
for run in active_runs:
|
|
626
636
|
child_key = run.get('childSessionKey', '')
|
|
627
|
-
if
|
|
637
|
+
if child_prefix in child_key:
|
|
628
638
|
current_task = _clean_task_name(run.get('task', ''))
|
|
629
639
|
break
|
|
630
640
|
|
|
@@ -695,6 +705,11 @@ async def get_collaboration():
|
|
|
695
705
|
if not agent_id:
|
|
696
706
|
continue
|
|
697
707
|
|
|
708
|
+
agent_id_canon = canonical_agent_id_from_config(agent_id)
|
|
709
|
+
requester_id_canon = (
|
|
710
|
+
canonical_agent_id_from_config(requester_id) if requester_id else requester_id
|
|
711
|
+
)
|
|
712
|
+
|
|
698
713
|
task_name = _clean_task_name(run.get('task', ''))
|
|
699
714
|
|
|
700
715
|
task_id = f"task-{run.get('runId', agent_id)}"
|
|
@@ -708,8 +723,8 @@ async def get_collaboration():
|
|
|
708
723
|
nodes.append(task_node)
|
|
709
724
|
|
|
710
725
|
edges.append(CollaborationEdge(
|
|
711
|
-
id=f"edge-{
|
|
712
|
-
source=
|
|
726
|
+
id=f"edge-{agent_id_canon}-{task_id}",
|
|
727
|
+
source=agent_id_canon,
|
|
713
728
|
target=task_id,
|
|
714
729
|
type="calls",
|
|
715
730
|
label="执行"
|
|
@@ -718,15 +733,15 @@ async def get_collaboration():
|
|
|
718
733
|
# Spawn 链:主 Agent 派发 -> 子 Agent 执行
|
|
719
734
|
if requester_id and requester_id != agent_id:
|
|
720
735
|
edges.append(CollaborationEdge(
|
|
721
|
-
id=f"edge-spawn-{
|
|
722
|
-
source=
|
|
736
|
+
id=f"edge-spawn-{requester_id_canon}-{task_id}",
|
|
737
|
+
source=requester_id_canon,
|
|
723
738
|
target=task_id,
|
|
724
739
|
type="delegates",
|
|
725
740
|
label="派发"
|
|
726
741
|
))
|
|
727
742
|
# 如果 requester 是主 agent,给主 agent 也添加一个任务节点(用户命令)
|
|
728
743
|
# 移除 main_agent_task_created 限制,支持多任务并行显示
|
|
729
|
-
if requester_id
|
|
744
|
+
if agent_ids_equal(requester_id, main_agent_id):
|
|
730
745
|
# 主 agent 的任务就是用户原始命令
|
|
731
746
|
main_task_id = f"task-main-{run.get('runId', 'current')}"
|
|
732
747
|
main_task_node = CollaborationNode(
|
|
@@ -745,7 +760,7 @@ async def get_collaboration():
|
|
|
745
760
|
label="执行"
|
|
746
761
|
))
|
|
747
762
|
|
|
748
|
-
active_path.extend([main_agent_id,
|
|
763
|
+
active_path.extend([main_agent_id, agent_id_canon, task_id])
|
|
749
764
|
|
|
750
765
|
except Exception as e:
|
|
751
766
|
print(f"Error building collaboration data: {e}")
|
|
@@ -892,8 +907,17 @@ async def get_collaboration_dynamic():
|
|
|
892
907
|
except Exception as e:
|
|
893
908
|
logger.warning(f"Failed to get display status for {aid}: {e}")
|
|
894
909
|
|
|
895
|
-
#
|
|
896
|
-
|
|
910
|
+
# PM:覆盖为与连线同源的状态;并修正 dynamic 文案,避免 calculate_agent_status 的 solo 与卡片矛盾
|
|
911
|
+
main_collab = _main_agent_status_for_collaboration(main_agent_id)
|
|
912
|
+
agent_statuses[main_agent_id] = main_collab
|
|
913
|
+
if main_collab == 'idle':
|
|
914
|
+
agent_dynamic_statuses[main_agent_id] = AgentDisplayStatus(
|
|
915
|
+
status='idle', display='空闲', duration=0, alert=False
|
|
916
|
+
)
|
|
917
|
+
elif main_collab == 'error':
|
|
918
|
+
agent_dynamic_statuses[main_agent_id] = AgentDisplayStatus(
|
|
919
|
+
status='idle', display='检测到错误', duration=0, alert=True
|
|
920
|
+
)
|
|
897
921
|
|
|
898
922
|
# 处理活跃任务(简化:不在流程图中创建任务节点,任务信息由 agentActiveTasks 提供)
|
|
899
923
|
# 任务详情在 Agent 卡片内显示,流程图只显示 Agent 之间的委托关系
|
|
@@ -262,9 +262,10 @@ def _get_session_message_count(child_session_key: str) -> int:
|
|
|
262
262
|
agent_id = parts[1]
|
|
263
263
|
|
|
264
264
|
try:
|
|
265
|
-
from data.config_reader import get_openclaw_root
|
|
265
|
+
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
|
|
266
266
|
openclaw_path = get_openclaw_root()
|
|
267
|
-
|
|
267
|
+
aid = normalize_openclaw_agent_id(agent_id)
|
|
268
|
+
sessions_index = openclaw_path / "agents" / aid / "sessions" / "sessions.json"
|
|
268
269
|
if not sessions_index.exists():
|
|
269
270
|
return 0
|
|
270
271
|
|
|
@@ -274,7 +275,7 @@ def _get_session_message_count(child_session_key: str) -> int:
|
|
|
274
275
|
entry = index_map.get(child_session_key)
|
|
275
276
|
if not entry:
|
|
276
277
|
return 0
|
|
277
|
-
sessions_dir = openclaw_path / "agents" /
|
|
278
|
+
sessions_dir = openclaw_path / "agents" / aid / "sessions"
|
|
278
279
|
session_path = resolve_session_jsonl_path(sessions_dir, entry)
|
|
279
280
|
if not session_path:
|
|
280
281
|
return 0
|
|
@@ -352,9 +353,10 @@ def _extract_subtasks_from_session(child_session_key: str) -> List[Dict[str, Any
|
|
|
352
353
|
agent_id = parts[1]
|
|
353
354
|
|
|
354
355
|
try:
|
|
355
|
-
from data.config_reader import get_openclaw_root
|
|
356
|
+
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
|
|
356
357
|
openclaw_path = get_openclaw_root()
|
|
357
|
-
|
|
358
|
+
aid = normalize_openclaw_agent_id(agent_id)
|
|
359
|
+
sessions_index = openclaw_path / "agents" / aid / "sessions" / "sessions.json"
|
|
358
360
|
if not sessions_index.exists():
|
|
359
361
|
return []
|
|
360
362
|
|
|
@@ -364,7 +366,7 @@ def _extract_subtasks_from_session(child_session_key: str) -> List[Dict[str, Any
|
|
|
364
366
|
entry = index_map.get(child_session_key)
|
|
365
367
|
if not entry:
|
|
366
368
|
return []
|
|
367
|
-
sessions_dir = openclaw_path / "agents" /
|
|
369
|
+
sessions_dir = openclaw_path / "agents" / aid / "sessions"
|
|
368
370
|
session_path = resolve_session_jsonl_path(sessions_dir, entry)
|
|
369
371
|
if not session_path:
|
|
370
372
|
return []
|
|
@@ -512,9 +514,10 @@ def _extract_timeline_from_session(child_session_key: str) -> List[Dict[str, Any
|
|
|
512
514
|
agent_id = parts[1]
|
|
513
515
|
|
|
514
516
|
try:
|
|
515
|
-
from data.config_reader import get_openclaw_root
|
|
517
|
+
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
|
|
516
518
|
openclaw_path = get_openclaw_root()
|
|
517
|
-
|
|
519
|
+
aid = normalize_openclaw_agent_id(agent_id)
|
|
520
|
+
sessions_index = openclaw_path / "agents" / aid / "sessions" / "sessions.json"
|
|
518
521
|
if not sessions_index.exists():
|
|
519
522
|
return []
|
|
520
523
|
|
|
@@ -524,7 +527,7 @@ def _extract_timeline_from_session(child_session_key: str) -> List[Dict[str, Any
|
|
|
524
527
|
entry = index_map.get(child_session_key)
|
|
525
528
|
if not entry:
|
|
526
529
|
return []
|
|
527
|
-
sessions_dir = openclaw_path / "agents" /
|
|
530
|
+
sessions_dir = openclaw_path / "agents" / aid / "sessions"
|
|
528
531
|
session_path = resolve_session_jsonl_path(sessions_dir, entry)
|
|
529
532
|
if not session_path:
|
|
530
533
|
return []
|
|
@@ -13,7 +13,7 @@ LOG = logging.getLogger(__name__)
|
|
|
13
13
|
sys.path.append(str(Path(__file__).parent.parent))
|
|
14
14
|
|
|
15
15
|
from data.timeline_reader import get_timeline_steps, StepType, StepStatus
|
|
16
|
-
from data.config_reader import
|
|
16
|
+
from data.config_reader import get_agent_config
|
|
17
17
|
|
|
18
18
|
router = APIRouter()
|
|
19
19
|
|
|
@@ -70,14 +70,7 @@ async def get_timeline(
|
|
|
70
70
|
- 工具调用及结果
|
|
71
71
|
- 错误信息
|
|
72
72
|
"""
|
|
73
|
-
|
|
74
|
-
agents = get_agents_list()
|
|
75
|
-
agent_info = None
|
|
76
|
-
for a in agents:
|
|
77
|
-
if a.get('id') == agent_id:
|
|
78
|
-
agent_info = a
|
|
79
|
-
break
|
|
80
|
-
|
|
73
|
+
agent_info = get_agent_config(agent_id)
|
|
81
74
|
if not agent_info:
|
|
82
75
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
83
76
|
|
|
@@ -118,8 +111,7 @@ async def get_timeline_steps_only(
|
|
|
118
111
|
|
|
119
112
|
可按步骤类型过滤
|
|
120
113
|
"""
|
|
121
|
-
|
|
122
|
-
if not any(a.get('id') == agent_id for a in agents):
|
|
114
|
+
if not get_agent_config(agent_id):
|
|
123
115
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
124
116
|
|
|
125
117
|
result = get_timeline_steps(agent_id, session_key, limit)
|
|
@@ -139,8 +131,7 @@ async def get_timeline_summary(agent_id: str, session_key: Optional[str] = Query
|
|
|
139
131
|
|
|
140
132
|
快速查看会话概览,不返回详细步骤
|
|
141
133
|
"""
|
|
142
|
-
|
|
143
|
-
if not any(a.get('id') == agent_id for a in agents):
|
|
134
|
+
if not get_agent_config(agent_id):
|
|
144
135
|
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
|
|
145
136
|
|
|
146
137
|
result = get_timeline_steps(agent_id, session_key, limit=10) # 只需基本信息
|
|
@@ -113,7 +113,7 @@ async def get_collaboration():
|
|
|
113
113
|
"""获取协作流程数据 - 主 Agent 与子 Agents 的拓扑关系,含模型配置与最近调用"""
|
|
114
114
|
from data.config_reader import (
|
|
115
115
|
get_agents_list, get_agent_models, get_models_configured_by_agents,
|
|
116
|
-
get_model_display_name, get_main_agent_id
|
|
116
|
+
get_model_display_name, get_main_agent_id, agent_ids_equal,
|
|
117
117
|
)
|
|
118
118
|
from data.subagent_reader import get_active_runs
|
|
119
119
|
from status.status_calculator import calculate_agent_status
|
|
@@ -132,8 +132,10 @@ async def get_collaboration():
|
|
|
132
132
|
active_runs = get_active_runs()
|
|
133
133
|
|
|
134
134
|
main_agent_id = get_main_agent_id()
|
|
135
|
-
main_agent_config = next(
|
|
136
|
-
|
|
135
|
+
main_agent_config = next(
|
|
136
|
+
(a for a in agents_list if agent_ids_equal(a.get('id'), main_agent_id)), None
|
|
137
|
+
)
|
|
138
|
+
sub_agents_config = [a for a in agents_list if not agent_ids_equal(a.get('id'), main_agent_id)]
|
|
137
139
|
|
|
138
140
|
all_agents = [a for a in agents_list if a.get('id')]
|
|
139
141
|
for agent in all_agents:
|
|
@@ -7,7 +7,7 @@ from typing import Dict, Any, List, Optional
|
|
|
7
7
|
import shutil
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
|
|
10
|
-
from data.config_reader import get_openclaw_root
|
|
10
|
+
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id, agent_ids_equal
|
|
11
11
|
from data.session_reader import normalize_sessions_index
|
|
12
12
|
|
|
13
13
|
|
|
@@ -52,7 +52,7 @@ def get_agent_config(agent_id: str) -> Optional[Dict[str, Any]]:
|
|
|
52
52
|
agent_list = agents.get('list', [])
|
|
53
53
|
|
|
54
54
|
for agent in agent_list:
|
|
55
|
-
if agent.get('id')
|
|
55
|
+
if agent_ids_equal(agent.get('id'), agent_id):
|
|
56
56
|
return agent
|
|
57
57
|
return None
|
|
58
58
|
|
|
@@ -192,7 +192,7 @@ def update_agent_model(agent_id: str, primary: Optional[str] = None, fallbacks:
|
|
|
192
192
|
|
|
193
193
|
found = False
|
|
194
194
|
for agent in agent_list:
|
|
195
|
-
if agent.get('id')
|
|
195
|
+
if agent_ids_equal(agent.get('id'), agent_id):
|
|
196
196
|
if 'model' not in agent:
|
|
197
197
|
agent['model'] = {}
|
|
198
198
|
|
|
@@ -223,7 +223,8 @@ def get_agent_full_info(agent_id: str) -> Dict[str, Any]:
|
|
|
223
223
|
model_config = get_agent_model_config(agent_id)
|
|
224
224
|
|
|
225
225
|
# 检查运行状态
|
|
226
|
-
|
|
226
|
+
aid = normalize_openclaw_agent_id(agent_id)
|
|
227
|
+
session_file = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
|
|
227
228
|
status = 'idle'
|
|
228
229
|
last_active = None
|
|
229
230
|
|
|
@@ -242,7 +243,7 @@ def get_agent_full_info(agent_id: str) -> Dict[str, Any]:
|
|
|
242
243
|
|
|
243
244
|
return {
|
|
244
245
|
'found': True,
|
|
245
|
-
'id': agent_id,
|
|
246
|
+
'id': agent_config.get('id', agent_id),
|
|
246
247
|
'name': agent_config.get('name', agent_id),
|
|
247
248
|
'workspace': agent_config.get('workspace', ''),
|
|
248
249
|
'model': model_config,
|
|
@@ -43,8 +43,10 @@ def _get_agent_info(agent_id: str) -> Dict[str, Any]:
|
|
|
43
43
|
"""获取单个 agent 的信息"""
|
|
44
44
|
config = _get_agents_config()
|
|
45
45
|
agents = config.get('agents', {}).get('list', [])
|
|
46
|
+
from data.config_reader import agent_ids_equal
|
|
47
|
+
|
|
46
48
|
for a in agents:
|
|
47
|
-
if a.get('id')
|
|
49
|
+
if agent_ids_equal(a.get('id'), agent_id):
|
|
48
50
|
return a
|
|
49
51
|
return {}
|
|
50
52
|
|
|
@@ -4,8 +4,46 @@
|
|
|
4
4
|
"""
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import List, Dict, Any
|
|
9
|
+
from typing import List, Dict, Any, Optional
|
|
10
|
+
|
|
11
|
+
# 与 OpenClaw core 的 normalizeAgentId 对齐(dist/session-key-*.js)
|
|
12
|
+
_DEFAULT_OPENCLAW_AGENT_ID = "main"
|
|
13
|
+
_VALID_OPENCLAW_AGENT_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$", re.IGNORECASE)
|
|
14
|
+
_INVALID_OPENCLAW_AGENT_CHARS_RE = re.compile(r"[^a-z0-9_-]+")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize_openclaw_agent_id(value: Optional[str]) -> str:
|
|
18
|
+
"""
|
|
19
|
+
将 agents.list[].id 规范化为状态目录名(agents/<id>/sessions)。
|
|
20
|
+
OpenClaw 落盘时始终使用该形式;配置里可写任意大小写。
|
|
21
|
+
"""
|
|
22
|
+
trimmed = (value or "").strip()
|
|
23
|
+
if not trimmed:
|
|
24
|
+
return _DEFAULT_OPENCLAW_AGENT_ID
|
|
25
|
+
normalized = trimmed.lower()
|
|
26
|
+
if _VALID_OPENCLAW_AGENT_ID_RE.match(trimmed):
|
|
27
|
+
return normalized
|
|
28
|
+
sanitized = _INVALID_OPENCLAW_AGENT_CHARS_RE.sub("-", normalized)
|
|
29
|
+
sanitized = sanitized.lstrip("-").rstrip("-")[:64] or _DEFAULT_OPENCLAW_AGENT_ID
|
|
30
|
+
return sanitized
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def agent_ids_equal(a: Optional[str], b: Optional[str]) -> bool:
|
|
34
|
+
"""两枚 Agent ID 是否在 OpenClaw 语义下相同(大小写/规范化后)。"""
|
|
35
|
+
return normalize_openclaw_agent_id(a) == normalize_openclaw_agent_id(b)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def canonical_agent_id_from_config(agent_id: str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
返回 agents.list 中条目的原始 id(与配置一致),供 UI 节点 id 与边 source/target 对齐。
|
|
41
|
+
若配置中无匹配,则回退为规范化 id。
|
|
42
|
+
"""
|
|
43
|
+
cfg = get_agent_config(agent_id)
|
|
44
|
+
if cfg and cfg.get("id"):
|
|
45
|
+
return str(cfg["id"])
|
|
46
|
+
return normalize_openclaw_agent_id(agent_id)
|
|
9
47
|
|
|
10
48
|
|
|
11
49
|
def get_openclaw_root() -> Path:
|
|
@@ -81,10 +119,11 @@ def get_workspace_paths() -> List[Path]:
|
|
|
81
119
|
|
|
82
120
|
|
|
83
121
|
def get_agent_config(agent_id: str) -> Dict[str, Any]:
|
|
84
|
-
"""获取单个 Agent
|
|
122
|
+
"""获取单个 Agent 配置(id 与 openclaw.json 中条目大小写可不一致)。"""
|
|
85
123
|
agents = get_agents_list()
|
|
124
|
+
target = normalize_openclaw_agent_id(agent_id)
|
|
86
125
|
for agent in agents:
|
|
87
|
-
if agent.get(
|
|
126
|
+
if normalize_openclaw_agent_id(agent.get("id")) == target:
|
|
88
127
|
return agent
|
|
89
128
|
return {}
|
|
90
129
|
|
|
@@ -31,7 +31,7 @@ class ErrorSeverity(Enum):
|
|
|
31
31
|
LOW = "low" # 轻微错误,可忽略
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
from data.config_reader import get_openclaw_root
|
|
34
|
+
from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
# 错误模式匹配规则
|
|
@@ -313,7 +313,8 @@ def get_tool_call_chain(session_path: Path, before_turn: int, limit: int = 10) -
|
|
|
313
313
|
|
|
314
314
|
def analyze_agent_errors(agent_id: str, session_limit: int = 5) -> Dict[str, Any]:
|
|
315
315
|
"""分析 Agent 的错误情况"""
|
|
316
|
-
|
|
316
|
+
aid = normalize_openclaw_agent_id(agent_id)
|
|
317
|
+
sessions_dir = get_openclaw_root() / "agents" / aid / "sessions"
|
|
317
318
|
if not sessions_dir.exists():
|
|
318
319
|
return {'agentId': agent_id, 'error': 'Sessions directory not found', 'errors': []}
|
|
319
320
|
|
|
@@ -415,7 +416,8 @@ def analyze_all_agents_errors() -> Dict[str, Any]:
|
|
|
415
416
|
|
|
416
417
|
def get_error_detail(agent_id: str, session_file: str, turn_index: int) -> Optional[Dict[str, Any]]:
|
|
417
418
|
"""获取单个错误的详细信息"""
|
|
418
|
-
|
|
419
|
+
aid = normalize_openclaw_agent_id(agent_id)
|
|
420
|
+
session_path = get_openclaw_root() / "agents" / aid / "sessions" / session_file
|
|
419
421
|
if not session_path.exists():
|
|
420
422
|
return None
|
|
421
423
|
|