openclaw-agent-dashboard 1.0.4
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/.github/workflows/release.yml +56 -0
- package/README.md +302 -0
- package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +132 -0
- package/docs/RELEASE-LATEST.md +189 -0
- package/docs/RELEASE-MODEL-CONFIG.md +95 -0
- package/docs/release-guide.md +259 -0
- package/docs/release-operations-manual.md +167 -0
- package/docs/specs/tr3-install-system.md +580 -0
- package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
- package/frontend/index.html +12 -0
- package/frontend/package-lock.json +1240 -0
- package/frontend/package.json +19 -0
- package/frontend/src/App.vue +331 -0
- package/frontend/src/components/AgentCard.vue +796 -0
- package/frontend/src/components/AgentConfigPanel.vue +539 -0
- package/frontend/src/components/AgentDetailPanel.vue +738 -0
- package/frontend/src/components/ErrorAnalysisView.vue +546 -0
- package/frontend/src/components/ErrorCenterPanel.vue +844 -0
- package/frontend/src/components/PerformanceMonitor.vue +515 -0
- package/frontend/src/components/SettingsPanel.vue +236 -0
- package/frontend/src/components/TokenAnalysisPanel.vue +683 -0
- package/frontend/src/components/chain/ChainEdge.vue +85 -0
- package/frontend/src/components/chain/ChainNode.vue +166 -0
- package/frontend/src/components/chain/TaskChainView.vue +425 -0
- package/frontend/src/components/chain/index.ts +3 -0
- package/frontend/src/components/chain/types.ts +70 -0
- package/frontend/src/components/collaboration/CollaborationFlowSection.vue +1032 -0
- package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +113 -0
- package/frontend/src/components/performance/PerformancePanel.vue +119 -0
- package/frontend/src/components/performance/PerformanceSection.vue +1137 -0
- package/frontend/src/components/tasks/TaskStatusSection.vue +973 -0
- package/frontend/src/components/timeline/TimelineConnector.vue +31 -0
- package/frontend/src/components/timeline/TimelineRound.vue +135 -0
- package/frontend/src/components/timeline/TimelineStep.vue +691 -0
- package/frontend/src/components/timeline/TimelineToolLink.vue +109 -0
- package/frontend/src/components/timeline/TimelineView.vue +540 -0
- package/frontend/src/components/timeline/index.ts +5 -0
- package/frontend/src/components/timeline/types.ts +120 -0
- package/frontend/src/composables/index.ts +7 -0
- package/frontend/src/composables/useDebounce.ts +48 -0
- package/frontend/src/composables/useRealtime.ts +52 -0
- package/frontend/src/composables/useState.ts +52 -0
- package/frontend/src/composables/useThrottle.ts +46 -0
- package/frontend/src/composables/useVirtualScroll.ts +106 -0
- package/frontend/src/main.ts +4 -0
- package/frontend/src/managers/EventDispatcher.ts +127 -0
- package/frontend/src/managers/RealtimeDataManager.ts +293 -0
- package/frontend/src/managers/StateManager.ts +128 -0
- package/frontend/src/managers/index.ts +5 -0
- package/frontend/src/types/collaboration.ts +135 -0
- package/frontend/src/types/index.ts +20 -0
- package/frontend/src/types/performance.ts +105 -0
- package/frontend/src/types/task.ts +38 -0
- package/frontend/vite.config.ts +18 -0
- package/package.json +22 -0
- package/plugin/README.md +99 -0
- package/plugin/config.json.example +1 -0
- package/plugin/index.js +250 -0
- package/plugin/openclaw.plugin.json +17 -0
- package/plugin/package.json +21 -0
- package/scripts/build-plugin.js +67 -0
- package/scripts/bundle.sh +62 -0
- package/scripts/install-plugin.sh +162 -0
- package/scripts/install-python-deps.js +346 -0
- package/scripts/install-python-deps.sh +226 -0
- package/scripts/install.js +512 -0
- package/scripts/install.sh +367 -0
- package/scripts/lib/common.js +490 -0
- package/scripts/lib/common.sh +137 -0
- package/scripts/release-pack.sh +110 -0
- package/scripts/start.js +50 -0
- package/scripts/test_available_models.py +284 -0
- package/scripts/test_websocket_ping.py +44 -0
- package/src/backend/agents.py +73 -0
- package/src/backend/api/__init__.py +1 -0
- package/src/backend/api/agent_config_api.py +90 -0
- package/src/backend/api/agents.py +73 -0
- package/src/backend/api/agents_config.py +75 -0
- package/src/backend/api/chains.py +126 -0
- package/src/backend/api/collaboration.py +902 -0
- package/src/backend/api/debug_paths.py +39 -0
- package/src/backend/api/error_analysis.py +146 -0
- package/src/backend/api/errors.py +281 -0
- package/src/backend/api/performance.py +784 -0
- package/src/backend/api/subagents.py +770 -0
- package/src/backend/api/timeline.py +144 -0
- package/src/backend/api/websocket.py +251 -0
- package/src/backend/collaboration.py +405 -0
- package/src/backend/data/__init__.py +1 -0
- package/src/backend/data/agent_config_manager.py +270 -0
- package/src/backend/data/chain_reader.py +299 -0
- package/src/backend/data/config_reader.py +153 -0
- package/src/backend/data/error_analyzer.py +430 -0
- package/src/backend/data/session_reader.py +445 -0
- package/src/backend/data/subagent_reader.py +244 -0
- package/src/backend/data/task_history.py +118 -0
- package/src/backend/data/timeline_reader.py +981 -0
- package/src/backend/errors.py +63 -0
- package/src/backend/main.py +89 -0
- package/src/backend/mechanism_reader.py +131 -0
- package/src/backend/mechanisms.py +32 -0
- package/src/backend/performance.py +474 -0
- package/src/backend/requirements.txt +5 -0
- package/src/backend/session_reader.py +238 -0
- package/src/backend/status/__init__.py +1 -0
- package/src/backend/status/error_detector.py +122 -0
- package/src/backend/status/status_calculator.py +301 -0
- package/src/backend/status_calculator.py +121 -0
- package/src/backend/subagent_reader.py +229 -0
- package/src/backend/watchers/__init__.py +4 -0
- package/src/backend/watchers/file_watcher.py +159 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
"""
|
|
2
|
+
协作流程 API 路由
|
|
3
|
+
符合 PRD: 展示老 K 与所有子 Agents 之间的连接关系,任务从老 K 流向子 Agents
|
|
4
|
+
扩展: Agent 模型配置、模型节点、最近调用(光球展示)
|
|
5
|
+
"""
|
|
6
|
+
import re
|
|
7
|
+
import logging
|
|
8
|
+
from fastapi import APIRouter
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from typing import List, Optional, Dict, Any
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from datetime import datetime, timedelta, timezone
|
|
14
|
+
from zoneinfo import ZoneInfo
|
|
15
|
+
|
|
16
|
+
TZ_DISPLAY = ZoneInfo('Asia/Shanghai')
|
|
17
|
+
|
|
18
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
19
|
+
|
|
20
|
+
router = APIRouter()
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CollaborationNode(BaseModel):
|
|
25
|
+
id: str
|
|
26
|
+
type: str # agent/task/tool/model
|
|
27
|
+
name: str
|
|
28
|
+
status: str # idle/working/error (Agent 状态: 空闲/工作中/异常)
|
|
29
|
+
timestamp: Optional[int] = None
|
|
30
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
31
|
+
# 当前任务(有效描述)
|
|
32
|
+
currentTask: Optional[str] = None
|
|
33
|
+
# 错误信息
|
|
34
|
+
error: Optional[Dict[str, Any]] = None
|
|
35
|
+
# 卡顿警告
|
|
36
|
+
stuckWarning: Optional[Dict[str, Any]] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CollaborationEdge(BaseModel):
|
|
40
|
+
id: str
|
|
41
|
+
source: str
|
|
42
|
+
target: str
|
|
43
|
+
type: str # delegates/calls/returns/error/model
|
|
44
|
+
label: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ModelCall(BaseModel):
|
|
48
|
+
id: str
|
|
49
|
+
agentId: str
|
|
50
|
+
model: str # provider/model
|
|
51
|
+
sessionId: str
|
|
52
|
+
trigger: str
|
|
53
|
+
tokens: int
|
|
54
|
+
timestamp: int # ms
|
|
55
|
+
time: str # HH:MM:SS
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AgentDisplayStatus(BaseModel):
|
|
59
|
+
"""Agent 显示状态(基于时间阈值)"""
|
|
60
|
+
status: str # 'idle' | 'working'
|
|
61
|
+
display: str # 显示文本
|
|
62
|
+
duration: int # 持续时间(秒)
|
|
63
|
+
alert: bool # 是否需要警告
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ActiveTask(BaseModel):
|
|
67
|
+
"""单个活跃任务(用于多任务并行展示)"""
|
|
68
|
+
id: str # task-{runId}
|
|
69
|
+
name: str # 任务名称(清理后)
|
|
70
|
+
status: str = "working" # working | retrying | failed
|
|
71
|
+
timestamp: Optional[int] = None # 开始时间
|
|
72
|
+
childAgentId: Optional[str] = None # 主 Agent 任务时,指向被派发的子 Agent
|
|
73
|
+
featureId: Optional[str] = None # FEATURE_ID(如果有)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _extract_feature_id(task_name: str) -> Optional[str]:
|
|
77
|
+
"""从任务名称中提取 FEATURE_ID"""
|
|
78
|
+
if not task_name:
|
|
79
|
+
return None
|
|
80
|
+
# 匹配 [FEATURE_ID] xxx 格式
|
|
81
|
+
match = re.search(r'\[FEATURE_ID\]\s*(\S+)', task_name)
|
|
82
|
+
if match:
|
|
83
|
+
return match.group(1)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _build_agent_active_tasks(
|
|
88
|
+
active_runs: List[Dict[str, Any]],
|
|
89
|
+
main_agent_id: str
|
|
90
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
91
|
+
"""
|
|
92
|
+
构建每个 Agent 的活跃任务列表(支持多任务并行展示)
|
|
93
|
+
|
|
94
|
+
自适应不同组网:
|
|
95
|
+
- 单 Agent 模式:main 没有子 agent,任务直接执行
|
|
96
|
+
- 主从模式:main 派发给子 agent
|
|
97
|
+
- 嵌套模式:子 agent 可以再派发给孙 agent
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
active_runs: 从 runs.json 读取的活跃任务
|
|
101
|
+
main_agent_id: 主 Agent ID
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
{
|
|
105
|
+
"main": [task1, task2, ...], # PM 派发的任务
|
|
106
|
+
"analyst-agent": [task3, ...], # 分析师执行的任务
|
|
107
|
+
...
|
|
108
|
+
}
|
|
109
|
+
"""
|
|
110
|
+
agent_active_tasks: Dict[str, List[Dict[str, Any]]] = {}
|
|
111
|
+
|
|
112
|
+
for run in active_runs:
|
|
113
|
+
child_key = run.get('childSessionKey', '')
|
|
114
|
+
requester_key = run.get('requesterSessionKey', '')
|
|
115
|
+
|
|
116
|
+
# 解析执行者 Agent
|
|
117
|
+
child_agent_id = _parse_agent_id(child_key)
|
|
118
|
+
# 解析派发者 Agent
|
|
119
|
+
requester_agent_id = _parse_agent_id(requester_key)
|
|
120
|
+
|
|
121
|
+
if not child_agent_id:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# 清理任务名称
|
|
125
|
+
task_name = _clean_task_name(run.get('task', ''))
|
|
126
|
+
if not task_name:
|
|
127
|
+
task_name = '未命名任务'
|
|
128
|
+
|
|
129
|
+
run_id = run.get('runId', child_agent_id)
|
|
130
|
+
|
|
131
|
+
# 提取 featureId
|
|
132
|
+
feature_id = _extract_feature_id(run.get('task', ''))
|
|
133
|
+
|
|
134
|
+
# 构建任务对象
|
|
135
|
+
task_item: Dict[str, Any] = {
|
|
136
|
+
'id': f"task-{run_id}",
|
|
137
|
+
'name': task_name,
|
|
138
|
+
'status': 'working',
|
|
139
|
+
'timestamp': run.get('startedAt'),
|
|
140
|
+
'featureId': feature_id
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# 如果有派发者,添加 childAgentId(用于主 Agent 显示任务流向)
|
|
144
|
+
if requester_agent_id and requester_agent_id != child_agent_id:
|
|
145
|
+
task_item['childAgentId'] = child_agent_id
|
|
146
|
+
|
|
147
|
+
# 1. 添加到派发者(如果派发者是某个已知 agent)
|
|
148
|
+
if requester_agent_id:
|
|
149
|
+
if requester_agent_id not in agent_active_tasks:
|
|
150
|
+
agent_active_tasks[requester_agent_id] = []
|
|
151
|
+
agent_active_tasks[requester_agent_id].append(task_item)
|
|
152
|
+
|
|
153
|
+
# 2. 添加到执行者(不带 childAgentId)
|
|
154
|
+
if child_agent_id not in agent_active_tasks:
|
|
155
|
+
agent_active_tasks[child_agent_id] = []
|
|
156
|
+
child_task = {k: v for k, v in task_item.items() if k != 'childAgentId'}
|
|
157
|
+
agent_active_tasks[child_agent_id].append(child_task)
|
|
158
|
+
|
|
159
|
+
return agent_active_tasks
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _get_display_task_summary(tasks: List[Dict[str, Any]]) -> str:
|
|
163
|
+
"""
|
|
164
|
+
获取用于显示的任务摘要
|
|
165
|
+
|
|
166
|
+
策略:
|
|
167
|
+
- 0 个任务:返回空
|
|
168
|
+
- 1 个任务:显示任务名称
|
|
169
|
+
- 2+ 个任务:显示 "N 个任务进行中"
|
|
170
|
+
"""
|
|
171
|
+
count = len(tasks)
|
|
172
|
+
if count == 0:
|
|
173
|
+
return ''
|
|
174
|
+
elif count == 1:
|
|
175
|
+
# 截断过长任务名
|
|
176
|
+
name = tasks[0].get('name', '')
|
|
177
|
+
return name[:50] + '...' if len(name) > 50 else name
|
|
178
|
+
else:
|
|
179
|
+
return f"{count} 个任务进行中"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class CollaborationFlow(BaseModel):
|
|
183
|
+
nodes: List[CollaborationNode]
|
|
184
|
+
edges: List[CollaborationEdge]
|
|
185
|
+
activePath: List[str]
|
|
186
|
+
lastUpdate: int
|
|
187
|
+
mainAgentId: Optional[str] = None # 主 Agent ID,前端用于布局与样式
|
|
188
|
+
agentModels: Optional[Dict[str, Dict[str, Any]]] = None
|
|
189
|
+
models: Optional[List[str]] = None
|
|
190
|
+
recentCalls: Optional[List[ModelCall]] = None
|
|
191
|
+
hierarchy: Optional[Dict[str, List[str]]] = None # agentId -> 子 agent 列表
|
|
192
|
+
depths: Optional[Dict[str, int]] = None # agentId -> 层级深度 (0=主, 1=子, 2=孙...)
|
|
193
|
+
# 多任务并行展示:每个 Agent 的活跃任务列表
|
|
194
|
+
agentActiveTasks: Optional[Dict[str, List[ActiveTask]]] = None
|
|
195
|
+
# 多任务并行展示:每个 Agent 的活跃任务列表
|
|
196
|
+
agentActiveTasks: Optional[Dict[str, List[ActiveTask]]] = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _parse_agent_id(session_key: str) -> str:
|
|
200
|
+
"""从 sessionKey (childSessionKey 或 requesterSessionKey) 解析 agentId"""
|
|
201
|
+
parts = (session_key or '').split(':')
|
|
202
|
+
if len(parts) >= 2 and parts[0] == 'agent':
|
|
203
|
+
return parts[1]
|
|
204
|
+
return ''
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ============================================================================
|
|
208
|
+
# 模型 ID 规范化(TR9-2)
|
|
209
|
+
# ============================================================================
|
|
210
|
+
|
|
211
|
+
# 模块级缓存
|
|
212
|
+
_model_mapping_cache: Optional[Dict[str, str]] = None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _get_model_mapping() -> Dict[str, str]:
|
|
216
|
+
"""
|
|
217
|
+
获取 model 映射(带缓存)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
{'claude-sonnet-4.6': 'anthropic/claude-sonnet-4.6', ...}
|
|
221
|
+
"""
|
|
222
|
+
global _model_mapping_cache
|
|
223
|
+
if _model_mapping_cache is None:
|
|
224
|
+
try:
|
|
225
|
+
from data.config_reader import get_all_models_from_agents
|
|
226
|
+
_model_mapping_cache = {}
|
|
227
|
+
for model_id in get_all_models_from_agents():
|
|
228
|
+
short = model_id.split('/')[-1]
|
|
229
|
+
# 精确匹配
|
|
230
|
+
_model_mapping_cache[short] = model_id
|
|
231
|
+
# 去除日期版本号的映射(使用正则精确匹配 -20YYMMDD)
|
|
232
|
+
base = re.sub(r'-20\d{6}$', '', short)
|
|
233
|
+
if base != short:
|
|
234
|
+
_model_mapping_cache[base] = model_id
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.warning(f"Failed to build model mapping: {e}")
|
|
237
|
+
_model_mapping_cache = {}
|
|
238
|
+
return _model_mapping_cache
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _normalize_model_id(model_from_session: str) -> str:
|
|
242
|
+
"""
|
|
243
|
+
将 session 中的 model 值规范化为标准格式
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
model_from_session: session 中的 model 值
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
标准化的模型 ID,如 "anthropic/claude-sonnet-4.6"
|
|
250
|
+
|
|
251
|
+
Examples:
|
|
252
|
+
>>> _normalize_model_id("claude-sonnet-4.6")
|
|
253
|
+
"anthropic/claude-sonnet-4.6"
|
|
254
|
+
>>> _normalize_model_id("claude-sonnet-4.6-20250514")
|
|
255
|
+
"anthropic/claude-sonnet-4.6"
|
|
256
|
+
>>> _normalize_model_id("anthropic/claude-sonnet-4.6")
|
|
257
|
+
"anthropic/claude-sonnet-4.6"
|
|
258
|
+
"""
|
|
259
|
+
if not model_from_session:
|
|
260
|
+
return '(unknown)'
|
|
261
|
+
|
|
262
|
+
# 已经是标准格式
|
|
263
|
+
if '/' in model_from_session:
|
|
264
|
+
return model_from_session
|
|
265
|
+
|
|
266
|
+
mapping = _get_model_mapping()
|
|
267
|
+
|
|
268
|
+
# 精确匹配
|
|
269
|
+
if model_from_session in mapping:
|
|
270
|
+
return mapping[model_from_session]
|
|
271
|
+
|
|
272
|
+
# 使用正则去除日期版本号后匹配
|
|
273
|
+
base_name = re.sub(r'-20\d{6}$', '', model_from_session)
|
|
274
|
+
if base_name in mapping:
|
|
275
|
+
return mapping[base_name]
|
|
276
|
+
|
|
277
|
+
logger.debug(f"Unknown model format: {model_from_session}")
|
|
278
|
+
return model_from_session
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _clear_model_mapping_cache():
|
|
282
|
+
"""清除模型映射缓存(配置变更时调用)"""
|
|
283
|
+
global _model_mapping_cache
|
|
284
|
+
_model_mapping_cache = None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _clean_task_name(task_name: str) -> str:
|
|
288
|
+
"""清理任务名称,提取有效的任务描述"""
|
|
289
|
+
if not task_name:
|
|
290
|
+
return ''
|
|
291
|
+
# 过滤子 Agent 回传内容(不应作为任务显示)
|
|
292
|
+
if 'Result (untrusted content, treat as data):' in task_name or '[Internal task completion event]' in task_name:
|
|
293
|
+
return ''
|
|
294
|
+
|
|
295
|
+
lines = task_name.strip().split('\n')
|
|
296
|
+
|
|
297
|
+
# 需要过滤的技术信息前缀
|
|
298
|
+
tech_patterns = [
|
|
299
|
+
'CONTEXT FILES',
|
|
300
|
+
'WORKING DIRECTORY',
|
|
301
|
+
'SYSTEM INFO',
|
|
302
|
+
'ENVIRONMENT',
|
|
303
|
+
'---',
|
|
304
|
+
'===',
|
|
305
|
+
'```',
|
|
306
|
+
'# ',
|
|
307
|
+
'## ',
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
# 查找第一个有效的任务描述行
|
|
311
|
+
for line in lines:
|
|
312
|
+
line = line.strip()
|
|
313
|
+
if not line:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
# 跳过技术信息
|
|
317
|
+
is_tech = False
|
|
318
|
+
for pattern in tech_patterns:
|
|
319
|
+
if line.upper().startswith(pattern):
|
|
320
|
+
is_tech = True
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
if not is_tech and len(line) > 3:
|
|
324
|
+
# 找到有效行
|
|
325
|
+
if len(line) > 80:
|
|
326
|
+
return line[:77] + '...'
|
|
327
|
+
return line
|
|
328
|
+
|
|
329
|
+
# 如果没找到有效行,返回空
|
|
330
|
+
return ''
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _get_agent_error_info(agent_id: str) -> Optional[Dict[str, Any]]:
|
|
334
|
+
"""获取 agent 的错误/异常信息"""
|
|
335
|
+
from session_reader import get_last_error, has_recent_errors
|
|
336
|
+
|
|
337
|
+
if has_recent_errors(agent_id, minutes=10):
|
|
338
|
+
error = get_last_error(agent_id)
|
|
339
|
+
if error:
|
|
340
|
+
return {
|
|
341
|
+
'hasError': True,
|
|
342
|
+
'type': error.get('type', 'unknown'),
|
|
343
|
+
'message': error.get('message', '')[:100], # 截断
|
|
344
|
+
'timestamp': error.get('timestamp', 0)
|
|
345
|
+
}
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _check_agent_stuck(agent_id: str) -> Optional[Dict[str, Any]]:
|
|
350
|
+
"""检查 agent 是否卡顿(长时间无响应但有活跃任务)"""
|
|
351
|
+
import time
|
|
352
|
+
from session_reader import get_session_updated_at
|
|
353
|
+
from data.subagent_reader import is_agent_working, get_active_runs
|
|
354
|
+
|
|
355
|
+
if not is_agent_working(agent_id):
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
last_update = get_session_updated_at(agent_id)
|
|
359
|
+
if not last_update:
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
now = int(time.time() * 1000)
|
|
363
|
+
idle_seconds = (now - last_update) / 1000
|
|
364
|
+
|
|
365
|
+
# 超过 60 秒无响应视为可能卡顿
|
|
366
|
+
if idle_seconds > 60:
|
|
367
|
+
# 分析卡顿原因
|
|
368
|
+
reason = _analyze_stuck_reason(agent_id, idle_seconds)
|
|
369
|
+
return {
|
|
370
|
+
'isStuck': True,
|
|
371
|
+
'idleSeconds': int(idle_seconds),
|
|
372
|
+
'lastUpdate': last_update,
|
|
373
|
+
'reason': reason.get('type', 'unknown'),
|
|
374
|
+
'reasonDetail': reason.get('detail', ''),
|
|
375
|
+
'waitingFor': reason.get('waitingFor')
|
|
376
|
+
}
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _analyze_stuck_reason(agent_id: str, idle_seconds: int) -> Dict[str, Any]:
|
|
381
|
+
"""
|
|
382
|
+
分析 Agent 卡顿的原因
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
{
|
|
386
|
+
'type': 'waiting_subagent' | 'model_delay' | 'tool_execution' | 'unknown',
|
|
387
|
+
'detail': '详细描述',
|
|
388
|
+
'waitingFor': {'agentId': 'xxx', 'task': 'xxx'} | None
|
|
389
|
+
}
|
|
390
|
+
"""
|
|
391
|
+
from data.subagent_reader import get_active_runs
|
|
392
|
+
|
|
393
|
+
# 检查是否在等待子 agent
|
|
394
|
+
active_runs = get_active_runs()
|
|
395
|
+
for run in active_runs:
|
|
396
|
+
requester_key = run.get('requesterSessionKey', '')
|
|
397
|
+
# 如果这个 agent 是 requester,说明它在等待子 agent
|
|
398
|
+
if f'agent:{agent_id}:' in requester_key:
|
|
399
|
+
child_key = run.get('childSessionKey', '')
|
|
400
|
+
if child_key and ':' in child_key:
|
|
401
|
+
parts = child_key.split(':')
|
|
402
|
+
if len(parts) >= 2:
|
|
403
|
+
child_agent_id = parts[1]
|
|
404
|
+
task = run.get('task', '')[:50]
|
|
405
|
+
return {
|
|
406
|
+
'type': 'waiting_subagent',
|
|
407
|
+
'detail': f'等待子代理 {child_agent_id} 完成任务',
|
|
408
|
+
'waitingFor': {
|
|
409
|
+
'agentId': child_agent_id,
|
|
410
|
+
'task': task
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# 检查最近是否有模型调用(可能是模型响应慢)
|
|
415
|
+
# 这里简单判断:如果 idle 时间很长但没有等待子 agent,可能是模型或工具问题
|
|
416
|
+
if idle_seconds > 120:
|
|
417
|
+
return {
|
|
418
|
+
'type': 'model_delay',
|
|
419
|
+
'detail': '模型响应时间过长,可能遇到限流或网络问题',
|
|
420
|
+
'waitingFor': None
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if idle_seconds > 60:
|
|
424
|
+
return {
|
|
425
|
+
'type': 'tool_execution',
|
|
426
|
+
'detail': '工具执行中或等待外部资源',
|
|
427
|
+
'waitingFor': None
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
'type': 'unknown',
|
|
432
|
+
'detail': '原因未知',
|
|
433
|
+
'waitingFor': None
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _get_recent_model_calls(minutes: int = 30) -> List[Dict]:
|
|
438
|
+
"""
|
|
439
|
+
获取最近 N 分钟的模型调用记录(用于光球展示)
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
minutes: 时间范围(分钟)
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
调用记录列表,model 字段已规范化
|
|
446
|
+
|
|
447
|
+
注意:
|
|
448
|
+
- since 为 timezone-aware datetime (UTC)
|
|
449
|
+
- timestamp 统一处理为 UTC timezone-aware
|
|
450
|
+
- model 字段规范化为 provider/model 格式
|
|
451
|
+
"""
|
|
452
|
+
from api.performance import parse_session_file_with_details
|
|
453
|
+
|
|
454
|
+
records = []
|
|
455
|
+
from data.config_reader import get_openclaw_root
|
|
456
|
+
openclaw_path = get_openclaw_root()
|
|
457
|
+
agents_path = openclaw_path / 'agents'
|
|
458
|
+
if not agents_path.exists():
|
|
459
|
+
return []
|
|
460
|
+
|
|
461
|
+
# 确保 since 是 UTC timezone-aware
|
|
462
|
+
now = datetime.now(timezone.utc)
|
|
463
|
+
since = now - timedelta(minutes=minutes)
|
|
464
|
+
|
|
465
|
+
for agent_dir in agents_path.iterdir():
|
|
466
|
+
if not agent_dir.is_dir():
|
|
467
|
+
continue
|
|
468
|
+
agent_id = agent_dir.name
|
|
469
|
+
sessions_path = agent_dir / 'sessions'
|
|
470
|
+
if not sessions_path.exists():
|
|
471
|
+
continue
|
|
472
|
+
for session_file in sessions_path.glob('*.jsonl'):
|
|
473
|
+
if 'lock' in session_file.name or 'deleted' in session_file.name:
|
|
474
|
+
continue
|
|
475
|
+
for r in parse_session_file_with_details(session_file, agent_id):
|
|
476
|
+
ts = r['timestamp']
|
|
477
|
+
# 确保 timestamp 是 timezone-aware
|
|
478
|
+
if ts.tzinfo is None:
|
|
479
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
480
|
+
|
|
481
|
+
if ts >= since:
|
|
482
|
+
ts_local = ts.astimezone(TZ_DISPLAY)
|
|
483
|
+
# 规范化 model 字段
|
|
484
|
+
normalized_model = _normalize_model_id(r.get('model', ''))
|
|
485
|
+
|
|
486
|
+
records.append({
|
|
487
|
+
'agentId': agent_id,
|
|
488
|
+
'model': normalized_model, # 使用规范化后的值
|
|
489
|
+
'sessionId': r['sessionId'],
|
|
490
|
+
'trigger': r.get('trigger', ''),
|
|
491
|
+
'tokens': r.get('tokens', 0),
|
|
492
|
+
'timestamp': int(ts.timestamp() * 1000),
|
|
493
|
+
'time': ts_local.strftime('%H:%M:%S')
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
records.sort(key=lambda x: x['timestamp'])
|
|
497
|
+
return records[-100:] # 最多 100 条
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@router.get("/collaboration", response_model=CollaborationFlow)
|
|
501
|
+
async def get_collaboration():
|
|
502
|
+
"""获取协作流程数据 - 主 Agent 与子 Agents 的拓扑关系,含模型配置与最近调用"""
|
|
503
|
+
from data.config_reader import (
|
|
504
|
+
get_agents_list, get_agent_models, get_models_configured_by_agents,
|
|
505
|
+
get_model_display_name, get_main_agent_id
|
|
506
|
+
)
|
|
507
|
+
from data.subagent_reader import get_active_runs
|
|
508
|
+
from status.status_calculator import calculate_agent_status
|
|
509
|
+
|
|
510
|
+
nodes = []
|
|
511
|
+
edges = []
|
|
512
|
+
active_path = []
|
|
513
|
+
agent_models: Dict[str, Dict[str, Any]] = {}
|
|
514
|
+
models_list: List[str] = []
|
|
515
|
+
recent_calls: List[Dict] = []
|
|
516
|
+
|
|
517
|
+
main_agent_id = 'main'
|
|
518
|
+
try:
|
|
519
|
+
main_agent_id = get_main_agent_id()
|
|
520
|
+
agents_list = get_agents_list()
|
|
521
|
+
active_runs = get_active_runs()
|
|
522
|
+
|
|
523
|
+
main_agent_id = get_main_agent_id()
|
|
524
|
+
main_agent_config = next((a for a in agents_list if a.get('id') == main_agent_id), None)
|
|
525
|
+
sub_agents_config = [a for a in agents_list if a.get('id') != main_agent_id]
|
|
526
|
+
|
|
527
|
+
all_agents = [a for a in agents_list if a.get('id')]
|
|
528
|
+
for agent in all_agents:
|
|
529
|
+
aid = agent.get('id', '')
|
|
530
|
+
agent_models[aid] = get_agent_models(aid)
|
|
531
|
+
models_list = get_models_configured_by_agents()
|
|
532
|
+
recent_calls = _get_recent_model_calls(30)
|
|
533
|
+
|
|
534
|
+
main_display_name = (main_agent_config.get('name') if main_agent_config else None) or "主 Agent"
|
|
535
|
+
main_status = "working" if active_runs else "idle"
|
|
536
|
+
|
|
537
|
+
# 获取主 agent 的当前任务和错误信息
|
|
538
|
+
main_current_task = ''
|
|
539
|
+
main_error = None
|
|
540
|
+
main_stuck = None
|
|
541
|
+
if active_runs:
|
|
542
|
+
# 找到主 agent 作为 requester 的任务
|
|
543
|
+
for run in active_runs:
|
|
544
|
+
requester_key = run.get('requesterSessionKey', '')
|
|
545
|
+
if f'agent:{main_agent_id}:' in requester_key:
|
|
546
|
+
main_current_task = _clean_task_name(run.get('task', ''))
|
|
547
|
+
break
|
|
548
|
+
if not main_current_task and active_runs:
|
|
549
|
+
main_current_task = _clean_task_name(active_runs[0].get('task', ''))
|
|
550
|
+
main_error = _get_agent_error_info(main_agent_id)
|
|
551
|
+
main_stuck = _check_agent_stuck(main_agent_id)
|
|
552
|
+
|
|
553
|
+
main_agent = CollaborationNode(
|
|
554
|
+
id=main_agent_id,
|
|
555
|
+
type="agent",
|
|
556
|
+
name=main_display_name,
|
|
557
|
+
status=main_status,
|
|
558
|
+
timestamp=int(__import__('time').time() * 1000),
|
|
559
|
+
metadata=agent_models.get(main_agent_id),
|
|
560
|
+
currentTask=main_current_task if main_current_task else None,
|
|
561
|
+
error=main_error,
|
|
562
|
+
stuckWarning=main_stuck
|
|
563
|
+
)
|
|
564
|
+
nodes.append(main_agent)
|
|
565
|
+
|
|
566
|
+
for agent in sub_agents_config:
|
|
567
|
+
agent_id = agent.get('id', '')
|
|
568
|
+
agent_name = agent.get('name', agent_id)
|
|
569
|
+
|
|
570
|
+
status = calculate_agent_status(agent_id)
|
|
571
|
+
if status == 'down':
|
|
572
|
+
status = 'error'
|
|
573
|
+
elif status == 'working':
|
|
574
|
+
status = 'working'
|
|
575
|
+
else:
|
|
576
|
+
status = 'idle'
|
|
577
|
+
|
|
578
|
+
# 获取子 agent 的当前任务
|
|
579
|
+
current_task = ''
|
|
580
|
+
for run in active_runs:
|
|
581
|
+
child_key = run.get('childSessionKey', '')
|
|
582
|
+
if f'agent:{agent_id}:' in child_key:
|
|
583
|
+
current_task = _clean_task_name(run.get('task', ''))
|
|
584
|
+
break
|
|
585
|
+
|
|
586
|
+
# 获取错误和卡顿信息
|
|
587
|
+
error_info = _get_agent_error_info(agent_id)
|
|
588
|
+
stuck_info = _check_agent_stuck(agent_id)
|
|
589
|
+
|
|
590
|
+
sub_node = CollaborationNode(
|
|
591
|
+
id=agent_id,
|
|
592
|
+
type="agent",
|
|
593
|
+
name=agent_name,
|
|
594
|
+
status=status,
|
|
595
|
+
timestamp=None,
|
|
596
|
+
metadata=agent_models.get(agent_id),
|
|
597
|
+
currentTask=current_task if current_task else None,
|
|
598
|
+
error=error_info,
|
|
599
|
+
stuckWarning=stuck_info
|
|
600
|
+
)
|
|
601
|
+
nodes.append(sub_node)
|
|
602
|
+
|
|
603
|
+
edges.append(CollaborationEdge(
|
|
604
|
+
id=f"edge-{main_agent_id}-{agent_id}",
|
|
605
|
+
source=main_agent_id,
|
|
606
|
+
target=agent_id,
|
|
607
|
+
type="delegates",
|
|
608
|
+
label="委托"
|
|
609
|
+
))
|
|
610
|
+
|
|
611
|
+
# 3. 模型节点(右侧)
|
|
612
|
+
for i, model_id in enumerate(models_list):
|
|
613
|
+
model_node = CollaborationNode(
|
|
614
|
+
id=f"model-{model_id.replace('/', '-')}",
|
|
615
|
+
type="model",
|
|
616
|
+
name=get_model_display_name(model_id),
|
|
617
|
+
status="idle",
|
|
618
|
+
timestamp=None,
|
|
619
|
+
metadata={"modelId": model_id}
|
|
620
|
+
)
|
|
621
|
+
nodes.append(model_node)
|
|
622
|
+
|
|
623
|
+
# 4. Agent -> Model 边(配置了该模型的 Agent)
|
|
624
|
+
for agent in all_agents:
|
|
625
|
+
aid = agent.get('id', '')
|
|
626
|
+
cfg = agent_models.get(aid, {})
|
|
627
|
+
primary = cfg.get('primary', '')
|
|
628
|
+
fallbacks = cfg.get('fallbacks', [])
|
|
629
|
+
all_models = [primary] + [f for f in fallbacks if f != primary]
|
|
630
|
+
for mid in all_models:
|
|
631
|
+
if mid and mid in models_list:
|
|
632
|
+
mid_safe = mid.replace('/', '-')
|
|
633
|
+
edges.append(CollaborationEdge(
|
|
634
|
+
id=f"edge-{aid}-model-{mid_safe}",
|
|
635
|
+
source=aid,
|
|
636
|
+
target=f"model-{mid_safe}",
|
|
637
|
+
type="model",
|
|
638
|
+
label=mid
|
|
639
|
+
))
|
|
640
|
+
|
|
641
|
+
# 5. 活跃任务与 spawn 链:requesterSessionKey -> childSessionKey -> task
|
|
642
|
+
for run in active_runs[:20]:
|
|
643
|
+
child_key = run.get('childSessionKey', '')
|
|
644
|
+
requester_key = run.get('requesterSessionKey', '')
|
|
645
|
+
agent_id = _parse_agent_id(child_key)
|
|
646
|
+
requester_id = _parse_agent_id(requester_key)
|
|
647
|
+
if not agent_id:
|
|
648
|
+
continue
|
|
649
|
+
|
|
650
|
+
task_name = _clean_task_name(run.get('task', ''))
|
|
651
|
+
|
|
652
|
+
task_id = f"task-{run.get('runId', agent_id)}"
|
|
653
|
+
task_node = CollaborationNode(
|
|
654
|
+
id=task_id,
|
|
655
|
+
type="task",
|
|
656
|
+
name=task_name,
|
|
657
|
+
status="working",
|
|
658
|
+
timestamp=run.get('startedAt')
|
|
659
|
+
)
|
|
660
|
+
nodes.append(task_node)
|
|
661
|
+
|
|
662
|
+
edges.append(CollaborationEdge(
|
|
663
|
+
id=f"edge-{agent_id}-{task_id}",
|
|
664
|
+
source=agent_id,
|
|
665
|
+
target=task_id,
|
|
666
|
+
type="calls",
|
|
667
|
+
label="执行"
|
|
668
|
+
))
|
|
669
|
+
|
|
670
|
+
# Spawn 链:主 Agent 派发 -> 子 Agent 执行
|
|
671
|
+
if requester_id and requester_id != agent_id:
|
|
672
|
+
edges.append(CollaborationEdge(
|
|
673
|
+
id=f"edge-spawn-{requester_id}-{task_id}",
|
|
674
|
+
source=requester_id,
|
|
675
|
+
target=task_id,
|
|
676
|
+
type="delegates",
|
|
677
|
+
label="派发"
|
|
678
|
+
))
|
|
679
|
+
# 如果 requester 是主 agent,给主 agent 也添加一个任务节点(用户命令)
|
|
680
|
+
# 移除 main_agent_task_created 限制,支持多任务并行显示
|
|
681
|
+
if requester_id == main_agent_id:
|
|
682
|
+
# 主 agent 的任务就是用户原始命令
|
|
683
|
+
main_task_id = f"task-main-{run.get('runId', 'current')}"
|
|
684
|
+
main_task_node = CollaborationNode(
|
|
685
|
+
id=main_task_id,
|
|
686
|
+
type="task",
|
|
687
|
+
name=task_name, # 用户命令
|
|
688
|
+
status="working",
|
|
689
|
+
timestamp=run.get('startedAt')
|
|
690
|
+
)
|
|
691
|
+
nodes.append(main_task_node)
|
|
692
|
+
edges.append(CollaborationEdge(
|
|
693
|
+
id=f"edge-{main_agent_id}-{main_task_id}",
|
|
694
|
+
source=main_agent_id,
|
|
695
|
+
target=main_task_id,
|
|
696
|
+
type="calls",
|
|
697
|
+
label="执行"
|
|
698
|
+
))
|
|
699
|
+
|
|
700
|
+
active_path.extend([main_agent_id, agent_id, task_id])
|
|
701
|
+
|
|
702
|
+
except Exception as e:
|
|
703
|
+
print(f"Error building collaboration data: {e}")
|
|
704
|
+
import traceback
|
|
705
|
+
traceback.print_exc()
|
|
706
|
+
|
|
707
|
+
if not nodes:
|
|
708
|
+
try:
|
|
709
|
+
main_agent_id = get_main_agent_id()
|
|
710
|
+
except Exception:
|
|
711
|
+
main_agent_id = 'main'
|
|
712
|
+
nodes = [
|
|
713
|
+
CollaborationNode(id=main_agent_id, type="agent", name="主 Agent", status="idle"),
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
# 构建 recentCalls 带 id
|
|
717
|
+
model_calls = [
|
|
718
|
+
ModelCall(
|
|
719
|
+
id=f"call-{i}",
|
|
720
|
+
agentId=r["agentId"],
|
|
721
|
+
model=r.get("model", ""),
|
|
722
|
+
sessionId=r.get("sessionId", ""),
|
|
723
|
+
trigger=r.get("trigger", ""),
|
|
724
|
+
tokens=r.get("tokens", 0),
|
|
725
|
+
timestamp=r.get("timestamp", 0),
|
|
726
|
+
time=r.get("time", "")
|
|
727
|
+
)
|
|
728
|
+
for i, r in enumerate(recent_calls)
|
|
729
|
+
]
|
|
730
|
+
|
|
731
|
+
# 计算层级深度 (depths) 和层级关系 (hierarchy)
|
|
732
|
+
# 从 edges 中提取 agent 之间的父子关系
|
|
733
|
+
hierarchy: Dict[str, List[str]] = {}
|
|
734
|
+
agent_ids = set(n.id for n in nodes if n.type == "agent")
|
|
735
|
+
|
|
736
|
+
# 构建 delegate 关系: source -> [targets]
|
|
737
|
+
for edge in edges:
|
|
738
|
+
if edge.type == "delegates":
|
|
739
|
+
source = edge.source
|
|
740
|
+
target = edge.target
|
|
741
|
+
# 只处理 agent 之间的委托关系
|
|
742
|
+
if source in agent_ids and target in agent_ids:
|
|
743
|
+
if source not in hierarchy:
|
|
744
|
+
hierarchy[source] = []
|
|
745
|
+
if target not in hierarchy[source]:
|
|
746
|
+
hierarchy[source].append(target)
|
|
747
|
+
|
|
748
|
+
# 计算 depth: 主 agent depth=0, 其子 agent depth=1, 孙 agent depth=2...
|
|
749
|
+
depths: Dict[str, int] = {}
|
|
750
|
+
depths[main_agent_id] = 0
|
|
751
|
+
|
|
752
|
+
# BFS 计算深度
|
|
753
|
+
queue = [main_agent_id]
|
|
754
|
+
while queue:
|
|
755
|
+
parent = queue.pop(0)
|
|
756
|
+
parent_depth = depths.get(parent, 0)
|
|
757
|
+
for child in hierarchy.get(parent, []):
|
|
758
|
+
if child not in depths:
|
|
759
|
+
depths[child] = parent_depth + 1
|
|
760
|
+
queue.append(child)
|
|
761
|
+
|
|
762
|
+
# 未在 hierarchy 中的 agent 默认 depth=1 (作为主 agent 的直接子节点)
|
|
763
|
+
for aid in agent_ids:
|
|
764
|
+
if aid not in depths:
|
|
765
|
+
depths[aid] = 1
|
|
766
|
+
|
|
767
|
+
# 构建多任务并行数据
|
|
768
|
+
agent_active_tasks = _build_agent_active_tasks(active_runs, main_agent_id)
|
|
769
|
+
|
|
770
|
+
return CollaborationFlow(
|
|
771
|
+
nodes=nodes,
|
|
772
|
+
edges=edges,
|
|
773
|
+
activePath=list(set(active_path)),
|
|
774
|
+
lastUpdate=int(__import__('time').time() * 1000),
|
|
775
|
+
mainAgentId=main_agent_id,
|
|
776
|
+
agentModels=agent_models,
|
|
777
|
+
models=models_list,
|
|
778
|
+
recentCalls=model_calls,
|
|
779
|
+
hierarchy=hierarchy,
|
|
780
|
+
depths=depths,
|
|
781
|
+
agentActiveTasks=agent_active_tasks
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
class CollaborationDynamic(BaseModel):
|
|
786
|
+
"""仅动态数据:状态、小球、任务节点,不包含静态拓扑"""
|
|
787
|
+
activePath: List[str]
|
|
788
|
+
recentCalls: List[ModelCall]
|
|
789
|
+
agentStatuses: Dict[str, str] # agentId -> idle/working/error
|
|
790
|
+
agentDynamicStatuses: Optional[Dict[str, AgentDisplayStatus]] = None # 详细显示状态
|
|
791
|
+
taskNodes: List[CollaborationNode]
|
|
792
|
+
taskEdges: List[CollaborationEdge]
|
|
793
|
+
mainAgentId: str
|
|
794
|
+
lastUpdate: int
|
|
795
|
+
# 多任务并行展示:每个 Agent 的活跃任务列表
|
|
796
|
+
agentActiveTasks: Optional[Dict[str, List[ActiveTask]]] = None
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
@router.get("/collaboration/dynamic", response_model=CollaborationDynamic)
|
|
800
|
+
async def get_collaboration_dynamic():
|
|
801
|
+
"""轻量接口:仅返回状态、小球、任务等动态数据,用于静默刷新,不触发整体重载"""
|
|
802
|
+
from data.config_reader import get_agents_list, get_main_agent_id
|
|
803
|
+
from data.subagent_reader import get_active_runs
|
|
804
|
+
from status.status_calculator import calculate_agent_status, get_display_status
|
|
805
|
+
|
|
806
|
+
active_path = []
|
|
807
|
+
agent_statuses: Dict[str, str] = {}
|
|
808
|
+
agent_dynamic_statuses: Dict[str, AgentDisplayStatus] = {}
|
|
809
|
+
task_nodes: List[CollaborationNode] = []
|
|
810
|
+
task_edges: List[CollaborationEdge] = []
|
|
811
|
+
main_agent_id = 'main'
|
|
812
|
+
|
|
813
|
+
try:
|
|
814
|
+
main_agent_id = get_main_agent_id()
|
|
815
|
+
agents_list = get_agents_list()
|
|
816
|
+
active_runs = get_active_runs()
|
|
817
|
+
|
|
818
|
+
for agent in agents_list:
|
|
819
|
+
aid = agent.get('id', '')
|
|
820
|
+
if not aid:
|
|
821
|
+
continue
|
|
822
|
+
status = calculate_agent_status(aid)
|
|
823
|
+
if status == 'down':
|
|
824
|
+
status = 'error'
|
|
825
|
+
elif status == 'working':
|
|
826
|
+
status = 'working'
|
|
827
|
+
else:
|
|
828
|
+
status = 'idle'
|
|
829
|
+
agent_statuses[aid] = status
|
|
830
|
+
|
|
831
|
+
# 获取详细显示状态
|
|
832
|
+
try:
|
|
833
|
+
dyn_status = get_display_status(aid)
|
|
834
|
+
agent_dynamic_statuses[aid] = AgentDisplayStatus(
|
|
835
|
+
status=dyn_status['status'],
|
|
836
|
+
display=dyn_status['display'],
|
|
837
|
+
duration=dyn_status['duration'],
|
|
838
|
+
alert=dyn_status['alert']
|
|
839
|
+
)
|
|
840
|
+
except Exception as e:
|
|
841
|
+
logger.warning(f"Failed to get display status for {aid}: {e}")
|
|
842
|
+
|
|
843
|
+
main_status = "working" if active_runs else "idle"
|
|
844
|
+
agent_statuses[main_agent_id] = main_status
|
|
845
|
+
|
|
846
|
+
# 获取主 agent 的详细显示状态
|
|
847
|
+
try:
|
|
848
|
+
main_dyn_status = get_display_status(main_agent_id)
|
|
849
|
+
agent_dynamic_statuses[main_agent_id] = AgentDisplayStatus(
|
|
850
|
+
status=main_dyn_status['status'],
|
|
851
|
+
display=main_dyn_status['display'],
|
|
852
|
+
duration=main_dyn_status['duration'],
|
|
853
|
+
alert=main_dyn_status['alert']
|
|
854
|
+
)
|
|
855
|
+
except Exception as e:
|
|
856
|
+
logger.warning(f"Failed to get display status for {main_agent_id}: {e}")
|
|
857
|
+
|
|
858
|
+
# 处理活跃任务(简化:不在流程图中创建任务节点,任务信息由 agentActiveTasks 提供)
|
|
859
|
+
# 任务详情在 Agent 卡片内显示,流程图只显示 Agent 之间的委托关系
|
|
860
|
+
for run in active_runs[:20]:
|
|
861
|
+
child_key = run.get('childSessionKey', '')
|
|
862
|
+
requester_key = run.get('requesterSessionKey', '')
|
|
863
|
+
agent_id = _parse_agent_id(child_key)
|
|
864
|
+
requester_id = _parse_agent_id(requester_key)
|
|
865
|
+
if not agent_id:
|
|
866
|
+
continue
|
|
867
|
+
# 只更新 activePath,不创建任务节点
|
|
868
|
+
active_path.extend([agent_id])
|
|
869
|
+
if requester_id and requester_id != agent_id:
|
|
870
|
+
active_path.extend([requester_id])
|
|
871
|
+
except Exception as e:
|
|
872
|
+
logger.error(f"Error building collaboration dynamic: {e}")
|
|
873
|
+
|
|
874
|
+
recent_calls_raw = _get_recent_model_calls(30)
|
|
875
|
+
model_calls = [
|
|
876
|
+
ModelCall(
|
|
877
|
+
id=f"call-{i}",
|
|
878
|
+
agentId=r["agentId"],
|
|
879
|
+
model=r.get("model", ""),
|
|
880
|
+
sessionId=r.get("sessionId", ""),
|
|
881
|
+
trigger=r.get("trigger", ""),
|
|
882
|
+
tokens=r.get("tokens", 0),
|
|
883
|
+
timestamp=r.get("timestamp", 0),
|
|
884
|
+
time=r.get("time", "")
|
|
885
|
+
)
|
|
886
|
+
for i, r in enumerate(recent_calls_raw)
|
|
887
|
+
]
|
|
888
|
+
|
|
889
|
+
# 构建多任务并行数据
|
|
890
|
+
agent_active_tasks = _build_agent_active_tasks(active_runs, main_agent_id)
|
|
891
|
+
|
|
892
|
+
return CollaborationDynamic(
|
|
893
|
+
activePath=list(set(active_path)),
|
|
894
|
+
recentCalls=model_calls,
|
|
895
|
+
agentStatuses=agent_statuses,
|
|
896
|
+
agentDynamicStatuses=agent_dynamic_statuses,
|
|
897
|
+
taskNodes=task_nodes,
|
|
898
|
+
taskEdges=task_edges,
|
|
899
|
+
mainAgentId=main_agent_id,
|
|
900
|
+
lastUpdate=int(__import__('time').time() * 1000),
|
|
901
|
+
agentActiveTasks=agent_active_tasks
|
|
902
|
+
)
|