openclaw-agent-dashboard 1.0.35 → 1.0.37

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
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Agent API 路由
3
3
  """
4
- from fastapi import APIRouter, HTTPException
4
+ from fastapi import APIRouter
5
5
  from pydantic import BaseModel
6
6
  from typing import List, Optional
7
7
  import sys
@@ -45,43 +45,18 @@ 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
53
55
 
56
+ from fastapi import HTTPException
54
57
  raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
55
58
 
56
59
 
57
- class TimelineContextResponse(BaseModel):
58
- """供「实时执行时序」与 runs.json 对齐:使用最新 run 的 childSessionKey 解析独立会话。"""
59
- childSessionKey: Optional[str] = None
60
-
61
-
62
- @router.get("/agents/{agent_id}/timeline-context", response_model=TimelineContextResponse)
63
- async def get_agent_timeline_context(agent_id: str):
64
- """
65
- 返回该 Agent 在 runs.json 中最近一条 run 的 childSessionKey(若有)。
66
- 前端传给 GET /api/timeline/{agent_id}?session_key= 以命中 sessions.json 指定 jsonl,
67
- 避免仅靠 mtime 选文件或误扫主会话合并路径。
68
- """
69
- from data.config_reader import get_agents_list
70
- from data.subagent_reader import get_agent_runs
71
-
72
- agents = get_agents_list()
73
- if not any(a.get("id") == agent_id for a in agents):
74
- raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
75
-
76
- runs = get_agent_runs(agent_id, limit=1)
77
- key = None
78
- if runs:
79
- key = runs[0].get("childSessionKey") or None
80
- if isinstance(key, str) and not key.strip():
81
- key = None
82
- return TimelineContextResponse(childSessionKey=key)
83
-
84
-
85
60
  @router.get("/agents/{agent_id}/output")
86
61
  async def get_agent_output(agent_id: str, limit: int = 50):
87
62
  """
@@ -89,10 +64,10 @@ async def get_agent_output(agent_id: str, limit: int = 50):
89
64
  用于调试视图展示
90
65
  """
91
66
  from data.session_reader import get_session_turns
92
- from data.config_reader import get_agents_list
93
-
94
- agents = get_agents_list()
95
- 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):
70
+ from fastapi import HTTPException
96
71
  raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
97
72
 
98
73
  turns = get_session_turns(agent_id, limit=limit)
@@ -107,16 +107,21 @@ def _build_agent_active_tasks(
107
107
  ...
108
108
  }
109
109
  """
110
+ from data.config_reader import agent_ids_equal, canonical_agent_id_from_config
111
+
110
112
  agent_active_tasks: Dict[str, List[Dict[str, Any]]] = {}
111
113
 
112
114
  for run in active_runs:
113
115
  child_key = run.get('childSessionKey', '')
114
116
  requester_key = run.get('requesterSessionKey', '')
115
117
 
116
- # 解析执行者 Agent
117
- child_agent_id = _parse_agent_id(child_key)
118
- # 解析派发者 Agent
119
- requester_agent_id = _parse_agent_id(requester_key)
118
+ # 解析执行者 Agent(会话 key 内为小写,映射到配置中的原始 id)
119
+ raw_child = _parse_agent_id(child_key)
120
+ raw_requester = _parse_agent_id(requester_key)
121
+ child_agent_id = canonical_agent_id_from_config(raw_child) if raw_child else ''
122
+ requester_agent_id = (
123
+ canonical_agent_id_from_config(raw_requester) if raw_requester else ''
124
+ )
120
125
 
121
126
  if not child_agent_id:
122
127
  continue
@@ -141,7 +146,7 @@ def _build_agent_active_tasks(
141
146
  }
142
147
 
143
148
  # 如果有派发者,添加 childAgentId(用于主 Agent 显示任务流向)
144
- if requester_agent_id and requester_agent_id != child_agent_id:
149
+ if requester_agent_id and not agent_ids_equal(requester_agent_id, child_agent_id):
145
150
  task_item['childAgentId'] = child_agent_id
146
151
 
147
152
  # 1. 添加到派发者(如果派发者是某个已知 agent)
@@ -420,13 +425,15 @@ def _analyze_stuck_reason(agent_id: str, idle_seconds: int) -> Dict[str, Any]:
420
425
  }
