openclaw-agent-dashboard 1.0.21 → 1.0.23

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 (135) hide show
  1. package/README.md +55 -321
  2. package/frontend-dist/assets/index-B7XqKAxm.css +1 -0
  3. package/frontend-dist/assets/index-CxJaSYyo.js +24 -0
  4. package/{frontend → frontend-dist}/index.html +2 -1
  5. package/{plugin/openclaw.plugin.json → openclaw.plugin.json} +2 -2
  6. package/package.json +21 -13
  7. package/.github/workflows/release.yml +0 -56
  8. package/VERSION_DISPLAY_delivery.md +0 -242
  9. package/VERSION_DISPLAY_implementation_summary.md +0 -315
  10. package/design_manifest.md +0 -100
  11. package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +0 -132
  12. package/docs/MAINTAINER_RELEASE_WORKFLOW.md +0 -211
  13. package/docs/Openclaw-Agent-Dashboard/345/217/221/345/270/203/344/270/216/346/233/264/346/226/260.md +0 -147
  14. package/docs/RELEASE-LATEST.md +0 -189
  15. package/docs/RELEASE-MODEL-CONFIG.md +0 -95
  16. package/docs/WINDOWS_INSTALL_TROUBLESHOOTING.md +0 -171
  17. package/docs/design/.gitkeep +0 -0
  18. package/docs/design/VERSION_DISPLAY_design.md +0 -1236
  19. package/docs/release-guide.md +0 -259
  20. package/docs/release-operations-manual.md +0 -167
  21. package/docs/reviews/.gitkeep +0 -0
  22. package/docs/reviews/approval_history.json +0 -14
  23. package/docs/reviews/cr_VERSION_DISPLAY.md +0 -397
  24. package/docs/reviews/traceability_manifest.json +0 -279
  25. package/docs/specs/VERSION_DISPLAY_spec.md +0 -371
  26. package/docs/specs/tr3-install-system.md +0 -580
  27. package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
  28. package/frontend/package-lock.json +0 -1240
  29. package/frontend/package.json +0 -19
  30. package/frontend/src/App.vue +0 -355
  31. package/frontend/src/components/AgentCard.vue +0 -796
  32. package/frontend/src/components/AgentConfigPanel.vue +0 -539
  33. package/frontend/src/components/AgentDetailPanel.vue +0 -738
  34. package/frontend/src/components/ErrorAnalysisView.vue +0 -546
  35. package/frontend/src/components/ErrorCenterPanel.vue +0 -844
  36. package/frontend/src/components/PerformanceMonitor.vue +0 -515
  37. package/frontend/src/components/SettingsPanel.vue +0 -236
  38. package/frontend/src/components/TokenAnalysisPanel.vue +0 -683
  39. package/frontend/src/components/chain/ChainEdge.vue +0 -85
  40. package/frontend/src/components/chain/ChainNode.vue +0 -166
  41. package/frontend/src/components/chain/TaskChainView.vue +0 -425
  42. package/frontend/src/components/chain/index.ts +0 -3
  43. package/frontend/src/components/chain/types.ts +0 -70
  44. package/frontend/src/components/collaboration/CollaborationFlowSection.vue +0 -1032
  45. package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +0 -113
  46. package/frontend/src/components/common/VersionDisplay.vue +0 -187
  47. package/frontend/src/components/performance/PerformancePanel.vue +0 -119
  48. package/frontend/src/components/performance/PerformanceSection.vue +0 -1137
  49. package/frontend/src/components/tasks/TaskStatusSection.vue +0 -973
  50. package/frontend/src/components/timeline/TimelineConnector.vue +0 -31
  51. package/frontend/src/components/timeline/TimelineRound.vue +0 -135
  52. package/frontend/src/components/timeline/TimelineStep.vue +0 -691
  53. package/frontend/src/components/timeline/TimelineToolLink.vue +0 -109
  54. package/frontend/src/components/timeline/TimelineView.vue +0 -540
  55. package/frontend/src/components/timeline/index.ts +0 -5
  56. package/frontend/src/components/timeline/types.ts +0 -120
  57. package/frontend/src/composables/index.ts +0 -7
  58. package/frontend/src/composables/useDebounce.ts +0 -48
  59. package/frontend/src/composables/useRealtime.ts +0 -52
  60. package/frontend/src/composables/useState.ts +0 -52
  61. package/frontend/src/composables/useThrottle.ts +0 -46
  62. package/frontend/src/composables/useVirtualScroll.ts +0 -106
  63. package/frontend/src/main.ts +0 -4
  64. package/frontend/src/managers/EventDispatcher.ts +0 -127
  65. package/frontend/src/managers/RealtimeDataManager.ts +0 -302
  66. package/frontend/src/managers/StateManager.ts +0 -128
  67. package/frontend/src/managers/index.ts +0 -5
  68. package/frontend/src/types/collaboration.ts +0 -135
  69. package/frontend/src/types/index.ts +0 -20
  70. package/frontend/src/types/performance.ts +0 -105
  71. package/frontend/src/types/task.ts +0 -38
  72. package/frontend/vite.config.ts +0 -18
  73. package/legacy_code_anatomy.md +0 -518
  74. package/plugin/README.md +0 -99
  75. package/plugin/config.json.example +0 -1
  76. package/plugin/package.json +0 -26
  77. package/scripts/build-plugin.js +0 -81
  78. package/scripts/bundle.sh +0 -62
  79. package/scripts/install-plugin.sh +0 -162
  80. package/scripts/install-python-deps.sh +0 -226
  81. package/scripts/install.js +0 -684
  82. package/scripts/install.sh +0 -367
  83. package/scripts/lib/common.sh +0 -137
  84. package/scripts/release-pack.sh +0 -110
  85. package/scripts/start.js +0 -50
  86. package/scripts/test_available_models.py +0 -284
  87. package/scripts/test_version_display.sh +0 -128
  88. package/scripts/test_websocket_ping.py +0 -44
  89. package/session_registry.json +0 -58
  90. package/tests/.gitkeep +0 -0
  91. package/tests/qa_regression_report.md +0 -359
  92. package/tests/qa_version_display_report.md +0 -598
  93. /package/{src/backend → dashboard}/agents.py +0 -0
  94. /package/{src/backend → dashboard}/api/__init__.py +0 -0
  95. /package/{src/backend → dashboard}/api/agent_config_api.py +0 -0
  96. /package/{src/backend → dashboard}/api/agents.py +0 -0
  97. /package/{src/backend → dashboard}/api/agents_config.py +0 -0
  98. /package/{src/backend → dashboard}/api/chains.py +0 -0
  99. /package/{src/backend → dashboard}/api/collaboration.py +0 -0
  100. /package/{src/backend → dashboard}/api/debug_paths.py +0 -0
  101. /package/{src/backend → dashboard}/api/error_analysis.py +0 -0
  102. /package/{src/backend → dashboard}/api/errors.py +0 -0
  103. /package/{src/backend → dashboard}/api/performance.py +0 -0
  104. /package/{src/backend → dashboard}/api/subagents.py +0 -0
  105. /package/{src/backend → dashboard}/api/timeline.py +0 -0
  106. /package/{src/backend → dashboard}/api/version.py +0 -0
  107. /package/{src/backend → dashboard}/api/websocket.py +0 -0
  108. /package/{src/backend → dashboard}/collaboration.py +0 -0
  109. /package/{src/backend → dashboard}/data/__init__.py +0 -0
  110. /package/{src/backend → dashboard}/data/agent_config_manager.py +0 -0
  111. /package/{src/backend → dashboard}/data/chain_reader.py +0 -0
  112. /package/{src/backend → dashboard}/data/config_reader.py +0 -0
  113. /package/{src/backend → dashboard}/data/error_analyzer.py +0 -0
  114. /package/{src/backend → dashboard}/data/session_reader.py +0 -0
  115. /package/{src/backend → dashboard}/data/subagent_reader.py +0 -0
  116. /package/{src/backend → dashboard}/data/task_history.py +0 -0
  117. /package/{src/backend → dashboard}/data/timeline_reader.py +0 -0
  118. /package/{src/backend → dashboard}/data/version_info_reader.py +0 -0
  119. /package/{src/backend → dashboard}/errors.py +0 -0
  120. /package/{src/backend → dashboard}/main.py +0 -0
  121. /package/{src/backend → dashboard}/mechanism_reader.py +0 -0
  122. /package/{src/backend → dashboard}/mechanisms.py +0 -0
  123. /package/{src/backend → dashboard}/performance.py +0 -0
  124. /package/{src/backend → dashboard}/requirements.txt +0 -0
  125. /package/{src/backend → dashboard}/session_reader.py +0 -0
  126. /package/{src/backend → dashboard}/status/__init__.py +0 -0
  127. /package/{src/backend → dashboard}/status/change_tracker.py +0 -0
  128. /package/{src/backend → dashboard}/status/error_detector.py +0 -0
  129. /package/{src/backend → dashboard}/status/status_cache.py +0 -0
  130. /package/{src/backend → dashboard}/status/status_calculator.py +0 -0
  131. /package/{src/backend → dashboard}/status_calculator.py +0 -0
  132. /package/{src/backend → dashboard}/subagent_reader.py +0 -0
  133. /package/{src/backend → dashboard}/watchers/__init__.py +0 -0
  134. /package/{src/backend → dashboard}/watchers/file_watcher.py +0 -0
  135. /package/{plugin/index.js → index.js} +0 -0
