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,973 +0,0 @@
1
- <template>
2
- <div class="task-status-section">
3
- <div class="section-header">
4
- <h2>任务状态</h2>
5
- <div class="summary-stats">
6
- <span
7
- class="stat running clickable"
8
- :class="{ active: activeFilters.includes('running') }"
9
- @click="quickFilter('running')"
10
- >执行中: {{ summary.running }}</span>
11
- <span
12
- class="stat completed clickable"
13
- :class="{ active: activeFilters.includes('completed') }"
14
- @click="quickFilter('completed')"
15
- >已完成: {{ summary.completed }}</span>
16
- <span
17
- class="stat failed clickable"
18
- :class="{ active: activeFilters.includes('failed') }"
19
- @click="quickFilter('failed')"
20
- >失败: {{ summary.failed }}</span>
21
- <span class="stat total">总计: {{ summary.total }}</span>
22
- </div>
23
- </div>
24
-
25
- <div class="filters-row">
26
- <div class="search-box">
27
- <input
28
- v-model="searchQuery"
29
- type="text"
30
- placeholder="搜索任务..."
31
- class="search-input"
32
- />
33
- </div>
34
- <div class="filter-buttons">
35
- <button
36
- v-for="status in statusFilters"
37
- :key="status.value"
38
- :class="['filter-btn', { active: activeFilters.includes(status.value) }]"
39
- @click="toggleFilter(status.value)"
40
- >
41
- {{ status.label }} ({{ getStatusCount(status.value) }})
42
- </button>
43
- </div>
44
- </div>
45
-
46
- <div v-if="loading" class="loading-state">
47
- <div class="spinner"></div>
48
- <span>加载任务数据...</span>
49
- </div>
50
-
51
- <div v-else-if="error" class="error-state">
52
- <span class="error-icon">⚠️</span>
53
- <span>{{ error }}</span>
54
- <button @click="refreshData" class="retry-btn">重试</button>
55
- </div>
56
-
57
- <div v-else-if="filteredTasks.length === 0" class="empty-state">
58
- <span class="empty-icon">📭</span>
59
- <span>{{ searchQuery ? '无匹配任务' : '暂无任务数据' }}</span>
60
- </div>
61
-
62
- <div v-else class="task-list-container" ref="containerRef">
63
- <div class="task-list">
64
- <div
65
- v-for="task in filteredTasks"
66
- :key="task.id"
67
- class="task-item"
68
- @click="selectedTask = task"
69
- >
70
- <span class="task-status-icon" :class="task.status">
71
- {{ getStatusIcon(task.status) }}
72
- </span>
73
- <div class="task-main">
74
- <div class="task-name-short">{{ getShortTaskName(task) }}</div>
75
- </div>
76
- <span class="task-agent" v-if="task.agentName">{{ task.agentName }}</span>
77
- <span class="task-time" v-if="task.startTime">{{ formatDuration(task) }}</span>
78
- <span class="task-detail-hint">详情 ›</span>
79
- </div>
80
- </div>
81
- </div>
82
-
83
- <!-- 任务详情弹窗 -->
84
- <div v-if="selectedTask" class="task-detail-overlay" @click.self="selectedTask = null">
85
- <div class="task-detail-modal">
86
- <div class="task-detail-header">
87
- <h3>任务详情</h3>
88
- <button class="close-btn" @click="selectedTask = null">×</button>
89
- </div>
90
- <div class="task-detail-body">
91
- <div class="detail-row">
92
- <span class="detail-label">任务</span>
93
- <span class="detail-value task-content">{{ sanitizeTaskDisplay(selectedTask.task ?? selectedTask.name) }}</span>
94
- </div>
95
- <div v-if="selectedTask.taskPath" class="detail-row">
96
- <span class="detail-label">项目路径</span>
97
- <span class="detail-value">{{ sanitizeTaskDisplay(selectedTask.taskPath) }}</span>
98
- </div>
99
- <div class="detail-row">
100
- <span class="detail-label">状态</span>
101
- <span class="detail-value">
102
- <span class="task-status-icon" :class="selectedTask.status">{{ getStatusIcon(selectedTask.status) }}</span>
103
- {{ getStatusLabel(selectedTask.status) }}
104
- </span>
105
- </div>
106
- <div v-if="selectedTask.agentName" class="detail-row">
107
- <span class="detail-label">执行者</span>
108
- <span class="detail-value">{{ selectedTask.agentName }}</span>
109
- </div>
110
- <div v-if="selectedTask.agentWorkspace" class="detail-row">
111
- <span class="detail-label">Agent 工作区路径</span>
112
- <span class="detail-value path-value">{{ selectedTask.agentWorkspace }}</span>
113
- </div>
114
- <div v-if="selectedTask.startTime" class="detail-row">
115
- <span class="detail-label">耗时</span>
116
- <span class="detail-value">{{ formatDuration(selectedTask) }}</span>
117
- </div>
118
- <div v-if="selectedTask.status === 'running'" class="detail-row">
119
- <span class="detail-label">进度</span>
120
- <div class="detail-progress">
121
- <div class="progress-bar">
122
- <div class="progress-fill" :style="{ width: `${selectedTask.progress}%` }"></div>
123
- </div>
124
- <span class="progress-text">{{ selectedTask.progress }}%</span>
125
- </div>
126
- </div>
127
- <div v-if="selectedTask.status === 'failed'" class="detail-row">
128
- <span class="detail-label">失败原因</span>
129
- <span class="detail-value error">{{ formatErrorDisplay(selectedTask.error) }}</span>
130
- </div>
131
- <div v-if="selectedTask.status === 'completed' && selectedTask.generatedFiles?.length" class="detail-row">
132
- <span class="detail-label">生成的文件</span>
133
- <ul class="generated-files-list">
134
- <li v-for="f in selectedTask.generatedFiles" :key="f" class="file-path-item">{{ f }}</li>
135
- </ul>
136
- </div>
137
- <div v-if="selectedTask.status === 'completed' && selectedTask.output" class="detail-row">
138
- <span class="detail-label">Agent 输出</span>
139
- <div class="detail-value output-content">{{ sanitizeTaskDisplay(selectedTask.output) }}</div>
140
- </div>
141
- <div v-if="selectedTask.subtasks?.length" class="detail-row">
142
- <span class="detail-label">子任务</span>
143
- <div class="detail-subtasks">
144
- <div v-for="st in selectedTask.subtasks" :key="st.id" class="subtask-row">
145
- <span class="task-status-icon" :class="st.status">{{ getStatusIcon(st.status) }}</span>
146
- <span>{{ st.name }}</span>
147
- <span v-if="st.status === 'failed'" class="subtask-error-inline">{{ formatErrorDisplay(st.error) }}</span>
148
- </div>
149
- </div>
150
- </div>
151
- <div v-if="taskTimeline.length > 0" class="detail-row">
152
- <span class="detail-label">执行时间线</span>
153
- <div class="timeline-container">
154
- <div class="timeline">
155
- <div v-for="(item, i) in taskTimeline" :key="i" class="timeline-item" :class="item.type">
156
- <span class="timeline-time">{{ formatTimelineTime(item.time) }}</span>
157
- <span class="timeline-dot"></span>
158
- <span class="timeline-desc">{{ item.description }}</span>
159
- </div>
160
- </div>
161
- </div>
162
- </div>
163
- </div>
164
- </div>
165
- </div>
166
- </div>
167
- </template>
168
-
169
- <script setup lang="ts">
170
- import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
171
- import { useRealtime, useDebounce } from '../../composables'
172
- import type { Task, TaskStatus as TaskStatusType } from '../../types'
173
-
174
- const { connectionState, subscribe } = useRealtime()
175
-
176
- const tasks = ref<Task[]>([])
177
- const loading = ref(true)
178
- const error = ref<string | null>(null)
179
- const searchQuery = ref('')
180
- const activeFilters = ref<TaskStatusType[]>([])
181
- const selectedTask = ref<Task | null>(null)
182
- const containerRef = ref<HTMLElement | null>(null)
183
- const taskTimeline = ref<Array<{time: number, type: string, description: string}>>([])
184
- const timelineLoading = ref(false)
185
-
186
- // 符合 PRD: 待分配/分配中/执行中/已完成/失败
187
- const statusFilters = [
188
- { value: 'running', label: '执行中' },
189
- { value: 'completed', label: '已完成' },
190
- { value: 'failed', label: '失败' },
191
- { value: 'pending', label: '待分配' }
192
- ]
193
-
194
- // 防抖搜索
195
- const { debouncedFn: debouncedSearch } = useDebounce((query: string) => {
196
- // 触发重新计算
197
- void query
198
- }, 300)
199
-
200
- // 过滤后的任务
201
- const filteredTasks = computed(() => {
202
- let result = tasks.value
203
-
204
- // 状态过滤
205
- if (activeFilters.value.length > 0) {
206
- result = result.filter(t => activeFilters.value.includes(t.status))
207
- }
208
-
209
- // 搜索过滤
210
- if (searchQuery.value) {
211
- const query = searchQuery.value.toLowerCase()
212
- result = result.filter(t =>
213
- t.name.toLowerCase().includes(query) ||
214
- t.agentName?.toLowerCase().includes(query)
215
- )
216
- }
217
-
218
- return result
219
- })
220
-
221
- // 统计摘要
222
- const summary = computed(() => ({
223
- total: tasks.value.length,
224
- running: tasks.value.filter(t => t.status === 'running').length,
225
- completed: tasks.value.filter(t => t.status === 'completed').length,
226
- failed: tasks.value.filter(t => t.status === 'failed').length,
227
- pending: tasks.value.filter(t => t.status === 'pending').length,
228
- cancelled: tasks.value.filter(t => t.status === 'cancelled').length
229
- }))
230
-
231
- // 符合 PRD 状态图标
232
- function getStatusIcon(status: TaskStatusType): string {
233
- const icons: Record<TaskStatusType, string> = {
234
- pending: '⏳',
235
- running: '🔄', // 执行中
236
- completed: '✅', // 已完成
237
- failed: '❌', // 失败
238
- cancelled: '🚫'
239
- }
240
- return icons[status] || '📋'
241
- }
242
-
243
- function getStatusCount(status: string): number {
244
- return tasks.value.filter(t => t.status === status).length
245
- }
246
-
247
- function formatErrorDisplay(raw: string | undefined): string {
248
- if (!raw || !raw.trim()) return '未知'
249
- const lower = raw.trim().toLowerCase()
250
- const mapping: Record<string, string> = {
251
- 'terminated': '任务被终止(可能是超时或被用户取消)',
252
- 'timeout': '任务执行超时',
253
- 'cancelled': '任务已取消',
254
- 'canceled': '任务已取消',
255
- 'killed': '任务被终止',
256
- 'subagent-error': '子任务执行异常',
257
- }
258
- for (const [key, desc] of Object.entries(mapping)) {
259
- if (lower.includes(key)) return desc
260
- }
261
- return raw.trim()
262
- }
263
-
264
- function sanitizeTaskDisplay(text: string | undefined): string {
265
- if (!text || typeof text !== 'string') return ''
266
- return text
267
- .replace(/\*\*/g, '')
268
- .replace(/`([^`]+)`/g, '$1')
269
- }
270
-
271
- function formatDuration(task: Task): string {
272
- if (!task.startTime) return ''
273
-
274
- const start = new Date(task.startTime).getTime()
275
- const end = task.endTime ? new Date(task.endTime).getTime() : Date.now()
276
- const duration = Math.floor((end - start) / 1000)
277
-
278
- if (duration < 60) return `${duration}s`
279
- if (duration < 3600) return `${Math.floor(duration / 60)}m ${duration % 60}s`
280
- return `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`
281
- }
282
-
283
- function toggleFilter(status: TaskStatusType): void {
284
- const index = activeFilters.value.indexOf(status)
285
- if (index === -1) {
286
- activeFilters.value.push(status)
287
- } else {
288
- activeFilters.value.splice(index, 1)
289
- }
290
- }
291
-
292
- function quickFilter(status: TaskStatusType): void {
293
- // 单击快速筛选:如果已选中则取消,否则只选中该状态
294
- if (activeFilters.value.length === 1 && activeFilters.value[0] === status) {
295
- activeFilters.value = []
296
- } else {
297
- activeFilters.value = [status]
298
- }
299
- }
300
-
301
- function getShortTaskName(task: Task): string {
302
- const full = sanitizeTaskDisplay(task.task ?? task.name)
303
- const firstLine = full.split('\n')[0].trim()
304
- if (firstLine.length <= 60) return firstLine
305
- return firstLine.slice(0, 60) + '…'
306
- }
307
-
308
- function getStatusLabel(status: string): string {
309
- const map: Record<string, string> = {
310
- pending: '待分配',
311
- running: '执行中',
312
- completed: '已完成',
313
- failed: '失败',
314
- cancelled: '已取消'
315
- }
316
- return map[status] || status
317
- }
318
-
319
- async function fetchData(): Promise<void> {
320
- loading.value = true
321
- error.value = null
322
-
323
- try {
324
- const response = await fetch('/api/tasks')
325
- if (!response.ok) throw new Error('Failed to fetch tasks')
326
-
327
- const data = await response.json()
328
- tasks.value = (data.tasks || []).map((t: any) => mapTaskFromApi(t))
329
- } catch (e) {
330
- error.value = (e as Error).message
331
- } finally {
332
- loading.value = false
333
- }
334
- }
335
-
336
- function mapTaskStatus(status: string): TaskStatusType {
337
- const statusMap: Record<string, TaskStatusType> = {
338
- 'pending': 'pending',
339
- 'assigning': 'pending', // 分配中归入 pending 显示
340
- 'running': 'running',
341
- 'in_progress': 'running',
342
- 'active': 'running',
343
- 'completed': 'completed',
344
- 'success': 'completed',
345
- 'failed': 'failed',
346
- 'error': 'failed',
347
- 'cancelled': 'cancelled'
348
- }
349
- return statusMap[status] || 'pending'
350
- }
351
-
352
- function refreshData(): void {
353
- fetchData()
354
- }
355
-
356
- function mapTaskFromApi(t: any): Task {
357
- const subtasks = (t.subtasks || []).map((s: any) => ({
358
- id: s.id || s.name,
359
- name: s.name || 'Unknown',
360
- task: s.task,
361
- status: mapTaskStatus(s.status),
362
- progress: s.progress ?? 0,
363
- startTime: s.startTime,
364
- endTime: s.endTime,
365
- agentId: s.agentId,
366
- agentName: s.agentName,
367
- agentWorkspace: s.agentWorkspace,
368
- taskPath: s.taskPath,
369
- error: s.error,
370
- output: s.output,
371
- generatedFiles: s.generatedFiles
372
- }))
373
- return {
374
- id: t.id,
375
- name: t.name || 'Unknown Task',
376
- task: t.task,
377
- status: mapTaskStatus(t.status),
378
- progress: t.progress ?? 0,
379
- startTime: t.startTime,
380
- endTime: t.endTime,
381
- agentId: t.agentId,
382
- agentName: t.agentName,
383
- agentWorkspace: t.agentWorkspace,
384
- taskPath: t.taskPath,
385
- error: t.error,
386
- output: t.output,
387
- generatedFiles: t.generatedFiles,
388
- subtasks: subtasks.length ? subtasks : undefined
389
- }
390
- }
391
-
392
- function handleTasksUpdate(data: unknown): void {
393
- // 兼容两种形态:WebSocket/轮询 可能传 { tasks: array },历史代码或其它路径可能传 array
394
- const rawList: any[] = Array.isArray(data)
395
- ? data
396
- : (data && typeof data === 'object' && 'tasks' in data)
397
- ? (data as { tasks?: any[] }).tasks ?? []
398
- : []
399
- const newTasks = rawList.map((t: any) => mapTaskFromApi(t))
400
-
401
- // 防止”概率性丢失”:后端/网络暂时返回空时,不覆盖已有列表(点 fetch 会重新拉取)
402
- if (newTasks.length === 0 && tasks.value.length > 0) {
403
- return
404
- }
405
-
406
- tasks.value = newTasks
407
- }
408
-
409
- function formatTimelineTime(timestamp: number | undefined): string {
410
- if (!timestamp) return ''
411
- const d = new Date(timestamp)
412
- return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
413
- }
414
-
415
- async function fetchTimeline(taskId: string): Promise<void> {
416
- if (!taskId) {
417
- taskTimeline.value = []
418
- return
419
- }
420
- timelineLoading.value = true
421
- try {
422
- const res = await fetch(`/api/tasks/${taskId}/timeline`)
423
- if (res.ok) {
424
- const data = await res.json()
425
- taskTimeline.value = data.timeline || []
426
- }
427
- } catch (e) {
428
- console.error('Failed to fetch timeline:', e)
429
- taskTimeline.value = []
430
- } finally {
431
- timelineLoading.value = false
432
- }
433
- }
434
-
435
- // 监听选中任务变化,加载时间线
436
- watch(selectedTask, (newTask) => {
437
- if (newTask) {
438
- fetchTimeline(newTask.id)
439
- } else {
440
- taskTimeline.value = []
441
- }
442
- })
443
-
444
- let unsubscribe: (() => void) | null = null
445
-
446
- onMounted(() => {
447
- fetchData()
448
- unsubscribe = subscribe('tasks', handleTasksUpdate)
449
- })
450
-
451
- onUnmounted(() => {
452
- if (unsubscribe) unsubscribe()
453
- })
454
- </script>
455
-
456
- <style scoped>
457
- .task-status-section {
458
- background: white;
459
- border-radius: 8px;
460
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
461
- padding: 1.5rem;
462
- }
463
-
464
- .section-header {
465
- display: flex;
466
- justify-content: space-between;
467
- align-items: center;
468
- margin-bottom: 1rem;
469
- flex-wrap: wrap;
470
- gap: 0.5rem;
471
- }
472
-
473
- .section-header h2 {
474
- margin: 0;
475
- font-size: 1.3rem;
476
- color: #333;
477
- }
478
-
479
- .summary-stats {
480
- display: flex;
481
- gap: 1rem;
482
- flex-wrap: wrap;
483
- }
484
-
485
- .stat {
486
- font-size: 0.85rem;
487
- padding: 0.25rem 0.75rem;
488
- border-radius: 4px;
489
- background: #f1f5f9;
490
- }
491
-
492
- /* 符合 PRD 颜色: 执行中=绿色, 已完成=金色, 失败=红色 */
493
- .stat.running {
494
- background: #dcfce7;
495
- color: #166534;
496
- }
497
-
498
- .stat.completed {
499
- background: #fef3c7;
500
- color: #92400e;
501
- }
502
-
503
- .stat.failed {
504
- background: #fee2e2;
505
- color: #991b1b;
506
- }
507
-
508
- .stat.clickable {
509
- cursor: pointer;
510
- transition: all 0.2s ease;
511
- }
512
-
513
- .stat.clickable:hover {
514
- transform: translateY(-1px);
515
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
516
- }
517
-
518
- .stat.clickable.active {
519
- font-weight: 600;
520
- box-shadow: 0 0 0 2px currentColor;
521
- }
522
-
523
- .filters-row {
524
- display: flex;
525
- gap: 1rem;
526
- margin-bottom: 1rem;
527
- flex-wrap: wrap;
528
- }
529
-
530
- .search-box {
531
- flex: 1;
532
- min-width: 200px;
533
- }
534
-
535
- .search-input {
536
- width: 100%;
537
- padding: 0.5rem 1rem;
538
- border: 1px solid #e5e7eb;
539
- border-radius: 6px;
540
- font-size: 0.9rem;
541
- outline: none;
542
- transition: border-color 0.2s;
543
- }
544
-
545
- .search-input:focus {
546
- border-color: #4a9eff;
547
- }
548
-
549
- .filter-buttons {
550
- display: flex;
551
- gap: 0.5rem;
552
- flex-wrap: wrap;
553
- }
554
-
555
- .filter-btn {
556
- padding: 0.5rem 1rem;
557
- border: 1px solid #e5e7eb;
558
- border-radius: 6px;
559
- background: white;
560
- font-size: 0.85rem;
561
- cursor: pointer;
562
- transition: all 0.2s;
563
- }
564
-
565
- .filter-btn:hover {
566
- border-color: #4a9eff;
567
- }
568
-
569
- .filter-btn.active {
570
- background: #4a9eff;
571
- color: white;
572
- border-color: #4a9eff;
573
- }
574
-
575
- .loading-state,
576
- .error-state,
577
- .empty-state {
578
- display: flex;
579
- flex-direction: column;
580
- align-items: center;
581
- justify-content: center;
582
- height: 200px;
583
- gap: 1rem;
584
- color: #6b7280;
585
- }
586
-
587
- .spinner {
588
- width: 32px;
589
- height: 32px;
590
- border: 3px solid #e5e7eb;
591
- border-top-color: #4a9eff;
592
- border-radius: 50%;
593
- animation: spin 1s linear infinite;
594
- }
595
-
596
- @keyframes spin {
597
- to { transform: rotate(360deg); }
598
- }
599
-
600
- .retry-btn {
601
- padding: 0.5rem 1rem;
602
- background: #4a9eff;
603
- color: white;
604
- border: none;
605
- border-radius: 4px;
606
- cursor: pointer;
607
- }
608
-
609
- .task-list-container {
610
- max-height: 600px;
611
- overflow-y: auto;
612
- border: 1px solid #e5e7eb;
613
- border-radius: 6px;
614
- position: relative;
615
- }
616
-
617
- .task-list {
618
- display: flex;
619
- flex-direction: column;
620
- }
621
-
622
- .task-item {
623
- display: flex;
624
- align-items: center;
625
- gap: 0.75rem;
626
- padding: 0.6rem 1rem;
627
- border-bottom: 1px solid #f1f5f9;
628
- background: white;
629
- cursor: pointer;
630
- transition: background 0.15s;
631
- }
632
-
633
- .task-item:hover {
634
- background: #f8fafc;
635
- }
636
-
637
- .task-item:last-child {
638
- border-bottom: none;
639
- }
640
-
641
- .task-status-icon {
642
- font-size: 1rem;
643
- }
644
-
645
- /* 符合 PRD 任务状态颜色 */
646
- .task-status-icon.running {
647
- color: #22c55e;
648
- }
649
-
650
- .task-status-icon.completed {
651
- color: #f59e0b;
652
- }
653
-
654
- .task-status-icon.failed {
655
- color: #ef4444;
656
- }
657
-
658
- .task-status-icon.pending {
659
- color: #9ca3af;
660
- }
661
-
662
- .task-main {
663
- flex: 1;
664
- min-width: 0;
665
- }
666
-
667
- .task-name-short {
668
- font-size: 0.9rem;
669
- font-weight: 500;
670
- color: #333;
671
- overflow: hidden;
672
- text-overflow: ellipsis;
673
- white-space: nowrap;
674
- }
675
-
676
- .task-detail-hint {
677
- font-size: 0.75rem;
678
- color: #94a3b8;
679
- flex-shrink: 0;
680
- }
681
-
682
- .task-agent {
683
- font-size: 0.8rem;
684
- color: #6b7280;
685
- background: #f1f5f9;
686
- padding: 0.125rem 0.5rem;
687
- border-radius: 4px;
688
- }
689
-
690
- .task-time {
691
- font-size: 0.8rem;
692
- color: #94a3b8;
693
- }
694
-
695
- /* 详情弹窗 */
696
- .task-detail-overlay {
697
- position: fixed;
698
- inset: 0;
699
- background: rgba(0, 0, 0, 0.4);
700
- display: flex;
701
- align-items: center;
702
- justify-content: center;
703
- z-index: 1000;
704
- }
705
-
706
- .task-detail-modal {
707
- background: white;
708
- border-radius: 12px;
709
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
710
- max-width: 560px;
711
- width: 90%;
712
- max-height: 85vh;
713
- display: flex;
714
- flex-direction: column;
715
- }
716
-
717
- .task-detail-header {
718
- display: flex;
719
- align-items: center;
720
- justify-content: space-between;
721
- padding: 1rem 1.25rem;
722
- border-bottom: 1px solid #e5e7eb;
723
- }
724
-
725
- .task-detail-header h3 {
726
- margin: 0;
727
- font-size: 1rem;
728
- color: #333;
729
- }
730
-
731
- .task-detail-header .close-btn {
732
- background: none;
733
- border: none;
734
- font-size: 1.5rem;
735
- cursor: pointer;
736
- color: #6b7280;
737
- line-height: 1;
738
- padding: 0 0.25rem;
739
- }
740
-
741
- .task-detail-header .close-btn:hover {
742
- color: #333;
743
- }
744
-
745
- .task-detail-body {
746
- padding: 1rem 1.25rem;
747
- overflow-y: auto;
748
- }
749
-
750
- .detail-row {
751
- margin-bottom: 1rem;
752
- }
753
-
754
- .detail-row:last-child {
755
- margin-bottom: 0;
756
- }
757
-
758
- .detail-label {
759
- display: block;
760
- font-size: 0.75rem;
761
- color: #64748b;
762
- margin-bottom: 0.25rem;
763
- }
764
-
765
- .detail-value {
766
- font-size: 0.9rem;
767
- color: #333;
768
- word-break: break-word;
769
- }
770
-
771
- .detail-value.task-content {
772
- white-space: pre-wrap;
773
- line-height: 1.5;
774
- }
775
-
776
- .detail-value.path-value {
777
- font-family: ui-monospace, monospace;
778
- font-size: 0.85rem;
779
- color: #475569;
780
- }
781
-
782
- .generated-files-list {
783
- margin: 0;
784
- padding-left: 1.25rem;
785
- font-family: ui-monospace, monospace;
786
- font-size: 0.85rem;
787
- color: #475569;
788
- line-height: 1.6;
789
- }
790
-
791
- .file-path-item {
792
- word-break: break-all;
793
- margin-bottom: 0.25rem;
794
- }
795
-
796
- .detail-value.output-content {
797
- white-space: pre-wrap;
798
- line-height: 1.6;
799
- max-height: 400px;
800
- overflow-y: auto;
801
- background: #f8fafc;
802
- padding: 0.75rem 1rem;
803
- border-radius: 6px;
804
- border: 1px solid #e2e8f0;
805
- font-size: 0.85rem;
806
- }
807
-
808
- .detail-value.error {
809
- color: #b91c1c;
810
- background: #fef2f2;
811
- padding: 0.5rem;
812
- border-radius: 6px;
813
- display: block;
814
- }
815
-
816
- .detail-progress {
817
- display: flex;
818
- align-items: center;
819
- gap: 0.75rem;
820
- }
821
-
822
- .detail-progress .progress-bar {
823
- flex: 1;
824
- height: 6px;
825
- background: #e5e7eb;
826
- border-radius: 3px;
827
- overflow: hidden;
828
- }
829
-
830
- .detail-progress .progress-fill {
831
- height: 100%;
832
- background: linear-gradient(90deg, #4a9eff, #6bb9ff);
833
- border-radius: 3px;
834
- transition: width 0.3s ease;
835
- }
836
-
837
- .detail-progress .progress-text {
838
- font-size: 0.8rem;
839
- color: #6b7280;
840
- min-width: 40px;
841
- }
842
-
843
- .detail-subtasks {
844
- display: flex;
845
- flex-direction: column;
846
- gap: 0.5rem;
847
- }
848
-
849
- .subtask-row {
850
- display: flex;
851
- align-items: center;
852
- gap: 0.5rem;
853
- font-size: 0.85rem;
854
- padding: 0.35rem 0.5rem;
855
- background: #f8fafc;
856
- border-radius: 6px;
857
- }
858
-
859
- .subtask-error-inline {
860
- margin-left: auto;
861
- font-size: 0.8rem;
862
- color: #b91c1c;
863
- }
864
-
865
- /* 时间线样式 */
866
- .timeline-container {
867
- flex: 1;
868
- max-height: 300px;
869
- overflow-y: auto;
870
- }
871
-
872
- .timeline {
873
- position: relative;
874
- padding-left: 20px;
875
- }
876
-
877
- .timeline::before {
878
- content: '';
879
- position: absolute;
880
- left: 6px;
881
- top: 0;
882
- bottom: 0;
883
- width: 2px;
884
- background: #e5e7eb;
885
- }
886
-
887
- .timeline-item {
888
- position: relative;
889
- display: flex;
890
- align-items: flex-start;
891
- gap: 12px;
892
- padding: 8px 0;
893
- font-size: 0.85rem;
894
- }
895
-
896
- .timeline-dot {
897
- position: absolute;
898
- left: -17px;
899
- top: 12px;
900
- width: 10px;
901
- height: 10px;
902
- border-radius: 50%;
903
- background: #9ca3af;
904
- border: 2px solid #fff;
905
- box-shadow: 0 0 0 2px #e5e7eb;
906
- }
907
-
908
- .timeline-item.created .timeline-dot {
909
- background: #3b82f6;
910
- box-shadow: 0 0 0 2px #3b82f6;
911
- }
912
-
913
- .timeline-item.completed .timeline-dot {
914
- background: #10b981;
915
- box-shadow: 0 0 0 2px #10b981;
916
- }
917
-
918
- .timeline-item.failed .timeline-dot {
919
- background: #ef4444;
920
- box-shadow: 0 0 0 2px #ef4444;
921
- }
922
-
923
- .timeline-item.tool .timeline-dot {
924
- background: #f59e0b;
925
- }
926
-
927
- .timeline-item.start .timeline-dot {
928
- background: #6366f1;
929
- }
930
-
931
- .timeline-time {
932
- flex-shrink: 0;
933
- width: 70px;
934
- font-size: 0.75rem;
935
- color: #6b7280;
936
- font-family: monospace;
937
- }
938
-
939
- .timeline-desc {
940
- flex: 1;
941
- color: #374151;
942
- }
943
-
944
- .timeline-item.failed .timeline-desc {
945
- color: #991b1b;
946
- }
947
-
948
-
949
- /* 响应式 */
950
- @media (max-width: 640px) {
951
- .section-header {
952
- flex-direction: column;
953
- align-items: flex-start;
954
- }
955
-
956
- .summary-stats {
957
- font-size: 0.75rem;
958
- }
959
-
960
- .filters-row {
961
- flex-direction: column;
962
- }
963
-
964
- .filter-buttons {
965
- width: 100%;
966
- justify-content: flex-start;
967
- }
968
-
969
- .task-agent {
970
- display: none;
971
- }
972
- }
973
- </style>