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.
@@ -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'] == 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 get_agents_list
66
-
67
- agents = get_agents_list()
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
 
@@ -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'] == 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 get_agents_list
66
-
67
- agents = get_agents_list()
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
- child_agent_id = _parse_agent_id(child_key)
118
- # 解析派发者 Agent
119
- requester_agent_id = _parse_agent_id(requester_key)
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 != child_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
- Agent 仅更新主会话、subagents/runs 无条目时,agentActiveTasks 为空;
339
- 在仍为 working 时补一条会话摘要,便于卡片「并行任务」区有文案。
366
+ PM 协作状态已与 runs 对齐;不再在「无 subagent run」时用 calculate_agent_status
367
+ 伪造一条主会话任务,否则会出现徽章已空闲、卡片仍显示「当前任务」的矛盾。
368
+ 主会话 solo 执行中的语义由 /api/agents 等入口展示。
340
369
  """
341
- from status.status_calculator import calculate_agent_status, get_current_task
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 f'agent:{agent_id}:' in requester_key:
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((a for a in agents_list if a.get('id') == main_agent_id), None)
557
- sub_agents_config = [a for a in agents_list if a.get('id') != main_agent_id]
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
- # 与子 Agent 一致:基于 runs + session 活动判断;独立 PM 无 subagent run 时仍可能在工作
568
- main_raw = calculate_agent_status(main_agent_id)
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 f'agent:{main_agent_id}:' in requester_key:
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 f'agent:{agent_id}:' in child_key:
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-{agent_id}-{task_id}",
712
- source=agent_id,
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-{requester_id}-{task_id}",
722
- source=requester_id,
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 == main_agent_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, agent_id, task_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
- # Agent 状态已在上方循环中与 calculate_agent_status 一致,勿再用「有无 active_runs」覆盖
896
- # (独立 PM 仅更新主会话时 runs 可能为空,否则会误显示空闲)
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
- sessions_index = openclaw_path / "agents" / agent_id / "sessions" / "sessions.json"
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" / agent_id / "sessions"
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
- sessions_index = openclaw_path / "agents" / agent_id / "sessions" / "sessions.json"
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" / agent_id / "sessions"
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
- sessions_index = openclaw_path / "agents" / agent_id / "sessions" / "sessions.json"
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" / agent_id / "sessions"
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 get_agents_list
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
- # 验证 agent_id
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
- agents = get_agents_list()
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
- agents = get_agents_list()
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((a for a in agents_list if a.get('id') == main_agent_id), None)
136
- sub_agents_config = [a for a in agents_list if a.get('id') != main_agent_id]
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') == agent_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') == agent_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
- session_file = get_openclaw_root() / "agents" / agent_id / "sessions" / "sessions.json"
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') == agent_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('id') == agent_id:
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
- sessions_dir = get_openclaw_root() / "agents" / agent_id / "sessions"
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
- session_path = get_openclaw_root() / "agents" / agent_id / "sessions" / session_file
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