@@ -1,1032 +0,0 @@
1
- <template>
2
- <div class="collaboration-flow-section">
3
- <div class="section-header">
4
- <h2>协作流程</h2>
5
- <div class="header-right">
6
- <div v-if="agentNodes.length > 0" class="flow-legend flow-legend-inline">
7
- <div v-for="aid in legendAgentIds" :key="aid" class="legend-item">
8
- <span class="legend-dot" :style="{ background: getAgentColor(aid) }"></span>
9
- <span class="legend-name">{{ getAgentName(aid) }}</span>
10
- </div>
11
- </div>
12
- <div class="connection-indicator" :class="connectionState.status">
13
- <span class="indicator-dot"></span>
14
- <span class="indicator-text">{{ connectionLabel }}</span>
15
- </div>
16
- </div>
17
- </div>
18
-
19
- <div class="flow-container" ref="flowContainerRef">
20
- <div v-if="loading" class="loading-state">
21
- <div class="spinner"></div>
22
- <span>加载中...</span>
23
- </div>
24
-
25
- <div v-else-if="error" class="error-state">
26
- <span class="error-icon">⚠️</span>
27
- <span>{{ error }}</span>
28
- <button @click="refreshData" class="retry-btn">重试</button>
29
- </div>
30
-
31
- <div v-else-if="nodes.length === 0" class="empty-state">
32
- <span class="empty-icon">📭</span>
33
- <span>暂无协作数据</span>
34
- </div>
35
-
36
- <!-- 主布局:Agent区 + 模型面板 -->
37
- <div v-else class="flow-layout">
38
- <!-- Agent 树形布局区 -->
39
- <div class="agent-area" ref="agentAreaRef">
40
- <!-- 按层级渲染 -->
41
- <div v-for="(levelAgents, level) in agentsByLevel" :key="level" class="level-section">
42
- <!-- 层级标签 -->
43
- <div class="level-header">
44
- <span class="level-badge">L{{ level }}</span>
45
- <span class="level-title">{{ getLevelTitle(Number(level)) }}</span>
46
- </div>
47
- <!-- 该层的 Agent 卡片 -->
48
- <div class="level-cards">
49
- <div
50
- v-for="node in levelAgents"
51
- :key="node.id"
52
- class="agent-card-wrapper"
53
- :class="{
54
- 'main-agent': node.id === computedMainAgentId,
55
- 'active': isActiveNode(node),
56
- [`status-${node.status}`]: true
57
- }"
58
- :style="{ '--agent-color': getAgentColor(node.id) }"
59
- :ref="el => setAgentRef(node.id, el)"
60
- @click="emit('agent-click', node)"
61
- >
62
- <AgentCard
63
- :agent="getAgentForNode(node)"
64
- :model-info="getModelInfoForNode(node)"
65
- :is-main="node.id === computedMainAgentId"
66
- :current-task="node.currentTask"
67
- :error="node.error"
68
- :stuck-warning="node.stuckWarning"
69
- :hierarchy-depth="getAgentDepth(node.id)"
70
- :agent-color="getAgentColor(node.id)"
71
- :sub-status="node.subStatus"
72
- :current-action="node.currentAction"
73
- :tool-name="node.toolName"
74
- :waiting-for="node.waitingFor"
75
- :agent-tasks="getAgentActiveTasks(node.id)"
76
- />
77
- </div>
78
- </div>
79
- </div>
80
-
81
- <!-- 连线 SVG 层 -->
82
- <svg class="edges-svg" ref="edgesSvgRef">
83
- <defs>
84
- <linearGradient id="lightGradient" x1="0%" y1="0%" x2="100%" y2="0%">
85
- <stop offset="0%" style="stop-color:#4a9eff;stop-opacity:0.2" />
86
- <stop offset="50%" style="stop-color:#4a9eff;stop-opacity:1" />
87
- <stop offset="100%" style="stop-color:#4a9eff;stop-opacity:0.2" />
88
- </linearGradient>
89
- </defs>
90
- <g v-for="edge in delegateEdges" :key="edge.id">
91
- <path
92
- :d="getEdgePath(edge)"
93
- class="edge-path"
94
- :class="{ active: isActiveEdge(edge) }"
95
- stroke="#4a9eff"
96
- stroke-width="2"
97
- fill="none"
98
- />
99
- <circle
100
- v-if="isActiveEdge(edge)"
101
- r="5"
102
- fill="url(#lightGradient)"
103
- >
104
- <animateMotion :dur="'2s'" repeatCount="1" rotate="0">
105
- <mpath :href="`#edge-${edge.id}`" />
106
- </animateMotion>
107
- </circle>
108
- </g>
109
- </svg>
110
- </div>
111
-
112
- <!-- 右侧模型面板 -->
113
- <div class="model-panel" v-if="modelNodes.length > 0">
114
- <div class="model-panel-header" @click="modelPanelExpanded = !modelPanelExpanded">
115
- <span class="model-panel-title">🧠 模型</span>
116
- <span class="model-toggle-icon">{{ modelPanelExpanded ? '▼' : '▶' }}</span>
117
- </div>
118
- <div class="model-panel-body" v-show="modelPanelExpanded">
119
- <div
120
- v-for="modelNode in modelNodes"
121
- :key="modelNode.id"
122
- class="model-card"
123
- :class="{ active: isModelActive(modelNode) }"
124
- >
125
- <div class="model-name">{{ modelNode.name }}</div>
126
- <div class="model-dots">
127
- <span
128
- v-for="call in getCallsForModelNode(modelNode).slice(0, 8)"
129
- :key="call.id"
130
- class="model-dot"
131
- :style="{ background: getAgentColor(call.agentId) }"
132
- :title="`${getAgentName(call.agentId)} @ ${call.model || 'unknown'}`"
133
- @click.stop="selectedCall = call"
134
- />
135
- </div>
136
- <div class="model-count">{{ getCallsForModelNode(modelNode).length }}</div>
137
- </div>
138
- </div>
139
- </div>
140
- </div>
141
- </div>
142
-
143
- <!-- 调用详情弹窗 -->
144
- <div v-if="selectedCall" class="call-detail-overlay" @click.self="selectedCall = null">
145
- <div class="call-detail-modal">
146
- <div class="call-detail-header">
147
- <h3>调用详情</h3>
148
- <button class="close-btn" @click="selectedCall = null">×</button>
149
- </div>
150
- <div class="call-detail-body">
151
- <div class="call-detail-row">
152
- <span class="label">Agent</span>
153
- <span class="value">{{ selectedCall.agentId }}</span>
154
- </div>
155
- <div class="call-detail-row">
156
- <span class="label">模型</span>
157
- <span class="value">{{ selectedCall.model }}</span>
158
- </div>
159
- <div class="call-detail-row">
160
- <span class="label">时间</span>
161
- <span class="value">{{ selectedCall.time }}</span>
162
- </div>
163
- <div class="call-detail-row">
164
- <span class="label">Tokens</span>
165
- <span class="value">{{ selectedCall.tokens }}</span>
166
- </div>
167
- <div class="call-detail-row trigger">
168
- <span class="label">触发</span>
169
- <div class="value">{{ selectedCall.trigger?.replace(/^【完成回传】/, '') }}</div>
170
- </div>
171
- </div>
172
- </div>
173
- </div>
174
- </div>
175
- </template>
176
-
177
- <script setup lang="ts">
178
- import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
179
- import { useRealtime } from '../../composables'
180
- import AgentCard from '../AgentCard.vue'
181
- import type { CollaborationNode, CollaborationEdge, CollaborationFlow, CollaborationDynamic, ModelCall, AgentDisplayStatus, ActiveTask } from '../../types'
182
-
183
- const DYNAMIC_POLL_INTERVAL_MS = 3000
184
-
185
- interface AgentForCard {
186
- name: string
187
- status: 'idle' | 'working' | 'down'
188
- currentTask?: string
189
- lastActiveFormatted?: string
190
- }
191
-
192
- interface TaskInfo {
193
- id: string
194
- name: string
195
- status: 'working' | 'completed' | 'failed' | 'pending'
196
- time?: string
197
- }
198
-
199
- const props = defineProps<{
200
- mainAgent?: AgentForCard & { id?: string } | null
201
- subAgents?: (AgentForCard & { id?: string })[]
202
- mainAgentId?: string
203
- }>()
204
-
205
- const emit = defineEmits<{
206
- 'agent-click': [node: CollaborationNode]
207
- }>()
208
-
209
- const { connectionState, subscribe } = useRealtime()
210
-
211
- const nodes = ref<CollaborationNode[]>([])
212
- const edges = ref<CollaborationEdge[]>([])
213
- const activePath = ref<string[]>([])
214
- const agentModels = ref<Record<string, { primary?: string; fallbacks?: string[] }>>({})
215
- const recentCalls = ref<ModelCall[]>([])
216
- const loading = ref(true)
217
- const error = ref<string | null>(null)
218
- const selectedCall = ref<ModelCall | null>(null)
219
- const hierarchy = ref<Record<string, string[]>>({})
220
- const depths = ref<Record<string, number>>({})
221
- const backendMainAgentId = ref<string>('')
222
- const modelPanelExpanded = ref(true)
223
- const agentActiveTasks = ref<Record<string, ActiveTask[]>>({})
224
-
225
- // DOM refs
226
- const agentAreaRef = ref<HTMLElement | null>(null)
227
- const edgesSvgRef = ref<SVGSVGElement | null>(null)
228
- const agentRefs = ref<Record<string, HTMLElement | null>>({})
229
-
230
- function setAgentRef(id: string, el: unknown) {
231
- if (el) agentRefs.value[id] = el as HTMLElement
232
- }
233
-
234
- // 计算主 Agent ID
235
- const computedMainAgentId = computed(() => {
236
- if (props.mainAgentId) return props.mainAgentId
237
- if (backendMainAgentId.value) return backendMainAgentId.value
238
- return 'main'
239
- })
240
-
241
- // Agent / Model 节点
242
- const agentNodes = computed(() => nodes.value.filter(n => n.type === 'agent'))
243
- const modelNodes = computed(() => nodes.value.filter(n => n.type === 'model'))
244
- const delegateEdges = computed(() => edges.value.filter(e => e.type === 'delegates'))
245
-
246
- // 按层级分组(注意:v-for 中 (value, key) 顺序)
247
- const agentsByLevel = computed(() => {
248
- const byDepth: Record<number, CollaborationNode[]> = {}
249
- for (const node of agentNodes.value) {
250
- const d = getAgentDepth(node.id)
251
- if (!byDepth[d]) byDepth[d] = []
252
- byDepth[d].push(node)
253
- }
254
- // 按 depth 排序返回
255
- const sorted: Record<number, CollaborationNode[]> = {}
256
- Object.keys(byDepth)
257
- .map(Number)
258
- .sort((a, b) => a - b)
259
- .forEach(d => { sorted[d] = byDepth[d] })
260
- return sorted
261
- })
262
-
263
- // 层级标题
264
- function getLevelTitle(level: number): string {
265
- if (level === 0) return '主控'
266
- if (level === 1) return '一级子代理'
267
- return `${level}级子代理`
268
- }
269
-
270
- // Agent 颜色
271
- const AGENT_COLORS: Record<string, string> = {
272
- 'analyst-agent': '#10b981',
273
- 'architect-agent': '#f59e0b',
274
- 'devops-agent': '#8b5cf6',
275
- 'test-agent': '#ec4899',
276
- 'frontend-agent': '#06b6d4',
277
- 'backend-agent': '#f43f5e'
278
- }
279
-
280
- function getAgentColor(agentId: string): string {
281
- if (agentId === computedMainAgentId.value) return '#4a9eff'
282
- return AGENT_COLORS[agentId] || '#64748b'
283
- }
284
-
285
- // 图例
286
- const legendAgentIds = computed(() => agentNodes.value.map(n => n.id).filter(Boolean))
287
-
288
- const connectionLabel = computed(() => {
289
- const labels: Record<string, string> = {
290
- connected: '已连接',
291
- connecting: '连接中...',
292
- disconnected: '未连接',
293
- error: '连接错误'
294
- }
295
- return labels[connectionState.value.status] || '未知'
296
- })
297
-
298
- // 获取 Agent 层级深度
299
- function getAgentDepth(agentId: string): number {
300
- // 主 agent depth=0,其他默认 depth=1
301
- if (agentId === computedMainAgentId.value) {
302
- return depths.value[agentId] ?? 0
303
- }
304
- return depths.value[agentId] ?? 1
305
- }
306
-
307
- function getAgentName(agentId: string): string {
308
- const node = agentNodes.value.find(n => n.id === agentId)
309
- return node?.name || agentId
310
- }
311
-
312
- function getAgentForNode(node: CollaborationNode): AgentForCard {
313
- const statusMap = (s: string): 'idle' | 'working' | 'down' =>
314
- s === 'error' ? 'down' : (s as 'idle' | 'working')
315
-
316
- const fromProps = node.id === computedMainAgentId.value
317
- ? props.mainAgent
318
- : props.subAgents?.find(a => a.id === node.id)
319
-
320
- if (fromProps) {
321
- return {
322
- name: fromProps.name,
323
- status: fromProps.status,
324
- currentTask: fromProps.currentTask,
325
- lastActiveFormatted: fromProps.lastActiveFormatted
326
- }
327
- }
328
- return { name: node.name, status: statusMap(node.status) }
329
- }
330
-
331
- function getModelInfoForNode(node: CollaborationNode): { primary?: string; fallbacks?: string[] } | undefined {
332
- if (node.type !== 'agent') return undefined
333
- return agentModels.value[node.id]
334
- }
335
-
336
- function getAgentActiveTasks(agentId: string): ActiveTask[] | undefined {
337
- return agentActiveTasks.value[agentId]
338
- }
339
-
340
- function getAgentTasks(agentId: string): TaskInfo[] {
341
- const tasks = nodes.value.filter(n => n.type === 'task')
342
- const taskEdges = edges.value.filter(e => e.type === 'calls' && e.source === agentId)
343
- return taskEdges.map(edge => {
344
- const taskNode = tasks.find(t => t.id === edge.target)
345
- if (!taskNode) return null
346
- return {
347
- id: taskNode.id,
348
- name: taskNode.name,
349
- status: taskNode.status === 'working' ? 'working' :
350
- taskNode.status === 'completed' ? 'completed' :
351
- taskNode.status === 'error' ? 'failed' : 'pending',
352
- time: taskNode.timestamp ? new Date(taskNode.timestamp).toLocaleTimeString() : undefined
353
- }
354
- }).filter(Boolean) as TaskInfo[]
355
- }
356
-
357
- function isActiveNode(node: CollaborationNode): boolean {
358
- return activePath.value.includes(node.id)
359
- }
360
-
361
- function isActiveEdge(edge: CollaborationEdge): boolean {
362
- return activePath.value.includes(edge.source) && activePath.value.includes(edge.target)
363
- }
364
-
365
- // 计算连线路径
366
- function getEdgePath(edge: CollaborationEdge): string {
367
- const sourceEl = agentRefs.value[edge.source]
368
- const targetEl = agentRefs.value[edge.target]
369
- const areaEl = agentAreaRef.value
370
-
371
- if (!sourceEl || !targetEl || !areaEl) return ''
372
-
373
- const areaRect = areaEl.getBoundingClientRect()
374
- const sourceRect = sourceEl.getBoundingClientRect()
375
- const targetRect = targetEl.getBoundingClientRect()
376
-
377
- // 相对于 agent-area 的坐标
378
- const x1 = sourceRect.left - areaRect.left + sourceRect.width / 2
379
- const y1 = sourceRect.top - areaRect.top + sourceRect.height
380
- const x2 = targetRect.left - areaRect.left + targetRect.width / 2
381
- const y2 = targetRect.top - areaRect.top
382
-
383
- const cy = (y1 + y2) / 2
384
- return `M ${x1} ${y1} C ${x1} ${cy}, ${x2} ${cy}, ${x2} ${y2}`
385
- }
386
-
387
- // 更新 SVG 尺寸和连线
388
- function updateEdges() {
389
- nextTick(() => {
390
- if (!agentAreaRef.value || !edgesSvgRef.value) return
391
- const rect = agentAreaRef.value.getBoundingClientRect()
392
- edgesSvgRef.value.setAttribute('width', String(rect.width))
393
- edgesSvgRef.value.setAttribute('height', String(rect.height))
394
- })
395
- }
396
-
397
- // 模型相关
398
- const callsPerModel = computed(() => {
399
- const map: Record<string, ModelCall[]> = {}
400
- for (const c of recentCalls.value) {
401
- const mid = c.model || '(unknown)'
402
- if (!map[mid]) map[mid] = []
403
- map[mid].push(c)
404
- }
405
- return map
406
- })
407
-
408
- function getCallsForModelNode(node: CollaborationNode): ModelCall[] {
409
- const modelId = (node.metadata as { modelId?: string })?.modelId || ''
410
- const short = modelId.split('/').pop() || modelId
411
- return callsPerModel.value[modelId] || callsPerModel.value[short] || []
412
- }
413
-
414
- function isModelActive(node: CollaborationNode): boolean {
415
- return getCallsForModelNode(node).length > 0
416
- }
417
-
418
- async function fetchData(): Promise<void> {
419
- loading.value = true
420
- error.value = null
421
- try {
422
- const response = await fetch('/api/collaboration')
423
- if (!response.ok) throw new Error('Failed to fetch collaboration data')
424
- const data: CollaborationFlow = await response.json()
425
- nodes.value = data.nodes || []
426
- edges.value = data.edges || []
427
- activePath.value = data.activePath || []
428
- agentModels.value = data.agentModels || {}
429
- recentCalls.value = data.recentCalls || []
430
- if (data.hierarchy) hierarchy.value = data.hierarchy
431
- if (data.depths) depths.value = data.depths
432
- if (data.mainAgentId) backendMainAgentId.value = data.mainAgentId
433
- if (data.agentActiveTasks) agentActiveTasks.value = data.agentActiveTasks
434
-
435
- nextTick(updateEdges)
436
- } catch (e) {
437
- error.value = (e as Error).message
438
- } finally {
439
- loading.value = false
440
- }
441
- }
442
-
443
- function refreshData(): void {
444
- fetchData()
445
- }
446
-
447
- function handleCollaborationUpdate(data: unknown): void {
448
- const flow = data as CollaborationFlow
449
- nodes.value = flow.nodes || []
450
- edges.value = flow.edges || []
451
- activePath.value = flow.activePath || []
452
- agentModels.value = flow.agentModels || {}
453
- recentCalls.value = flow.recentCalls || []
454
- if (flow.hierarchy) hierarchy.value = flow.hierarchy
455
- if (flow.depths) depths.value = flow.depths
456
- if (flow.mainAgentId) backendMainAgentId.value = flow.mainAgentId
457
- if (flow.agentActiveTasks) agentActiveTasks.value = flow.agentActiveTasks
458
- nextTick(updateEdges)
459
- }
460
-
461
- function handleCollaborationDynamicUpdate(dyn: CollaborationDynamic): void {
462
- activePath.value = dyn.activePath || []
463
- recentCalls.value = dyn.recentCalls || []
464
- if (dyn.agentActiveTasks) agentActiveTasks.value = dyn.agentActiveTasks
465
- const agentNodesLocal = nodes.value.filter(n => n.type === 'agent')
466
- for (const node of agentNodesLocal) {
467
- if (node.id && dyn.agentStatuses && dyn.agentStatuses[node.id] !== undefined) {
468
- node.status = dyn.agentStatuses[node.id] as CollaborationNode['status']
469
- }
470
- // 更新详细状态 (TR5)
471
- if (node.id && dyn.agentDynamicStatuses && dyn.agentDynamicStatuses[node.id]) {
472
- const dynStatus = dyn.agentDynamicStatuses[node.id]
473
- node.subStatus = dynStatus.subStatus
474
- node.currentAction = dynStatus.currentAction
475
- node.toolName = dynStatus.toolName
476
- node.waitingFor = dynStatus.waitingFor
477
- }
478
- // 更新显示状态 (TR9-1:基于时间阈值)
479
- if (node.id && dyn.agentDisplayStatuses && dyn.agentDisplayStatuses[node.id]) {
480
- const displayStatus = dyn.agentDisplayStatuses[node.id]
481
- node.currentAction = displayStatus.display
482
- // 存储 duration 和 alert 到 metadata
483
- if (!node.metadata) node.metadata = {}
484
- node.metadata = {
485
- ...node.metadata,
486
- duration: displayStatus.duration,
487
- alert: displayStatus.alert
488
- }
489
- // 设置 alert 标志用于 UI 警告样式
490
- if (displayStatus.alert) {
491
- node.stuckWarning = {
492
- isStuck: true,
493
- idleSeconds: displayStatus.duration,
494
- lastUpdate: Date.now(),
495
- reason: 'self_busy',
496
- reasonDetail: displayStatus.display
497
- }
498
- } else {
499
- // 清除警告
500
- if (node.stuckWarning && !node.stuckWaitingForChildAgent) {
501
- node.stuckWarning = undefined
502
- }
503
- }
504
- }
505
- }
506
- const taskIdsBefore = new Set(nodes.value.filter(n => n.type === 'task').map(n => n.id))
507
- const taskIdsAfter = new Set((dyn.taskNodes || []).map(n => n.id))
508
- const topologyChanged = taskIdsBefore.size !== taskIdsAfter.size ||
509
- [...taskIdsAfter].some(id => !taskIdsBefore.has(id))
510
- if (topologyChanged) {
511
- const modelNodesLocal = nodes.value.filter(n => n.type === 'model')
512
- const delegateEdgesLocal = edges.value.filter(e => e.type === 'delegates')
513
- nodes.value = [...agentNodesLocal, ...(dyn.taskNodes || []), ...modelNodesLocal]
514
- edges.value = [...delegateEdgesLocal, ...(dyn.taskEdges || [])]
515
- nextTick(updateEdges)
516
- } else {
517
- const taskNodeMap = new Map((dyn.taskNodes || []).map(n => [n.id, n]))
518
- for (const t of nodes.value.filter(n => n.type === 'task')) {
519
- const updated = taskNodeMap.get(t.id)
520
- if (updated) {
521
- t.status = updated.status
522
- t.name = updated.name
523
- if (updated.timestamp) t.timestamp = updated.timestamp
524
- }
525
- }
526
- }
527
- }
528
-
529
- async function fetchDynamicData(): Promise<void> {
530
- if (loading.value || nodes.value.length === 0) return
531
- try {
532
- const res = await fetch('/api/collaboration/dynamic')
533
- if (!res.ok) return
534
- const data: CollaborationDynamic = await res.json()
535
- handleCollaborationDynamicUpdate(data)
536
- } catch {
537
- // 静默失败
538
- }
539
- }
540
-
541
- let unsubscribe: (() => void) | null = null
542
- let dynamicPollTimer: ReturnType<typeof setInterval> | null = null
543
-
544
- watch([agentNodes, modelPanelExpanded], () => {
545
- nextTick(updateEdges)
546
- })
547
-
548
- onMounted(() => {
549
- fetchData()
550
- unsubscribe = subscribe('collaboration', handleCollaborationUpdate)
551
- dynamicPollTimer = setInterval(fetchDynamicData, DYNAMIC_POLL_INTERVAL_MS)
552
- window.addEventListener('resize', updateEdges)
553
- })
554
-
555
- onUnmounted(() => {
556
- if (unsubscribe) unsubscribe()
557
- if (dynamicPollTimer) clearInterval(dynamicPollTimer)
558
- window.removeEventListener('resize', updateEdges)
559
- })
560
- </script>
561
-
562
- <style scoped>
563
- .collaboration-flow-section {
564
- background: white;
565
- border-radius: 8px;
566
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
567
- padding: 1.5rem;
568
- }
569
-
570
- .section-header {
571
- display: flex;
572
- justify-content: space-between;
573
- align-items: center;
574
- margin-bottom: 1rem;
575
- flex-wrap: wrap;
576
- gap: 0.5rem;
577
- }
578
-
579
- .section-header .header-right {
580
- display: flex;
581
- align-items: center;
582
- gap: 0.75rem;
583
- flex-wrap: wrap;
584
- }
585
-
586
- .section-header h2 {
587
- margin: 0;
588
- font-size: 1.3rem;
589
- color: #333;
590
- }
591
-
592
- .connection-indicator {
593
- display: flex;
594
- align-items: center;
595
- gap: 0.4rem;
596
- font-size: 0.8rem;
597
- padding: 0.25rem 0.6rem;
598
- border-radius: 12px;
599
- background: #f1f5f9;
600
- }
601
-
602
- .connection-indicator.connected { background: #dcfce7; color: #166534; }
603
- .connection-indicator.connecting { background: #fef3c7; color: #92400e; }
604
- .connection-indicator.disconnected,
605
- .connection-indicator.error { background: #fee2e2; color: #991b1b; }
606
-
607
- .indicator-dot {
608
- width: 6px;
609
- height: 6px;
610
- border-radius: 50%;
611
- background: currentColor;
612
- }
613
-
614
- .connection-indicator.connecting .indicator-dot {
615
- animation: pulse 1.5s ease-in-out infinite;
616
- }
617
-
618
- @keyframes pulse {
619
- 0%, 100% { opacity: 1; }
620
- 50% { opacity: 0.5; }
621
- }
622
-
623
- .flow-container {
624
- border: 1px solid #e5e7eb;
625
- border-radius: 6px;
626
- background: #fafbfc;
627
- min-height: 480px;
628
- position: relative;
629
- overflow: auto;
630
- }
631
-
632
- .loading-state, .error-state, .empty-state {
633
- display: flex;
634
- flex-direction: column;
635
- align-items: center;
636
- justify-content: center;
637
- height: 280px;
638
- gap: 0.75rem;
639
- color: #6b7280;
640
- }
641
-
642
- .spinner {
643
- width: 28px;
644
- height: 28px;
645
- border: 3px solid #e5e7eb;
646
- border-top-color: #4a9eff;
647
- border-radius: 50%;
648
- animation: spin 0.8s linear infinite;
649
- }
650
-
651
- @keyframes spin { to { transform: rotate(360deg); } }
652
-
653
- .retry-btn {
654
- padding: 0.4rem 0.8rem;
655
- background: #4a9eff;
656
- color: white;
657
- border: none;
658
- border-radius: 4px;
659
- cursor: pointer;
660
- font-size: 0.85rem;
661
- }
662
-
663
- /* 主布局 */
664
- .flow-layout {
665
- display: flex;
666
- min-height: 460px;
667
- }
668
-
669
- /* Agent 区域 */
670
- .agent-area {
671
- flex: 1;
672
- position: relative;
673
- padding: 1rem 1rem 2rem 0.5rem;
674
- min-width: 300px;
675
- }
676
-
677
- /* 层级区块 - 建立堆叠上下文,确保卡片在连线 SVG 之上 */
678
- .level-section {
679
- margin-bottom: 1.5rem;
680
- position: relative;
681
- z-index: 1;
682
- }
683
-
684
- /* L0 主控层:与子 agent 拉开距离 */
685
- .level-section:first-child {
686
- margin-bottom: 2.5rem;
687
- z-index: 2;
688
- }
689
-
690
- .level-header {
691
- display: flex;
692
- align-items: center;
693
- gap: 0.5rem;
694
- margin-bottom: 0.5rem;
695
- padding-left: 0.5rem;
696
- }
697
-
698
- .level-badge {
699
- display: inline-flex;
700
- align-items: center;
701
- justify-content: center;
702
- width: 24px;
703
- height: 20px;
704
- background: #e0e7ff;
705
- color: #4338ca;
706
- font-size: 0.65rem;
707
- font-weight: 700;
708
- border-radius: 4px;
709
- }
710
-
711
- .level-title {
712
- font-size: 0.75rem;
713
- color: #64748b;
714
- font-weight: 500;
715
- }
716
-
717
- /* L0 层(主 agent)- 居中 */
718
- .level-cards {
719
- display: flex;
720
- flex-wrap: wrap;
721
- gap: 16px;
722
- justify-content: center;
723
- padding: 0.5rem;
724
- }
725
-
726
- /* L0 层卡片容器 - 移除固定高度 */
727
- .level-section:first-child .level-cards {
728
- min-height: auto;
729
- padding-bottom: 1rem;
730
- }
731
-
732
- /* 子 agent 层级 - 使用 grid 均匀分布 */
733
- .level-section:not(:first-child) .level-cards {
734
- display: grid;
735
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
736
- gap: 16px;
737
- max-width: 900px;
738
- margin: 0 auto;
739
- }
740
-
741
- /* Agent 卡片 - z-index 确保在连线 SVG 之上 */
742
- .agent-card-wrapper {
743
- cursor: pointer;
744
- transition: transform 0.2s, box-shadow 0.2s;
745
- border-radius: 12px;
746
- position: relative;
747
- z-index: 1;
748
- overflow: hidden;
749
- }
750
-
751
- /* 主 agent 卡片 - 更大 */
752
- .level-section:first-child .agent-card-wrapper.main-agent {
753
- width: 320px;
754
- max-width: 100%;
755
- }
756
-
757
- .agent-card-wrapper:hover {
758
- transform: translateY(-2px);
759
- }
760
-
761
- .agent-card-wrapper.active {
762
- box-shadow: 0 0 0 3px rgba(74, 158, 255, 0.25);
763
- }
764
-
765
- /* 卡片左侧颜色条 - 用于区分不同 agent,与主框圆角对齐 */
766
- .agent-card-wrapper::before {
767
- content: '';
768
- position: absolute;
769
- left: 0;
770
- top: 0;
771
- bottom: 0;
772
- width: 4px;
773
- border-radius: 4px 0 0 4px;
774
- background: var(--agent-color, #64748b);
775
- z-index: 1;
776
- }
777
-
778
- /* 连线 SVG - 置于最上层才能可见(level-section 为块级覆盖整区,z-index 低会被完全遮挡) */
779
- .edges-svg {
780
- position: absolute;
781
- top: 0;
782
- left: 0;
783
- width: 100%;
784
- height: 100%;
785
- pointer-events: none;
786
- z-index: 5;
787
- }
788
-
789
- .edge-path {
790
- fill: none;
791
- stroke: #4a9eff;
792
- stroke-width: 2;
793
- }
794
-
795
- .edge-path.active {
796
- stroke-width: 2.5;
797
- stroke-dasharray: 6 4;
798
- animation: flowAnim 1s linear infinite;
799
- }
800
-
801
- @keyframes flowAnim {
802
- to { stroke-dashoffset: -10; }
803
- }
804
-
805
- /* 模型面板 */
806
- .model-panel {
807
- width: 150px;
808
- min-width: 120px;
809
- border-left: 1px solid #e5e7eb;
810
- background: #f8fafc;
811
- flex-shrink: 0;
812
- display: flex;
813
- flex-direction: column;
814
- }
815
-
816
- .model-panel-header {
817
- display: flex;
818
- justify-content: space-between;
819
- align-items: center;
820
- padding: 0.5rem 0.6rem;
821
- background: #f1f5f9;
822
- border-bottom: 1px solid #e5e7eb;
823
- cursor: pointer;
824
- }
825
-
826
- .model-panel-title {
827
- font-size: 0.7rem;
828
- font-weight: 600;
829
- color: #475569;
830
- }
831
-
832
- .model-toggle-icon {
833
- font-size: 0.6rem;
834
- color: #94a3b8;
835
- }
836
-
837
- .model-panel-body {
838
- flex: 1;
839
- padding: 0.4rem;
840
- overflow-y: auto;
841
- }
842
-
843
- .model-card {
844
- background: white;
845
- border: 1px solid #e2e8f0;
846
- border-radius: 5px;
847
- padding: 0.4rem 0.5rem;
848
- margin-bottom: 0.4rem;
849
- }
850
-
851
- .model-card.active {
852
- border-color: #f97316;
853
- }
854
-
855
- .model-name {
856
- font-size: 0.65rem;
857
- font-weight: 600;
858
- font-family: ui-monospace, monospace;
859
- color: #334155;
860
- margin-bottom: 0.25rem;
861
- white-space: nowrap;
862
- overflow: hidden;
863
- text-overflow: ellipsis;
864
- }
865
-
866
- .model-dots {
867
- display: flex;
868
- flex-wrap: wrap;
869
- gap: 2px;
870
- margin-bottom: 0.2rem;
871
- }
872
-
873
- .model-dot {
874
- width: 5px;
875
- height: 5px;
876
- border-radius: 50%;
877
- cursor: pointer;
878
- }
879
-
880
- .model-dot:hover {
881
- transform: scale(1.3);
882
- }
883
-
884
- .model-count {
885
- font-size: 0.55rem;
886
- color: #94a3b8;
887
- }
888
-
889
- /* 图例 - 移至标题行,避免遮挡 agent 卡片 */
890
- .flow-legend.flow-legend-inline {
891
- position: static;
892
- display: flex;
893
- flex-wrap: wrap;
894
- gap: 0.5rem;
895
- padding: 0.25rem 0.5rem;
896
- background: #f8fafc;
897
- border-radius: 6px;
898
- border: 1px solid #e5e7eb;
899
- font-size: 0.7rem;
900
- color: #475569;
901
- }
902
-
903
- .legend-item {
904
- display: flex;
905
- align-items: center;
906
- gap: 0.35rem;
907
- padding: 2px 6px;
908
- border-radius: 4px;
909
- background: #f8fafc;
910
- }
911
-
912
- .legend-dot {
913
- width: 10px;
914
- height: 10px;
915
- border-radius: 3px;
916
- }
917
-
918
- .legend-name {
919
- max-width: 100px;
920
- overflow: hidden;
921
- text-overflow: ellipsis;
922
- white-space: nowrap;
923
- font-weight: 500;
924
- }
925
-
926
- /* 调用详情弹窗 */
927
- .call-detail-overlay {
928
- position: fixed;
929
- inset: 0;
930
- background: rgba(0, 0, 0, 0.35);
931
- display: flex;
932
- align-items: center;
933
- justify-content: center;
934
- z-index: 1000;
935
- }
936
-
937
- .call-detail-modal {
938
- background: white;
939
- border-radius: 10px;
940
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
941
- min-width: 300px;
942
- max-width: 90%;
943
- }
944
-
945
- .call-detail-header {
946
- display: flex;
947
- justify-content: space-between;
948
- align-items: center;
949
- padding: 0.6rem 0.9rem;
950
- border-bottom: 1px solid #e5e7eb;
951
- }
952
-
953
- .call-detail-header h3 {
954
- margin: 0;
955
- font-size: 0.9rem;
956
- }
957
-
958
- .close-btn {
959
- background: none;
960
- border: none;
961
- font-size: 1.2rem;
962
- cursor: pointer;
963
- color: #6b7280;
964
- line-height: 1;
965
- }
966
-
967
- .call-detail-body {
968
- padding: 0.6rem 0.9rem;
969
- }
970
-
971
- .call-detail-row {
972
- display: flex;
973
- gap: 0.6rem;
974
- margin-bottom: 0.4rem;
975
- }
976
-
977
- .call-detail-row .label {
978
- color: #6b7280;
979
- min-width: 45px;
980
- font-size: 0.8rem;
981
- }
982
-
983
- .call-detail-row .value {
984
- color: #333;
985
- font-size: 0.8rem;
986
- word-break: break-word;
987
- }
988
-
989
- .call-detail-row.trigger .value {
990
- font-size: 0.75rem;
991
- }
992
-
993
- /* 响应式适配 */
994
- @media (max-width: 1280px) {
995
- .level-section:first-child .agent-card-wrapper.main-agent {
996
- width: 280px;
997
- }
998
-
999
- .level-section:not(:first-child) .level-cards {
1000
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
1001
- }
1002
-
1003
- .model-panel {
1004
- width: 130px;
1005
- }
1006
- }
1007
-
1008
- @media (max-width: 1024px) {
1009
- .flow-layout {
1010
- flex-direction: column;
1011
- }
1012
-
1013
- .model-panel {
1014
- width: 100%;
1015
- border-left: none;
1016
- border-top: 1px solid #e5e7eb;
1017
- max-height: 200px;
1018
- }
1019
-
1020
- .model-panel-body {
1021
- display: flex;
1022
- flex-wrap: wrap;
1023
- gap: 0.5rem;
1024
- }
1025
-
1026
- .model-card {
1027
- flex: 1;
1028
- min-width: 120px;
1029
- margin-bottom: 0;
1030
- }
1031
- }
1032
- </style>