421
426
  """
422
427
  from data.subagent_reader import get_active_runs
428
+ from data.config_reader import normalize_openclaw_agent_id
423
429
 
424
430
  # 检查是否在等待子 agent
425
431
  active_runs = get_active_runs()
432
+ prefix = f"agent:{normalize_openclaw_agent_id(agent_id)}:"
426
433
  for run in active_runs:
427
434
  requester_key = run.get('requesterSessionKey', '')
428
435
  # 如果这个 agent 是 requester,说明它在等待子 agent
429
- if f'agent:{agent_id}:' in requester_key:
436
+ if prefix in requester_key:
430
437
  child_key = run.get('childSessionKey', '')
431
438
  if child_key and ':' in child_key:
432
439
  parts = child_key.split(':')
@@ -533,7 +540,8 @@ async def get_collaboration():
533
540
  """获取协作流程数据 - 主 Agent 与子 Agents 的拓扑关系,含模型配置与最近调用"""
534
541
  from data.config_reader import (
535
542
  get_agents_list, get_agent_models, get_models_configured_by_agents,
536
- get_model_display_name, get_main_agent_id
543
+ get_model_display_name, get_main_agent_id,
544
+ agent_ids_equal, normalize_openclaw_agent_id, canonical_agent_id_from_config,
537
545
  )
538
546
  from data.subagent_reader import get_active_runs
539
547
  from status.status_calculator import calculate_agent_status, get_current_task
@@ -553,8 +561,10 @@ async def get_collaboration():
553
561
  active_runs = get_active_runs()
554
562
 
555
563
  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]
564
+ main_agent_config = next(
565
+ (a for a in agents_list if agent_ids_equal(a.get('id'), main_agent_id)), None
566
+ )
567
+ sub_agents_config = [a for a in agents_list if not agent_ids_equal(a.get('id'), main_agent_id)]
558
568
 
559
569
  all_agents = [a for a in agents_list if a.get('id')]
560
570
  for agent in all_agents:
@@ -579,9 +589,10 @@ async def get_collaboration():
579
589
  main_stuck = None
580
590
  if active_runs:
581
591
  # 找到主 agent 作为 requester 的任务
592
+ main_prefix = f"agent:{normalize_openclaw_agent_id(main_agent_id)}:"
582
593
  for run in active_runs:
583
594
  requester_key = run.get('requesterSessionKey', '')
584
- if f'agent:{main_agent_id}:' in requester_key:
595
+ if main_prefix in requester_key:
585
596
  main_current_task = _clean_task_name(run.get('task', ''))
586
597
  break
587
598
  if not main_current_task and active_runs:
@@ -622,9 +633,10 @@ async def get_collaboration():
622
633
 
623
634
  # 获取子 agent 的当前任务
624
635
  current_task = ''
636
+ child_prefix = f"agent:{normalize_openclaw_agent_id(agent_id)}:"
625
637
  for run in active_runs:
626
638
  child_key = run.get('childSessionKey', '')
627
- if f'agent:{agent_id}:' in child_key:
639
+ if child_prefix in child_key:
628
640
  current_task = _clean_task_name(run.get('task', ''))
629
641
  break
630
642
 
@@ -695,6 +707,11 @@ async def get_collaboration():
695
707
  if not agent_id:
696
708
  continue
697
709
 
710
+ agent_id_canon = canonical_agent_id_from_config(agent_id)
711
+ requester_id_canon = (
712
+ canonical_agent_id_from_config(requester_id) if requester_id else requester_id
713
+ )
714
+
698
715
  task_name = _clean_task_name(run.get('task', ''))
699
716
 
700
717
  task_id = f"task-{run.get('runId', agent_id)}"
@@ -708,8 +725,8 @@ async def get_collaboration():
708
725
  nodes.append(task_node)
709
726
 
710
727
  edges.append(CollaborationEdge(
711
- id=f"edge-{agent_id}-{task_id}",
712
- source=agent_id,
728
+ id=f"edge-{agent_id_canon}-{task_id}",
729
+ source=agent_id_canon,
713
730
  target=task_id,
714
731
  type="calls",
715
732
  label="执行"
@@ -718,15 +735,15 @@ async def get_collaboration():
718
735
  # Spawn 链:主 Agent 派发 -> 子 Agent 执行
719
736
  if requester_id and requester_id != agent_id:
720
737
  edges.append(CollaborationEdge(
721
- id=f"edge-spawn-{requester_id}-{task_id}",
722
- source=requester_id,
738
+ id=f"edge-spawn-{requester_id_canon}-{task_id}",
739
+ source=requester_id_canon,
723
740
  target=task_id,
724
741
  type="delegates",
725
742
  label="派发"
726
743
  ))
727
744
  # 如果 requester 是主 agent,给主 agent 也添加一个任务节点(用户命令)
728
745
  # 移除 main_agent_task_created 限制,支持多任务并行显示
729
- if requester_id == main_agent_id:
746
+ if agent_ids_equal(requester_id, main_agent_id):
730
747
  # 主 agent 的任务就是用户原始命令
731
748
  main_task_id = f"task-main-{run.get('runId', 'current')}"
732
749
  main_task_node = CollaborationNode(
@@ -745,7 +762,7 @@ async def get_collaboration():
745
762
  label="执行"
746
763
  ))
747
764
 
748
- active_path.extend([main_agent_id, agent_id, task_id])
765
+ active_path.extend([main_agent_id, agent_id_canon, task_id])
749
766
 
750
767
  except Exception as e:
751
768
  print(f"Error building collaboration data: {e}")
@@ -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 []
@@ -1,15 +1,19 @@
1
1
  """
