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.
Files changed (111) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/README.md +302 -0
  3. package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +132 -0
  4. package/docs/RELEASE-LATEST.md +189 -0
  5. package/docs/RELEASE-MODEL-CONFIG.md +95 -0
  6. package/docs/release-guide.md +259 -0
  7. package/docs/release-operations-manual.md +167 -0
  8. package/docs/specs/tr3-install-system.md +580 -0
  9. package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
  10. package/frontend/index.html +12 -0
  11. package/frontend/package-lock.json +1240 -0
  12. package/frontend/package.json +19 -0
  13. package/frontend/src/App.vue +331 -0
  14. package/frontend/src/components/AgentCard.vue +796 -0
  15. package/frontend/src/components/AgentConfigPanel.vue +539 -0
  16. package/frontend/src/components/AgentDetailPanel.vue +738 -0
  17. package/frontend/src/components/ErrorAnalysisView.vue +546 -0
  18. package/frontend/src/components/ErrorCenterPanel.vue +844 -0
  19. package/frontend/src/components/PerformanceMonitor.vue +515 -0
  20. package/frontend/src/components/SettingsPanel.vue +236 -0
  21. package/frontend/src/components/TokenAnalysisPanel.vue +683 -0
  22. package/frontend/src/components/chain/ChainEdge.vue +85 -0
  23. package/frontend/src/components/chain/ChainNode.vue +166 -0
  24. package/frontend/src/components/chain/TaskChainView.vue +425 -0
  25. package/frontend/src/components/chain/index.ts +3 -0
  26. package/frontend/src/components/chain/types.ts +70 -0
  27. package/frontend/src/components/collaboration/CollaborationFlowSection.vue +1032 -0
  28. package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +113 -0
  29. package/frontend/src/components/performance/PerformancePanel.vue +119 -0
  30. package/frontend/src/components/performance/PerformanceSection.vue +1137 -0
  31. package/frontend/src/components/tasks/TaskStatusSection.vue +973 -0
  32. package/frontend/src/components/timeline/TimelineConnector.vue +31 -0
  33. package/frontend/src/components/timeline/TimelineRound.vue +135 -0
  34. package/frontend/src/components/timeline/TimelineStep.vue +691 -0
  35. package/frontend/src/components/timeline/TimelineToolLink.vue +109 -0
  36. package/frontend/src/components/timeline/TimelineView.vue +540 -0
  37. package/frontend/src/components/timeline/index.ts +5 -0
  38. package/frontend/src/components/timeline/types.ts +120 -0
  39. package/frontend/src/composables/index.ts +7 -0
  40. package/frontend/src/composables/useDebounce.ts +48 -0
  41. package/frontend/src/composables/useRealtime.ts +52 -0
  42. package/frontend/src/composables/useState.ts +52 -0
  43. package/frontend/src/composables/useThrottle.ts +46 -0
  44. package/frontend/src/composables/useVirtualScroll.ts +106 -0
  45. package/frontend/src/main.ts +4 -0
  46. package/frontend/src/managers/EventDispatcher.ts +127 -0
  47. package/frontend/src/managers/RealtimeDataManager.ts +293 -0
  48. package/frontend/src/managers/StateManager.ts +128 -0
  49. package/frontend/src/managers/index.ts +5 -0
  50. package/frontend/src/types/collaboration.ts +135 -0
  51. package/frontend/src/types/index.ts +20 -0
  52. package/frontend/src/types/performance.ts +105 -0
  53. package/frontend/src/types/task.ts +38 -0
  54. package/frontend/vite.config.ts +18 -0
  55. package/package.json +22 -0
  56. package/plugin/README.md +99 -0
  57. package/plugin/config.json.example +1 -0
  58. package/plugin/index.js +250 -0
  59. package/plugin/openclaw.plugin.json +17 -0
  60. package/plugin/package.json +21 -0
  61. package/scripts/build-plugin.js +67 -0
  62. package/scripts/bundle.sh +62 -0
  63. package/scripts/install-plugin.sh +162 -0
  64. package/scripts/install-python-deps.js +346 -0
  65. package/scripts/install-python-deps.sh +226 -0
  66. package/scripts/install.js +512 -0
  67. package/scripts/install.sh +367 -0
  68. package/scripts/lib/common.js +490 -0
  69. package/scripts/lib/common.sh +137 -0
  70. package/scripts/release-pack.sh +110 -0
  71. package/scripts/start.js +50 -0
  72. package/scripts/test_available_models.py +284 -0
  73. package/scripts/test_websocket_ping.py +44 -0
  74. package/src/backend/agents.py +73 -0
  75. package/src/backend/api/__init__.py +1 -0
  76. package/src/backend/api/agent_config_api.py +90 -0
  77. package/src/backend/api/agents.py +73 -0
  78. package/src/backend/api/agents_config.py +75 -0
  79. package/src/backend/api/chains.py +126 -0
  80. package/src/backend/api/collaboration.py +902 -0
  81. package/src/backend/api/debug_paths.py +39 -0
  82. package/src/backend/api/error_analysis.py +146 -0
  83. package/src/backend/api/errors.py +281 -0
  84. package/src/backend/api/performance.py +784 -0
  85. package/src/backend/api/subagents.py +770 -0
  86. package/src/backend/api/timeline.py +144 -0
  87. package/src/backend/api/websocket.py +251 -0
  88. package/src/backend/collaboration.py +405 -0
  89. package/src/backend/data/__init__.py +1 -0
  90. package/src/backend/data/agent_config_manager.py +270 -0
  91. package/src/backend/data/chain_reader.py +299 -0
  92. package/src/backend/data/config_reader.py +153 -0
  93. package/src/backend/data/error_analyzer.py +430 -0
  94. package/src/backend/data/session_reader.py +445 -0
  95. package/src/backend/data/subagent_reader.py +244 -0
  96. package/src/backend/data/task_history.py +118 -0
  97. package/src/backend/data/timeline_reader.py +981 -0
  98. package/src/backend/errors.py +63 -0
  99. package/src/backend/main.py +89 -0
  100. package/src/backend/mechanism_reader.py +131 -0
  101. package/src/backend/mechanisms.py +32 -0
  102. package/src/backend/performance.py +474 -0
  103. package/src/backend/requirements.txt +5 -0
  104. package/src/backend/session_reader.py +238 -0
  105. package/src/backend/status/__init__.py +1 -0
  106. package/src/backend/status/error_detector.py +122 -0
  107. package/src/backend/status/status_calculator.py +301 -0
  108. package/src/backend/status_calculator.py +121 -0
  109. package/src/backend/subagent_reader.py +229 -0
  110. package/src/backend/watchers/__init__.py +4 -0
  111. 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
+ )