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,1137 +0,0 @@
1
- <template>
2
- <div class="performance-section">
3
- <div class="section-header">
4
- <h2>性能数据</h2>
5
- <div class="time-range-selector">
6
- <button
7
- v-for="range in timeRanges"
8
- :key="range.value"
9
- :class="['range-btn', { active: selectedRange === range.value }]"
10
- @click="selectedRange = range.value"
11
- >
12
- {{ range.label }}
13
- </button>
14
- </div>
15
- </div>
16
-
17
- <div v-if="loading" class="loading-state">
18
- <div class="spinner"></div>
19
- <span>加载性能数据...</span>
20
- </div>
21
-
22
- <div v-else-if="error" class="error-state">
23
- <span class="error-icon">⚠️</span>
24
- <span>{{ error }}</span>
25
- <button @click="refreshData" class="retry-btn">重试</button>
26
- </div>
27
-
28
- <template v-else>
29
- <!-- Metric Cards -->
30
- <div class="metrics-grid">
31
- <div class="metric-card primary">
32
- <div class="metric-icon">⚡</div>
33
- <div class="metric-content">
34
- <div class="metric-label">TPM</div>
35
- <div class="metric-value">{{ formatNumber(performanceData.current.tpm) }}</div>
36
- <div class="metric-unit">Tokens/分钟</div>
37
- </div>
38
- <div v-if="hasAlert('high_tpm')" class="metric-alert">⚠️</div>
39
- </div>
40
-
41
- <div class="metric-card">
42
- <div class="metric-icon">🔄</div>
43
- <div class="metric-content">
44
- <div class="metric-label">RPM</div>
45
- <div class="metric-value">{{ formatNumber(performanceData.current.rpm) }}</div>
46
- <div class="metric-unit">Requests/分钟</div>
47
- </div>
48
- </div>
49
-
50
- <div class="metric-card highlight">
51
- <div class="metric-icon">📊</div>
52
- <div class="metric-content">
53
- <div class="metric-label">总 Token</div>
54
- <div class="metric-value">{{ formatNumber(performanceData.current.windowTotal.tokens) }}</div>
55
- <div class="metric-unit">{{ timeRangeLabel }}</div>
56
- </div>
57
- </div>
58
-
59
- <div class="metric-card highlight">
60
- <div class="metric-icon">🔢</div>
61
- <div class="metric-content">
62
- <div class="metric-label">总请求</div>
63
- <div class="metric-value">{{ formatNumber(performanceData.current.windowTotal.requests) }}</div>
64
- <div class="metric-unit">{{ timeRangeLabel }}</div>
65
- </div>
66
- </div>
67
- </div>
68
-
69
- <!-- Trend Charts -->
70
- <div class="charts-stack">
71
- <div class="chart-card">
72
- <div class="chart-header">
73
- <h3>TPM 趋势</h3>
74
- <span class="chart-datetime">{{ formatDateTime(chartDisplayTime) }}</span>
75
- </div>
76
- <div class="chart-container">
77
- <div class="chart-bars">
78
- <div
79
- v-for="(point, index) in chartData"
80
- :key="index"
81
- class="chart-bar clickable"
82
- :style="{ height: `${getBarHeight(point.tpm, maxValue('tpm'))}%` }"
83
- :title="`${formatTime(point.timestamp)}: ${formatNumber(point.tpm)} TPM - 点击查看详情`"
84
- @click="showBarDetail(point)"
85
- >
86
- <span class="bar-value">{{ formatNumber(point.tpm) }}</span>
87
- <span class="bar-time-label">{{ formatTime(point.timestamp) }}</span>
88
- </div>
89
- </div>
90
- </div>
91
- </div>
92
-
93
- <div class="chart-card">
94
- <div class="chart-header">
95
- <h3>RPM 趋势</h3>
96
- <span class="chart-datetime">{{ formatDateTime(chartDisplayTime) }}</span>
97
- </div>
98
- <div class="chart-container">
99
- <div class="chart-bars rpm">
100
- <div
101
- v-for="(point, index) in chartData"
102
- :key="index"
103
- class="chart-bar clickable"
104
- :style="{ height: `${getBarHeight(point.rpm, maxValue('rpm'))}%` }"
105
- :title="`${formatTime(point.timestamp)}: ${point.rpm} RPM - 点击查看调用详情`"
106
- @click="showBarDetail(point)"
107
- >
108
- <span class="bar-value">{{ point.rpm }}</span>
109
- <span class="bar-time-label">{{ formatTime(point.timestamp) }}</span>
110
- </div>
111
- </div>
112
- </div>
113
- </div>
114
- </div>
115
-
116
- <!-- Statistics Summary -->
117
- <div class="summary-section">
118
- <h3>统计摘要</h3>
119
- <div class="summary-grid">
120
- <div class="summary-item">
121
- <span class="summary-label">平均 TPM</span>
122
- <span class="summary-value">{{ formatNumber(performanceData.statistics.avgTpm) }}</span>
123
- </div>
124
- <div class="summary-item">
125
- <span class="summary-label">峰值 TPM</span>
126
- <span class="summary-value">{{ formatNumber(performanceData.statistics.peakTpm) }}</span>
127
- <span v-if="performanceData.statistics.peakTime" class="summary-sub">峰值时间: {{ performanceData.statistics.peakTime }}</span>
128
- </div>
129
- <div class="summary-item highlight">
130
- <span class="summary-label">时间窗口</span>
131
- <span class="summary-value">{{ timeRangeLabel }}</span>
132
- </div>
133
- </div>
134
- </div>
135
-
136
- <!-- 柱体详情弹窗 -->
137
- <div v-if="detailModalVisible" class="detail-modal-overlay" @click.self="detailModalVisible = false">
138
- <div class="detail-modal">
139
- <div class="detail-modal-header">
140
- <h3>{{ detailModalTitle }}</h3>
141
- <button class="close-btn" @click="detailModalVisible = false">×</button>
142
- </div>
143
- <div class="detail-modal-body">
144
- <div v-if="detailLoading" class="detail-loading">加载详情...</div>
145
- <div v-else-if="detailData?.calls?.length" class="detail-calls">
146
- <div class="detail-summary">
147
- {{ filteredCalls.length }} / {{ detailData.totalCalls }} 次调用 · {{ formatNumber(filteredTotalTokens) }} Tokens · 平均 {{ formatNumber(detailData.summary?.avgTokens || 0) }} Tokens/调用
148
- </div>
149
- <!-- 搜索和筛选 -->
150
- <div class="detail-filters">
151
- <input
152
- v-model="detailSearch"
153
- type="text"
154
- class="search-input"
155
- placeholder="搜索触发内容..."
156
- />
157
- <select v-model="detailAgentFilter" class="agent-filter">
158
- <option value="">全部 Agent</option>
159
- <option v-for="agent in detailAgents" :key="agent" :value="agent">{{ agent }}</option>
160
- </select>
161
- <select v-model="detailSort" class="sort-select">
162
- <option value="tokens_desc">Token 降序</option>
163
- <option value="tokens_asc">Token 升序</option>
164
- <option value="time_asc">时间 升序</option>
165
- <option value="time_desc">时间 降序</option>
166
- </select>
167
- </div>
168
- <div v-if="detailData.calls.some((c: { trigger?: string }) => c.trigger?.startsWith('【完成回传】'))" class="detail-call-hint">
169
- <span class="hint-badge">完成回传</span>
170
- <span class="hint-text">此时间戳为子任务完成后的回传时间,不是派发时间</span>
171
- </div>
172
- <div v-for="(call, i) in filteredCalls" :key="i" class="detail-call-item">
173
- <div class="call-header">
174
- <span class="call-agent">{{ call.agentId }}</span>
175
- <span class="call-time">{{ call.time }}</span>
176
- <span class="call-tokens">{{ formatNumber(call.tokens) }} tokens</span>
177
- </div>
178
- <div class="call-trigger" :title="call.trigger">
179
- <span v-if="call.trigger?.startsWith('【完成回传】')" class="call-trigger-badge">完成回传</span>
180
- {{ call.trigger?.replace(/^【完成回传】/, '') }}
181
- </div>
182
- <div v-if="call.model" class="call-meta">模型: {{ call.model }}</div>
183
- </div>
184
- </div>
185
- <div v-else class="detail-empty">该时段无调用记录</div>
186
- </div>
187
- </div>
188
- </div>
189
-
190
- <!-- Alerts Panel -->
191
- <div v-if="alerts.length > 0" class="alerts-panel">
192
- <h3>⚠️ 性能告警</h3>
193
- <div class="alerts-list">
194
- <div
195
- v-for="alert in alerts"
196
- :key="alert.id"
197
- class="alert-item"
198
- :class="alert.type"
199
- >
200
- <span class="alert-message">{{ alert.message }}</span>
201
- <span class="alert-value">{{ formatNumber(alert.value) }} (阈值: {{ formatNumber(alert.threshold) }})</span>
202
- <span class="alert-time">{{ formatTime(alert.timestamp) }}</span>
203
- <button class="ack-btn" @click="acknowledgeAlert(alert.id)">确认</button>
204
- </div>
205
- </div>
206
- </div>
207
- </template>
208
- </div>
209
- </template>
210
-
211
- <script setup lang="ts">
212
- import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
213
- import type { PerformanceData, TimeRange, PerformanceAlert, CallDetailsResponse } from '../../types/performance'
214
-
215
- const loading = ref(true)
216
- const error = ref<string | null>(null)
217
- const selectedRange = ref<TimeRange>('20m')
218
- const alerts = ref<PerformanceAlert[]>([])
219
-
220
- // 柱体详情弹窗
221
- const detailModalVisible = ref(false)
222
- const detailLoading = ref(false)
223
- const detailData = ref<CallDetailsResponse | null>(null)
224
- const detailModalTitle = ref('')
225
- const detailSearch = ref('')
226
- const detailAgentFilter = ref('')
227
- const detailSort = ref('tokens_desc')
228
-
229
- // 从详情数据中提取所有 Agent
230
- const detailAgents = computed(() => {
231
- if (!detailData.value?.calls) return []
232
- const agents = new Set(detailData.value.calls.map(c => c.agentId))
233
- return Array.from(agents).sort()
234
- })
235
-
236
- // 筛选后的调用列表
237
- const filteredCalls = computed(() => {
238
- if (!detailData.value?.calls) return []
239
-
240
- let calls = [...detailData.value.calls]
241
-
242
- // 搜索过滤
243
- if (detailSearch.value) {
244
- const search = detailSearch.value.toLowerCase()
245
- calls = calls.filter(c => c.trigger?.toLowerCase().includes(search))
246
- }
247
-
248
- // Agent 筛选
249
- if (detailAgentFilter.value) {
250
- calls = calls.filter(c => c.agentId === detailAgentFilter.value)
251
- }
252
-
253
- // 排序
254
- switch (detailSort.value) {
255
- case 'tokens_desc':
256
- calls.sort((a, b) => b.tokens - a.tokens)
257
- break
258
- case 'tokens_asc':
259
- calls.sort((a, b) => a.tokens - b.tokens)
260
- break
261
- case 'time_asc':
262
- calls.sort((a, b) => a.time.localeCompare(b.time))
263
- break
264
- case 'time_desc':
265
- calls.sort((a, b) => b.time.localeCompare(a.time))
266
- break
267
- }
268
-
269
- return calls
270
- })
271
-
272
- // 筛选后的总 Token
273
- const filteredTotalTokens = computed(() => {
274
- return filteredCalls.value.reduce((sum, c) => sum + c.tokens, 0)
275
- })
276
-
277
- // 默认性能数据
278
- const performanceData = ref<PerformanceData>({
279
- current: {
280
- tpm: 0,
281
- rpm: 0,
282
- windowTotal: {
283
- tokens: 0,
284
- requests: 0
285
- }
286
- },
287
- history: {
288
- tpm: [],
289
- rpm: [],
290
- timestamps: []
291
- },
292
- statistics: {
293
- avgTpm: 0,
294
- peakTpm: 0,
295
- peakTime: ''
296
- }
297
- })
298
-
299
- const timeRanges = [
300
- { value: '20m' as TimeRange, label: '20分钟' },
301
- { value: '1h' as TimeRange, label: '1小时' },
302
- { value: '24h' as TimeRange, label: '24小时' }
303
- ]
304
-
305
- // 时间范围标签
306
- const timeRangeLabel = computed(() => {
307
- const range = timeRanges.find(r => r.value === selectedRange.value)
308
- return range ? `最近 ${range.label}` : ''
309
- })
310
-
311
- // 根据时间范围获取数据点数
312
- const dataPoints = computed(() => {
313
- switch (selectedRange.value) {
314
- case '20m': return 20
315
- case '1h': return 60
316
- case '24h': return 24
317
- default: return 20
318
- }
319
- })
320
-
321
- // 图表数据
322
- const chartData = computed(() => {
323
- const history = performanceData.value.history
324
- const points = dataPoints.value
325
-
326
- // 如果历史数据不够,用数据填充
327
- if (history.tpm.length === 0) {
328
- const interval = selectedRange.value === '24h' ? 3600000 : 60000
329
- return Array.from({ length: points }, (_, i) => ({
330
- timestamp: Date.now() - (points - i - 1) * interval,
331
- tpm: 0,
332
- rpm: 0
333
- }))
334
- }
335
-
336
- // 直接使用后端返回的数据
337
- return history.tpm.map((tpm, i) => ({
338
- timestamp: history.timestamps[i] || Date.now() - i * 60000,
339
- tpm,
340
- rpm: history.rpm[i] || 0
341
- }))
342
- })
343
-
344
- function maxValue(metric: 'tpm' | 'rpm'): number {
345
- const values = chartData.value.map(p => p[metric])
346
- const max = Math.max(...values, 1)
347
- return max * 1.2 // 添加 20% 余量
348
- }
349
-
350
- function getBarHeight(value: number, max: number): number {
351
- return Math.max((value / max) * 100, 5)
352
- }
353
-
354
- function formatNumber(num: number): string {
355
- if (num >= 1000000) {
356
- return (num / 1000000).toFixed(1) + 'M'
357
- } else if (num >= 1000) {
358
- return (num / 1000).toFixed(1) + 'K'
359
- }
360
- return num.toString()
361
- }
362
-
363
- // 图表右上角时间
364
- const chartDisplayTime = ref(Date.now())
365
-
366
- function formatDateTime(timestamp: number | string | undefined): string {
367
- const ts = timestamp ?? Date.now()
368
- const num = typeof ts === 'number' ? ts : Number(ts)
369
- if (isNaN(num)) return new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })
370
- const ms = num < 1e12 ? num * 1000 : num
371
- const date = new Date(ms)
372
- if (isNaN(date.getTime())) return new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })
373
- return date.toLocaleString('zh-CN', {
374
- year: 'numeric',
375
- month: '2-digit',
376
- day: '2-digit',
377
- hour: '2-digit',
378
- minute: '2-digit',
379
- second: '2-digit',
380
- hour12: false
381
- })
382
- }
383
-
384
- function formatTime(timestamp: number | string | undefined): string {
385
- if (timestamp === undefined || timestamp === null) {
386
- return '--:--'
387
- }
388
- if (typeof timestamp === 'string') {
389
- const parts = timestamp.split(':').map(Number)
390
- if (parts.length >= 2) {
391
- const now = new Date()
392
- const utcDate = new Date(Date.UTC(
393
- now.getUTCFullYear(),
394
- now.getUTCMonth(),
395
- now.getUTCDate(),
396
- parts[0],
397
- parts[1],
398
- 0,
399
- 0
400
- ))
401
- return utcDate.toLocaleString('zh-CN', {
402
- hour: '2-digit',
403
- minute: '2-digit',
404
- hour12: false
405
- })
406
- }
407
- return timestamp
408
- }
409
- const ts = Number(timestamp)
410
- if (isNaN(ts)) return '--:--'
411
- const date = new Date(ts)
412
- if (isNaN(date.getTime())) return '--:--'
413
-
414
- // 24h 模式显示小时,其他显示分钟
415
- if (selectedRange.value === '24h') {
416
- return date.toLocaleString('zh-CN', {
417
- hour: '2-digit',
418
- hour12: false
419
- }) + ':00'
420
- }
421
- return date.toLocaleString('zh-CN', {
422
- hour: '2-digit',
423
- minute: '2-digit',
424
- hour12: false
425
- })
426
- }
427
-
428
- function hasAlert(type: string): boolean {
429
- return alerts.value.some(a => a.type === type && !a.acknowledged)
430
- }
431
-
432
- function acknowledgeAlert(id: string): void {
433
- const alert = alerts.value.find(a => a.id === id)
434
- if (alert) {
435
- alert.acknowledged = true
436
- }
437
- }
438
-
439
- async function fetchData(): Promise<void> {
440
- loading.value = true
441
- error.value = null
442
-
443
- try {
444
- const response = await fetch(`/api/performance?range=${selectedRange.value}`)
445
- if (!response.ok) throw new Error('Failed to fetch performance data')
446
-
447
- const data = await response.json()
448
- performanceData.value = {
449
- current: {
450
- tpm: data.current?.tpm ?? 0,
451
- rpm: data.current?.rpm ?? 0,
452
- windowTotal: {
453
- tokens: data.current?.windowTotal?.tokens ?? 0,
454
- requests: data.current?.windowTotal?.requests ?? 0
455
- }
456
- },
457
- history: {
458
- tpm: data.history?.tpm ?? [],
459
- rpm: data.history?.rpm ?? [],
460
- timestamps: data.history?.timestamps ?? []
461
- },
462
- statistics: {
463
- avgTpm: data.statistics?.avgTpm ?? 0,
464
- peakTpm: data.statistics?.peakTpm ?? 0,
465
- peakTime: data.statistics?.peakTime ?? ''
466
- }
467
- }
468
- chartDisplayTime.value = Date.now()
469
- checkAlerts()
470
- } catch (e) {
471
- error.value = (e as Error).message
472
- } finally {
473
- loading.value = false
474
- }
475
- }
476
-
477
- function checkAlerts(): void {
478
- const { current } = performanceData.value
479
-
480
- // TPM 告警阈值
481
- if (current.tpm > 100000) {
482
- alerts.value.push({
483
- id: `high_tpm_${Date.now()}`,
484
- type: 'high_tpm',
485
- message: 'TPM 过高',
486
- value: current.tpm,
487
- threshold: 100000,
488
- timestamp: Date.now(),
489
- acknowledged: false
490
- })
491
- }
492
-
493
- // 保留最近 10 条告警
494
- alerts.value = alerts.value.slice(-10)
495
- }
496
-
497
- function refreshData(): void {
498
- fetchData()
499
- }
500
-
501
- async function showBarDetail(point: { timestamp: number | string; tpm?: number; rpm?: number }): Promise<void> {
502
- let tsMs: number
503
- if (typeof point.timestamp === 'number') {
504
- tsMs = point.timestamp < 1e12 ? point.timestamp * 1000 : point.timestamp
505
- } else if (typeof point.timestamp === 'string') {
506
- const parts = point.timestamp.split(':').map(Number)
507
- if (parts.length >= 2) {
508
- const now = new Date()
509
- let utcDate = now.getUTCDate()
510
- const slotMins = parts[0] * 60 + parts[1]
511
- const nowMins = now.getUTCHours() * 60 + now.getUTCMinutes()
512
- if (slotMins > nowMins + 10) {
513
- utcDate -= 1
514
- }
515
- const d = new Date(Date.UTC(
516
- now.getUTCFullYear(),
517
- now.getUTCMonth(),
518
- utcDate,
519
- parts[0],
520
- parts[1],
521
- 0,
522
- 0
523
- ))
524
- tsMs = d.getTime()
525
- } else {
526
- return
527
- }
528
- } else {
529
- return
530
- }
531
-
532
- const granularity = selectedRange.value === '24h' ? 'hour' : 'minute'
533
-
534
- detailModalVisible.value = true
535
- detailModalTitle.value = `${formatTime(point.timestamp)} 调用详情`
536
- detailData.value = null
537
- detailLoading.value = true
538
- // 重置筛选器
539
- detailSearch.value = ''
540
- detailAgentFilter.value = ''
541
- detailSort.value = 'tokens_desc'
542
-
543
- try {
544
- const res = await fetch(`/api/performance/details?timestamp=${tsMs}&granularity=${granularity}`)
545
- const data = await res.json()
546
- if (!res.ok) {
547
- detailData.value = { timeWindow: formatTime(point.timestamp), calls: [], totalCalls: 0, totalTokens: 0, summary: { avgTokens: 0 } }
548
- } else {
549
- detailData.value = data
550
- detailModalTitle.value = `${data.timeWindow || formatTime(point.timestamp)} 调用详情`
551
- }
552
- } catch (e) {
553
- detailData.value = { timeWindow: formatTime(point.timestamp), calls: [], totalCalls: 0, totalTokens: 0, summary: { avgTokens: 0 } }
554
- } finally {
555
- detailLoading.value = false
556
- }
557
- }
558
-
559
- // 自动刷新定时器
560
- let refreshTimer: ReturnType<typeof setInterval> | null = null
561
-
562
- function startAutoRefresh(): void {
563
- stopAutoRefresh()
564
- // 根据时间范围设置刷新间隔
565
- const interval = selectedRange.value === '24h' ? 300000 : 30000 // 24h: 5分钟, 其他: 30秒
566
- refreshTimer = setInterval(fetchData, interval)
567
- }
568
-
569
- function stopAutoRefresh(): void {
570
- if (refreshTimer) {
571
- clearInterval(refreshTimer)
572
- refreshTimer = null
573
- }
574
- }
575
-
576
- onMounted(() => {
577
- fetchData()
578
- startAutoRefresh()
579
- })
580
-
581
- onUnmounted(() => {
582
- stopAutoRefresh()
583
- })
584
-
585
- // 时间范围变化时重新获取数据
586
- watch(selectedRange, () => {
587
- fetchData()
588
- startAutoRefresh()
589
- })
590
- </script>
591
-
592
- <style scoped>
593
- .performance-section {
594
- background: white;
595
- border-radius: 8px;
596
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
597
- padding: 1.5rem;
598
- }
599
-
600
- .section-header {
601
- display: flex;
602
- justify-content: space-between;
603
- align-items: center;
604
- margin-bottom: 1.5rem;
605
- flex-wrap: wrap;
606
- gap: 1rem;
607
- }
608
-
609
- .section-header h2 {
610
- margin: 0;
611
- font-size: 1.3rem;
612
- color: #333;
613
- }
614
-
615
- .time-range-selector {
616
- display: flex;
617
- gap: 0.5rem;
618
- }
619
-
620
- .range-btn {
621
- padding: 0.5rem 1rem;
622
- border: 1px solid #e5e7eb;
623
- border-radius: 6px;
624
- background: white;
625
- font-size: 0.85rem;
626
- cursor: pointer;
627
- transition: all 0.2s;
628
- }
629
-
630
- .range-btn:hover {
631
- border-color: #4a9eff;
632
- }
633
-
634
- .range-btn.active {
635
- background: #4a9eff;
636
- color: white;
637
- border-color: #4a9eff;
638
- }
639
-
640
- .loading-state,
641
- .error-state {
642
- display: flex;
643
- flex-direction: column;
644
- align-items: center;
645
- justify-content: center;
646
- height: 200px;
647
- gap: 1rem;
648
- color: #6b7280;
649
- }
650
-
651
- .spinner {
652
- width: 32px;
653
- height: 32px;
654
- border: 3px solid #e5e7eb;
655
- border-top-color: #4a9eff;
656
- border-radius: 50%;
657
- animation: spin 1s linear infinite;
658
- }
659
-
660
- @keyframes spin {
661
- to { transform: rotate(360deg); }
662
- }
663
-
664
- .retry-btn {
665
- padding: 0.5rem 1rem;
666
- background: #4a9eff;
667
- color: white;
668
- border: none;
669
- border-radius: 4px;
670
- cursor: pointer;
671
- }
672
-
673
- .metrics-grid {
674
- display: grid;
675
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
676
- gap: 1rem;
677
- margin-bottom: 2rem;
678
- }
679
-
680
- .metric-card {
681
- display: flex;
682
- align-items: center;
683
- gap: 1rem;
684
- padding: 1.25rem;
685
- background: #f9fafb;
686
- border-radius: 8px;
687
- border: 1px solid #e5e7eb;
688
- position: relative;
689
- }
690
-
691
- .metric-card.primary {
692
- background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
693
- border-color: #93c5fd;
694
- }
695
-
696
- .metric-card.highlight {
697
- background: #f0fdf4;
698
- border-color: #86efac;
699
- }
700
-
701
- .metric-icon {
702
- font-size: 2rem;
703
- }
704
-
705
- .metric-content {
706
- flex: 1;
707
- }
708
-
709
- .metric-label {
710
- font-size: 0.8rem;
711
- color: #6b7280;
712
- margin-bottom: 0.25rem;
713
- }
714
-
715
- .metric-value {
716
- font-size: 1.75rem;
717
- font-weight: 600;
718
- color: #333;
719
- }
720
-
721
- .metric-unit {
722
- font-size: 0.75rem;
723
- color: #94a3b8;
724
- }
725
-
726
- .metric-alert {
727
- position: absolute;
728
- top: 0.5rem;
729
- right: 0.5rem;
730
- font-size: 1.25rem;
731
- animation: pulse 2s infinite;
732
- }
733
-
734
- @keyframes pulse {
735
- 0%, 100% { opacity: 1; }
736
- 50% { opacity: 0.5; }
737
- }
738
-
739
- .charts-stack {
740
- display: flex;
741
- flex-direction: column;
742
- gap: 1.5rem;
743
- margin-bottom: 2rem;
744
- }
745
-
746
- .chart-card {
747
- background: white;
748
- border: 1px solid #e5e7eb;
749
- border-radius: 8px;
750
- padding: 1.25rem;
751
- }
752
-
753
- .chart-header {
754
- display: flex;
755
- justify-content: space-between;
756
- align-items: center;
757
- margin-bottom: 1rem;
758
- }
759
-
760
- .chart-header h3 {
761
- margin: 0;
762
- font-size: 1rem;
763
- color: #666;
764
- }
765
-
766
- .chart-datetime {
767
- font-size: 0.8rem;
768
- color: #94a3b8;
769
- }
770
-
771
- .chart-container {
772
- height: 200px;
773
- display: flex;
774
- flex-direction: column;
775
- }
776
-
777
- .chart-bars {
778
- flex: 1;
779
- display: flex;
780
- align-items: flex-end;
781
- gap: 4px;
782
- padding-bottom: 36px;
783
- padding-top: 28px;
784
- overflow-x: auto;
785
- min-width: 0;
786
- }
787
-
788
- .chart-bar {
789
- flex: 1;
790
- min-width: 16px;
791
- background: linear-gradient(to top, #4a9eff, #6bb9ff);
792
- border-radius: 3px 3px 0 0;
793
- min-height: 8px;
794
- position: relative;
795
- transition: height 0.3s ease;
796
- cursor: pointer;
797
- display: flex;
798
- flex-direction: column;
799
- align-items: center;
800
- }
801
-
802
- .chart-bars.rpm .chart-bar {
803
- background: linear-gradient(to top, #10b981, #34d399);
804
- }
805
-
806
- .bar-value {
807
- position: absolute;
808
- top: -22px;
809
- left: 50%;
810
- transform: translateX(-50%);
811
- font-size: 0.6rem;
812
- color: #6b7280;
813
- white-space: nowrap;
814
- }
815
-
816
- .bar-time-label {
817
- position: absolute;
818
- bottom: -30px;
819
- left: 50%;
820
- transform: translateX(-50%);
821
- font-size: 0.65rem;
822
- color: #6b7280;
823
- white-space: nowrap;
824
- }
825
-
826
- .summary-section {
827
- padding-top: 1.5rem;
828
- border-top: 1px solid #e5e7eb;
829
- margin-bottom: 1.5rem;
830
- }
831
-
832
- .summary-section h3 {
833
- margin: 0 0 1rem 0;
834
- font-size: 1rem;
835
- color: #666;
836
- }
837
-
838
- .summary-grid {
839
- display: grid;
840
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
841
- gap: 1rem;
842
- }
843
-
844
- .summary-item {
845
- display: flex;
846
- flex-direction: column;
847
- gap: 0.25rem;
848
- padding: 1rem;
849
- background: #f9fafb;
850
- border-radius: 6px;
851
- }
852
-
853
- .summary-item.highlight {
854
- background: #eff6ff;
855
- border: 1px solid #bfdbfe;
856
- }
857
-
858
- .summary-label {
859
- font-size: 0.8rem;
860
- color: #6b7280;
861
- }
862
-
863
- .summary-value {
864
- font-size: 1.25rem;
865
- font-weight: 600;
866
- color: #333;
867
- }
868
-
869
- .summary-sub {
870
- font-size: 0.75rem;
871
- color: #6b7280;
872
- }
873
-
874
- .chart-bar.clickable {
875
- cursor: pointer;
876
- }
877
- .chart-bar.clickable:hover {
878
- opacity: 0.9;
879
- }
880
-
881
- .detail-modal-overlay {
882
- position: fixed;
883
- inset: 0;
884
- background: rgba(0, 0, 0, 0.4);
885
- display: flex;
886
- align-items: center;
887
- justify-content: center;
888
- z-index: 1000;
889
- }
890
- .detail-modal {
891
- background: #fff;
892
- border-radius: 12px;
893
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
894
- max-width: 560px;
895
- width: 90%;
896
- max-height: 80vh;
897
- display: flex;
898
- flex-direction: column;
899
- }
900
- .detail-modal-header {
901
- display: flex;
902
- align-items: center;
903
- justify-content: space-between;
904
- padding: 1rem 1.25rem;
905
- border-bottom: 1px solid #e5e7eb;
906
- }
907
- .detail-modal-header h3 {
908
- margin: 0;
909
- font-size: 1rem;
910
- color: #333;
911
- }
912
- .detail-modal-header .close-btn {
913
- background: none;
914
- border: none;
915
- font-size: 1.5rem;
916
- cursor: pointer;
917
- color: #6b7280;
918
- line-height: 1;
919
- padding: 0 0.25rem;
920
- }
921
- .detail-modal-header .close-btn:hover {
922
- color: #333;
923
- }
924
- .detail-modal-body {
925
- padding: 1rem 1.25rem;
926
- overflow-y: auto;
927
- }
928
- .detail-loading {
929
- text-align: center;
930
- padding: 2rem;
931
- color: #6b7280;
932
- }
933
- .detail-summary {
934
- font-size: 0.9rem;
935
- color: #6b7280;
936
- margin-bottom: 1rem;
937
- }
938
-
939
- .detail-filters {
940
- display: flex;
941
- gap: 0.75rem;
942
- margin-bottom: 1rem;
943
- flex-wrap: wrap;
944
- }
945
-
946
- .search-input {
947
- flex: 1;
948
- min-width: 150px;
949
- padding: 0.5rem 0.75rem;
950
- border: 1px solid #e5e7eb;
951
- border-radius: 6px;
952
- font-size: 0.85rem;
953
- outline: none;
954
- }
955
-
956
- .search-input:focus {
957
- border-color: #4a9eff;
958
- }
959
-
960
- .agent-filter,
961
- .sort-select {
962
- padding: 0.5rem 0.75rem;
963
- border: 1px solid #e5e7eb;
964
- border-radius: 6px;
965
- font-size: 0.85rem;
966
- background: white;
967
- cursor: pointer;
968
- outline: none;
969
- }
970
-
971
- .agent-filter:focus,
972
- .sort-select:focus {
973
- border-color: #4a9eff;
974
- }
975
- .detail-calls {
976
- display: flex;
977
- flex-direction: column;
978
- gap: 0.75rem;
979
- }
980
- .detail-call-hint {
981
- display: flex;
982
- align-items: center;
983
- gap: 0.5rem;
984
- padding: 0.5rem 1rem;
985
- background: #eff6ff;
986
- border: 1px solid #93c5fd;
987
- border-radius: 6px;
988
- margin-bottom: 0.75rem;
989
- }
990
- .detail-call-hint .hint-badge {
991
- font-size: 0.8rem;
992
- font-weight: 600;
993
- color: #1d4ed8;
994
- padding: 0.15rem 0.5rem;
995
- background: #dbeafe;
996
- border-radius: 4px;
997
- }
998
- .detail-call-hint .hint-text {
999
- font-size: 0.8rem;
1000
- color: #1e40af;
1001
- }
1002
-
1003
- .detail-call-item {
1004
- padding: 0.75rem 1rem;
1005
- background: #f9fafb;
1006
- border-radius: 8px;
1007
- border-left: 3px solid #4a9eff;
1008
- }
1009
- .detail-call-item .call-header {
1010
- display: flex;
1011
- align-items: center;
1012
- gap: 0.5rem;
1013
- margin-bottom: 0.35rem;
1014
- }
1015
- .detail-call-item .call-agent {
1016
- font-weight: 600;
1017
- color: #333;
1018
- }
1019
- .detail-call-item .call-time {
1020
- font-size: 0.8rem;
1021
- color: #6b7280;
1022
- }
1023
- .detail-call-item .call-tokens {
1024
- font-size: 0.8rem;
1025
- color: #10b981;
1026
- margin-left: auto;
1027
- }
1028
- .detail-call-item .call-trigger {
1029
- font-size: 0.85rem;
1030
- color: #555;
1031
- word-break: break-word;
1032
- margin-top: 0.25rem;
1033
- white-space: pre-wrap;
1034
- }
1035
- .detail-call-item .call-trigger-badge {
1036
- display: inline-block;
1037
- font-size: 0.75rem;
1038
- font-weight: 600;
1039
- color: #1d4ed8;
1040
- padding: 0.1rem 0.4rem;
1041
- background: #dbeafe;
1042
- border-radius: 4px;
1043
- margin-right: 0.35rem;
1044
- }
1045
- .detail-call-item .call-meta {
1046
- font-size: 0.75rem;
1047
- color: #9ca3af;
1048
- margin-top: 0.25rem;
1049
- }
1050
- .detail-empty {
1051
- text-align: center;
1052
- padding: 2rem;
1053
- color: #9ca3af;
1054
- }
1055
-
1056
- .alerts-panel {
1057
- background: #fef3c7;
1058
- border: 1px solid #fcd34d;
1059
- border-radius: 8px;
1060
- padding: 1rem;
1061
- }
1062
-
1063
- .alerts-panel h3 {
1064
- margin: 0 0 0.75rem 0;
1065
- font-size: 0.95rem;
1066
- color: #92400e;
1067
- }
1068
-
1069
- .alerts-list {
1070
- display: flex;
1071
- flex-direction: column;
1072
- gap: 0.5rem;
1073
- }
1074
-
1075
- .alert-item {
1076
- display: flex;
1077
- align-items: center;
1078
- gap: 1rem;
1079
- padding: 0.75rem;
1080
- background: white;
1081
- border-radius: 6px;
1082
- font-size: 0.85rem;
1083
- }
1084
-
1085
- .alert-message {
1086
- flex: 1;
1087
- color: #333;
1088
- }
1089
-
1090
- .alert-value {
1091
- color: #6b7280;
1092
- }
1093
-
1094
- .alert-time {
1095
- font-size: 0.75rem;
1096
- color: #94a3b8;
1097
- }
1098
-
1099
- .ack-btn {
1100
- padding: 0.25rem 0.75rem;
1101
- background: #4a9eff;
1102
- color: white;
1103
- border: none;
1104
- border-radius: 4px;
1105
- font-size: 0.75rem;
1106
- cursor: pointer;
1107
- }
1108
-
1109
- .ack-btn:hover {
1110
- background: #3a8eef;
1111
- }
1112
-
1113
- /* 响应式 */
1114
- @media (max-width: 640px) {
1115
- .section-header {
1116
- flex-direction: column;
1117
- align-items: flex-start;
1118
- }
1119
-
1120
- .time-range-selector {
1121
- width: 100%;
1122
- justify-content: space-between;
1123
- }
1124
-
1125
- .range-btn {
1126
- flex: 1;
1127
- text-align: center;
1128
- padding: 0.5rem;
1129
- font-size: 0.75rem;
1130
- }
1131
-
1132
- .metrics-grid {
1133
- grid-template-columns: 1fr 1fr;
1134
- }
1135
-
1136
- }
1137
- </style>