2
2
  Timeline API 路由 - 实时执行时序图
3
3
  """
4
+ import logging
5
+ import time
4
6
  from fastapi import APIRouter, Query, HTTPException
5
7
  from pydantic import BaseModel
6
8
  from typing import Optional, List, Dict, Any
7
9
  import sys
8
10
  from pathlib import Path
11
+
12
+ LOG = logging.getLogger(__name__)
9
13
  sys.path.append(str(Path(__file__).parent.parent))
10
14
 
11
15
  from data.timeline_reader import get_timeline_steps, StepType, StepStatus
12
- from data.config_reader import get_agents_list
16
+ from data.config_reader import get_agent_config
13
17
 
14
18
  router = APIRouter()
15
19
 
@@ -38,13 +42,17 @@ class TimelineResponse(BaseModel):
38
42
  agentName: Optional[str] = None
39
43
  model: Optional[str] = None
40
44
  startedAt: Optional[int] = None
45
+ runStartedAt: Optional[int] = None
41
46
  status: str
42
47
  steps: List[Dict[str, Any]]
43
48
  stats: TimelineStats
44
49
  message: Optional[str] = None
50
+ # 主 Agent 无会话文件时由后端置 True,避免前端误用「子代理」空态文案
51
+ isMainAgent: Optional[bool] = None
45
52
  # LLM 轮次分组
46
53
  rounds: Optional[List[LLMRound]] = None
47
54
  roundMode: Optional[bool] = None
55
+ dataSource: Optional[str] = None
48
56
 
49
57
 
50
58
  @router.get("/timeline/{agent_id}", response_model=TimelineResponse)
@@ -62,19 +70,21 @@ async def get_timeline(
62
70
  - 工具调用及结果
63
71
  - 错误信息
64
72
  """
65
- # 验证 agent_id
66
- agents = get_agents_list()
67
- agent_info = None
68
- for a in agents:
69
- if a.get('id') == agent_id:
70
- agent_info = a
71
- break
72
-
73
+ agent_info = get_agent_config(agent_id)
73
74
  if not agent_info:
74
75
  raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
75
76
 
76
- # 获取时序数据
77
+ t0 = time.perf_counter()
77
78
  result = get_timeline_steps(agent_id, session_key, limit)
79
+ elapsed_ms = (time.perf_counter() - t0) * 1000
80
+ if elapsed_ms >= 200.0:
81
+ LOG.info(
82
+ "timeline agent=%s limit=%d steps=%d ms=%.1f",
83
+ agent_id,
84
+ limit,
85
+ len(result.get("steps", [])),
86
+ elapsed_ms,
87
+ )
78
88
 
79
89
  # 补充 Agent 信息
80
90
  result['agentName'] = agent_info.get('name', agent_id)
