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.
- package/dashboard/agents.py +6 -5
- package/dashboard/api/agents.py +9 -34
- package/dashboard/api/collaboration.py +34 -17
- package/dashboard/api/subagents.py +12 -9
- package/dashboard/api/timeline.py +22 -14
- 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 +46 -4
- package/dashboard/data/error_analyzer.py +5 -3
- package/dashboard/data/session_reader.py +7 -5
- package/dashboard/data/subagent_reader.py +10 -8
- package/dashboard/data/timeline_reader.py +370 -176
- 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-CVnP0JTr.js +24 -0
- package/frontend-dist/assets/index-DsWSoANz.css +1 -0
- package/frontend-dist/index.html +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/frontend-dist/assets/index-BkM6T7k-.js +0 -24
- package/frontend-dist/assets/index-CFl9xHR7.css +0 -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Agent API 路由
|
|
3
3
|
"""
|
|
4
|
-
from fastapi import APIRouter
|
|
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']
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
|
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(
|
|
557
|
-
|
|
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
|
|
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
|
|
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-{
|
|
712
|
-
source=
|
|
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-{
|
|
722
|
-
source=
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 []
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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" /
|
|
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:
|