@@ -101,8 +111,7 @@ async def get_timeline_steps_only(
101
111
 
102
112
  可按步骤类型过滤
103
113
  """
104
- agents = get_agents_list()
105
- if not any(a.get('id') == agent_id for a in agents):
114
+ if not get_agent_config(agent_id):
106
115
  raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
107
116
 
108
117
  result = get_timeline_steps(agent_id, session_key, limit)
@@ -122,8 +131,7 @@ async def get_timeline_summary(agent_id: str, session_key: Optional[str] = Query
122
131
 
123
132
  快速查看会话概览,不返回详细步骤
124
133
  """
125
- agents = get_agents_list()
126
- if not any(a.get('id') == agent_id for a in agents):
134
+ if not get_agent_config(agent_id):
127
135
  raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
128
136
 
129
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:
@@ -52,8 +90,11 @@ def get_agents_list() -> List[Dict[str, Any]]:
52
90
 
53
91
 
54
92
  def get_main_agent_id() -> str:
55
- """获取主 Agent ID(配置中 id 为 main 的,或列表第一个)"""
93
+ """获取主 Agent ID:优先 default:true,其次 id 为 main,否则列表第一项。"""
56
94
  agents = get_agents_list()
95
+ for a in agents:
96
+ if a.get('default') is True:
97
+ return a.get('id', 'main')
57
98
  for a in agents:
58
99
  if a.get('id') == 'main':
59
100
  return 'main'
@@ -78,10 +119,11 @@ def get_workspace_paths() -> List[Path]:
78
119
 
79
120
 
80
121
  def get_agent_config(agent_id: str) -> Dict[str, Any]:
81
- """获取单个 Agent 配置"""
122
+ """获取单个 Agent 配置(id 与 openclaw.json 中条目大小写可不一致)。"""
82
123
  agents = get_agents_list()
124
+ target = normalize_openclaw_agent_id(agent_id)
83
125
  for agent in agents:
84
- if agent.get('id') == agent_id:
126
+ if normalize_openclaw_agent_id(agent.get("id")) == target:
85
127
  return agent
86
128
  return {}
87
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
 
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from typing import List, Dict, Any, Optional
8
8
 
9
9
 
10
- from data.config_reader import get_openclaw_root
10
+ from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
11
11
 
12
12
  _META_SESSION_INDEX_KEYS = frozenset({"entries", "version", "schema"})
13
13
 
@@ -70,7 +70,7 @@ def resolve_session_jsonl_path(sessions_dir: Path, entry: Dict[str, Any]) -> Opt
70
70
 
71
71
  def get_agent_sessions_path(agent_id: str) -> Optional[Path]:
72
72
  """获取 Agent 的 sessions 目录"""
73
- sessions_path = get_openclaw_root() / "agents" / agent_id / "sessions"
73
+ sessions_path = get_openclaw_root() / "agents" / normalize_openclaw_agent_id(agent_id) / "sessions"
74
74
  if not sessions_path.exists():
75
75
  return None
76
76
  return sessions_path
@@ -211,7 +211,8 @@ def get_session_updated_at(agent_id: str) -> int:
211
211
  获取 Agent 会话的最后更新时间(sessions.json 中 updatedAt 的最大值)
212
212
  用于判断「最近 5 分钟是否有 session 活动」
213
213
  """
214
- sessions_index = get_openclaw_root() / "agents" / agent_id / "sessions" / "sessions.json"
214
+ aid = normalize_openclaw_agent_id(agent_id)
215
+ sessions_index = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
215
216
  if not sessions_index.exists():
216
217
  return 0
217
218
 
@@ -261,12 +262,13 @@ def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: i
261
262
  解析 jsonl 获取会话轮次,每轮包含 user/assistant/toolResult 及 usage
262
263
  返回格式: [{ turnIndex, role, content, usage?, toolCalls?, stopReason?, timestamp }]
263
264
  """
264
- sessions_index = get_openclaw_root() / "agents" / agent_id / "sessions" / "sessions.json"
265
+ aid = normalize_openclaw_agent_id(agent_id)
266
+ sessions_index = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
265
267
  if not sessions_index.exists():
266
268
  return []
267
269
 
268
270
  session_file: Optional[Path] = None
269
- sessions_path = get_openclaw_root() / "agents" / agent_id / "sessions"
271
+ sessions_path = get_openclaw_root() / "agents" / aid / "sessions"
270
272
  if session_key:
271
273
  try:
272
274
  with open(sessions_index, 'r', encoding='utf-8') as f: