openclaw-agent-dashboard 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/README.md +302 -0
  3. package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +132 -0
  4. package/docs/RELEASE-LATEST.md +189 -0
  5. package/docs/RELEASE-MODEL-CONFIG.md +95 -0
  6. package/docs/release-guide.md +259 -0
  7. package/docs/release-operations-manual.md +167 -0
  8. package/docs/specs/tr3-install-system.md +580 -0
  9. package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
  10. package/frontend/index.html +12 -0
  11. package/frontend/package-lock.json +1240 -0
  12. package/frontend/package.json +19 -0
  13. package/frontend/src/App.vue +331 -0
  14. package/frontend/src/components/AgentCard.vue +796 -0
  15. package/frontend/src/components/AgentConfigPanel.vue +539 -0
  16. package/frontend/src/components/AgentDetailPanel.vue +738 -0
  17. package/frontend/src/components/ErrorAnalysisView.vue +546 -0
  18. package/frontend/src/components/ErrorCenterPanel.vue +844 -0
  19. package/frontend/src/components/PerformanceMonitor.vue +515 -0
  20. package/frontend/src/components/SettingsPanel.vue +236 -0
  21. package/frontend/src/components/TokenAnalysisPanel.vue +683 -0
  22. package/frontend/src/components/chain/ChainEdge.vue +85 -0
  23. package/frontend/src/components/chain/ChainNode.vue +166 -0
  24. package/frontend/src/components/chain/TaskChainView.vue +425 -0
  25. package/frontend/src/components/chain/index.ts +3 -0
  26. package/frontend/src/components/chain/types.ts +70 -0
  27. package/frontend/src/components/collaboration/CollaborationFlowSection.vue +1032 -0
  28. package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +113 -0
  29. package/frontend/src/components/performance/PerformancePanel.vue +119 -0
  30. package/frontend/src/components/performance/PerformanceSection.vue +1137 -0
  31. package/frontend/src/components/tasks/TaskStatusSection.vue +973 -0
  32. package/frontend/src/components/timeline/TimelineConnector.vue +31 -0
  33. package/frontend/src/components/timeline/TimelineRound.vue +135 -0
  34. package/frontend/src/components/timeline/TimelineStep.vue +691 -0
  35. package/frontend/src/components/timeline/TimelineToolLink.vue +109 -0
  36. package/frontend/src/components/timeline/TimelineView.vue +540 -0
  37. package/frontend/src/components/timeline/index.ts +5 -0
  38. package/frontend/src/components/timeline/types.ts +120 -0
  39. package/frontend/src/composables/index.ts +7 -0
  40. package/frontend/src/composables/useDebounce.ts +48 -0
  41. package/frontend/src/composables/useRealtime.ts +52 -0
  42. package/frontend/src/composables/useState.ts +52 -0
  43. package/frontend/src/composables/useThrottle.ts +46 -0
  44. package/frontend/src/composables/useVirtualScroll.ts +106 -0
  45. package/frontend/src/main.ts +4 -0
  46. package/frontend/src/managers/EventDispatcher.ts +127 -0
  47. package/frontend/src/managers/RealtimeDataManager.ts +293 -0
  48. package/frontend/src/managers/StateManager.ts +128 -0
  49. package/frontend/src/managers/index.ts +5 -0
  50. package/frontend/src/types/collaboration.ts +135 -0
  51. package/frontend/src/types/index.ts +20 -0
  52. package/frontend/src/types/performance.ts +105 -0
  53. package/frontend/src/types/task.ts +38 -0
  54. package/frontend/vite.config.ts +18 -0
  55. package/package.json +22 -0
  56. package/plugin/README.md +99 -0
  57. package/plugin/config.json.example +1 -0
  58. package/plugin/index.js +250 -0
  59. package/plugin/openclaw.plugin.json +17 -0
  60. package/plugin/package.json +21 -0
  61. package/scripts/build-plugin.js +67 -0
  62. package/scripts/bundle.sh +62 -0
  63. package/scripts/install-plugin.sh +162 -0
  64. package/scripts/install-python-deps.js +346 -0
  65. package/scripts/install-python-deps.sh +226 -0
  66. package/scripts/install.js +512 -0
  67. package/scripts/install.sh +367 -0
  68. package/scripts/lib/common.js +490 -0
  69. package/scripts/lib/common.sh +137 -0
  70. package/scripts/release-pack.sh +110 -0
  71. package/scripts/start.js +50 -0
  72. package/scripts/test_available_models.py +284 -0
  73. package/scripts/test_websocket_ping.py +44 -0
  74. package/src/backend/agents.py +73 -0
  75. package/src/backend/api/__init__.py +1 -0
  76. package/src/backend/api/agent_config_api.py +90 -0
  77. package/src/backend/api/agents.py +73 -0
  78. package/src/backend/api/agents_config.py +75 -0
  79. package/src/backend/api/chains.py +126 -0
  80. package/src/backend/api/collaboration.py +902 -0
  81. package/src/backend/api/debug_paths.py +39 -0
  82. package/src/backend/api/error_analysis.py +146 -0
  83. package/src/backend/api/errors.py +281 -0
  84. package/src/backend/api/performance.py +784 -0
  85. package/src/backend/api/subagents.py +770 -0
  86. package/src/backend/api/timeline.py +144 -0
  87. package/src/backend/api/websocket.py +251 -0
  88. package/src/backend/collaboration.py +405 -0
  89. package/src/backend/data/__init__.py +1 -0
  90. package/src/backend/data/agent_config_manager.py +270 -0
  91. package/src/backend/data/chain_reader.py +299 -0
  92. package/src/backend/data/config_reader.py +153 -0
  93. package/src/backend/data/error_analyzer.py +430 -0
  94. package/src/backend/data/session_reader.py +445 -0
  95. package/src/backend/data/subagent_reader.py +244 -0
  96. package/src/backend/data/task_history.py +118 -0
  97. package/src/backend/data/timeline_reader.py +981 -0
  98. package/src/backend/errors.py +63 -0
  99. package/src/backend/main.py +89 -0
  100. package/src/backend/mechanism_reader.py +131 -0
  101. package/src/backend/mechanisms.py +32 -0
  102. package/src/backend/performance.py +474 -0
  103. package/src/backend/requirements.txt +5 -0
  104. package/src/backend/session_reader.py +238 -0
  105. package/src/backend/status/__init__.py +1 -0
  106. package/src/backend/status/error_detector.py +122 -0
  107. package/src/backend/status/status_calculator.py +301 -0
  108. package/src/backend/status_calculator.py +121 -0
  109. package/src/backend/subagent_reader.py +229 -0
  110. package/src/backend/watchers/__init__.py +4 -0
  111. package/src/backend/watchers/file_watcher.py +159 -0
@@ -0,0 +1,120 @@
1
+ // Timeline 类型定义
2
+
3
+ /** 步骤类型 */
4
+ export type StepType =
5
+ | 'user' // 用户消息
6
+ | 'thinking' // Agent 思考
7
+ | 'text' // Agent 文本响应
8
+ | 'toolCall' // 工具调用
9
+ | 'toolResult' // 工具结果
10
+ | 'error' // 错误
11
+
12
+ /** 步骤状态 */
13
+ export type StepStatus = 'pending' | 'running' | 'success' | 'error'
14
+
15
+ /** Token 使用 */
16
+ export interface TokenUsage {
17
+ input: number
18
+ output: number
19
+ cumulative?: number
20
+ }
21
+
22
+ /** 时序步骤 */
23
+ export interface TimelineStep {
24
+ id: string
25
+ type: StepType
26
+ status: StepStatus
27
+ timestamp: number
28
+ duration?: number
29
+
30
+ // 内容
31
+ content?: string
32
+ thinking?: string
33
+
34
+ // 工具调用
35
+ toolName?: string
36
+ toolCallId?: string
37
+ toolArguments?: Record<string, unknown>
38
+ toolResult?: string
39
+ toolResultStatus?: 'ok' | 'error'
40
+ toolResultError?: string // 工具失败时的错误信息
41
+
42
+ // 工具链路关联
43
+ pairedToolCallId?: string // toolResult 专用:对应的 toolCall ID
44
+ pairedToolResultId?: string // toolCall 专用:对应的 toolResult ID
45
+ executionTime?: number // 工具执行耗时(ms),toolResult 专用
46
+
47
+ // 错误
48
+ errorMessage?: string
49
+ errorType?: string
50
+
51
+ // 统计
52
+ tokens?: TokenUsage
53
+
54
+ // 展示控制
55
+ collapsed?: boolean
56
+
57
+ // 消息来源(用于区分真实用户和其他 Agent)
58
+ senderId?: string // 发送者 Agent ID(如 'main')
59
+ senderName?: string // 发送者显示名(如 '老K')
60
+ }
61
+
62
+ /** 时序统计 */
63
+ export interface TimelineStats {
64
+ totalDuration: number
65
+ totalInputTokens: number
66
+ totalOutputTokens: number
67
+ toolCallCount: number
68
+ stepCount: number
69
+ }
70
+
71
+ /** LLM 轮次 */
72
+ export interface LLMRound {
73
+ id: string // round_1, round_2, ...
74
+ index: number // 轮次序号(从1开始)
75
+ trigger: 'user_input' | 'tool_result' | 'subagent_result' | 'start'
76
+ triggerBy?: string // 触发来源描述
77
+ stepIds: string[] // 该轮次包含的步骤 ID 列表
78
+ duration: number // 该轮次耗时(ms)
79
+ tokens?: TokenUsage // 该轮次的 token 使用
80
+ }
81
+
82
+ /** 时序会话响应 */
83
+ export interface TimelineResponse {
84
+ sessionId: string | null
85
+ agentId: string
86
+ agentName?: string
87
+ model?: string
88
+ startedAt: number | null
89
+ status: 'running' | 'completed' | 'error' | 'empty' | 'no_sessions'
90
+ steps: TimelineStep[]
91
+ stats: TimelineStats
92
+ message?: string
93
+ // LLM 轮次分组
94
+ rounds?: LLMRound[]
95
+ roundMode?: boolean
96
+ }
97
+
98
+ /** 步骤图标和颜色配置 */
99
+ export const stepConfig: Record<StepType, { icon: string; bgColor: string; borderColor: string; label: string }> = {
100
+ user: { icon: '👤', bgColor: '#f0f9ff', borderColor: '#3b82f6', label: '用户' },
101
+ thinking: { icon: '🧠', bgColor: '#fef3c7', borderColor: '#f59e0b', label: '思考' },
102
+ text: { icon: '🤖', bgColor: '#f0fdf4', borderColor: '#22c55e', label: '回复' },
103
+ toolCall: { icon: '🔧', bgColor: '#f5f3ff', borderColor: '#8b5cf6', label: '调用' },
104
+ toolResult: { icon: '✅', bgColor: '#ecfdf5', borderColor: '#10b981', label: '结果' },
105
+ error: { icon: '⚠️', bgColor: '#fef2f2', borderColor: '#dc2626', label: '错误' }
106
+ }
107
+
108
+ /**
109
+ * 获取用户步骤的显示标签
110
+ * 如果有 senderName,显示发送者名称;否则显示"用户"
111
+ */
112
+ export function getUserStepLabel(step: TimelineStep): string {
113
+ if (step.senderName) {
114
+ return step.senderName
115
+ }
116
+ if (step.senderId) {
117
+ return step.senderId
118
+ }
119
+ return '用户'
120
+ }
@@ -0,0 +1,7 @@
1
+ // 组合函数入口文件
2
+
3
+ export { useDebounce } from './useDebounce'
4
+ export { useThrottle } from './useThrottle'
5
+ export { useRealtime } from './useRealtime'
6
+ export { useState, useCache } from './useState'
7
+ export { useVirtualScroll } from './useVirtualScroll'
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 防抖组合函数
3
+ */
4
+
5
+ import { ref, onUnmounted } from 'vue'
6
+
7
+ export function useDebounce<T extends (...args: unknown[]) => unknown>(
8
+ fn: T,
9
+ delay: number = 300
10
+ ): { debouncedFn: T; cancel: () => void; flush: () => void } {
11
+ const timer = ref<ReturnType<typeof setTimeout> | null>(null)
12
+ let lastArgs: unknown[] | null = null
13
+
14
+ const debouncedFn = ((...args: unknown[]) => {
15
+ lastArgs = args
16
+ if (timer.value) {
17
+ clearTimeout(timer.value)
18
+ }
19
+ timer.value = setTimeout(() => {
20
+ fn(...args)
21
+ timer.value = null
22
+ lastArgs = null
23
+ }, delay)
24
+ }) as T
25
+
26
+ const cancel = () => {
27
+ if (timer.value) {
28
+ clearTimeout(timer.value)
29
+ timer.value = null
30
+ lastArgs = null
31
+ }
32
+ }
33
+
34
+ const flush = () => {
35
+ if (timer.value && lastArgs) {
36
+ clearTimeout(timer.value)
37
+ fn(...lastArgs)
38
+ timer.value = null
39
+ lastArgs = null
40
+ }
41
+ }
42
+
43
+ onUnmounted(() => {
44
+ cancel()
45
+ })
46
+
47
+ return { debouncedFn, cancel, flush }
48
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 实时数据组合函数
3
+ */
4
+
5
+ import { ref, onMounted, onUnmounted } from 'vue'
6
+ import { getRealtimeManager } from '../managers'
7
+ import type { ConnectionState } from '../types'
8
+
9
+ export function useRealtime() {
10
+ const manager = getRealtimeManager()
11
+ const connectionState = ref<ConnectionState>(manager.getConnectionState())
12
+ const isConnected = ref(false)
13
+
14
+ let unsubscribeState: (() => void) | null = null
15
+
16
+ const subscribe = <T>(event: string, callback: (data: T) => void) => {
17
+ return manager.subscribe(event, callback as (data: unknown) => void)
18
+ }
19
+
20
+ const connect = () => {
21
+ manager.connect()
22
+ }
23
+
24
+ const disconnect = () => {
25
+ manager.disconnect()
26
+ }
27
+
28
+ onMounted(() => {
29
+ unsubscribeState = manager.onStateChange((state) => {
30
+ connectionState.value = state
31
+ isConnected.value = state.status === 'connected'
32
+ })
33
+
34
+ if (!manager.isConnected()) {
35
+ manager.connect()
36
+ }
37
+ })
38
+
39
+ onUnmounted(() => {
40
+ if (unsubscribeState) {
41
+ unsubscribeState()
42
+ }
43
+ })
44
+
45
+ return {
46
+ connectionState,
47
+ isConnected,
48
+ subscribe,
49
+ connect,
50
+ disconnect
51
+ }
52
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 状态管理组合函数
3
+ */
4
+
5
+ import { computed, type Ref } from 'vue'
6
+ import { getStateManager } from '../managers'
7
+
8
+ export function useState<T>(key: string, defaultValue: T): Ref<T> {
9
+ const manager = getStateManager()
10
+ return manager.useStore(key, defaultValue)
11
+ }
12
+
13
+ export function useCache<T>(key: string, fetcher: () => Promise<T>, ttl?: number) {
14
+ const manager = getStateManager()
15
+ const data = useState<T | null>(`cache:${key}`, null)
16
+ const loading = useState(`cache:${key}:loading`, false)
17
+ const error = useState<Error | null>(`cache:${key}:error`, null)
18
+
19
+ const fetch = async () => {
20
+ // 检查缓存
21
+ if (manager.hasValidCache(key)) {
22
+ data.value = manager.getCache<T>(key) ?? null
23
+ return
24
+ }
25
+
26
+ loading.value = true
27
+ error.value = null
28
+
29
+ try {
30
+ const result = await fetcher()
31
+ data.value = result
32
+ manager.setCache(key, result, ttl)
33
+ } catch (e) {
34
+ error.value = e as Error
35
+ } finally {
36
+ loading.value = false
37
+ }
38
+ }
39
+
40
+ const invalidate = () => {
41
+ manager.invalidateCache(key)
42
+ data.value = null
43
+ }
44
+
45
+ return {
46
+ data: computed(() => data.value),
47
+ loading: computed(() => loading.value),
48
+ error: computed(() => error.value),
49
+ fetch,
50
+ invalidate
51
+ }
52
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 节流组合函数
3
+ */
4
+
5
+ import { ref, onUnmounted } from 'vue'
6
+
7
+ export function useThrottle<T extends (...args: unknown[]) => unknown>(
8
+ fn: T,
9
+ interval: number = 100
10
+ ): { throttledFn: T; cancel: () => void } {
11
+ const lastExec = ref(0)
12
+ const timer = ref<ReturnType<typeof setTimeout> | null>(null)
13
+
14
+ const throttledFn = ((...args: unknown[]) => {
15
+ const now = Date.now()
16
+ const timeSinceLastExec = now - lastExec.value
17
+
18
+ if (timeSinceLastExec >= interval) {
19
+ lastExec.value = now
20
+ fn(...args)
21
+ } else {
22
+ // 确保最后一次调用会被执行
23
+ if (timer.value) {
24
+ clearTimeout(timer.value)
25
+ }
26
+ timer.value = setTimeout(() => {
27
+ lastExec.value = Date.now()
28
+ fn(...args)
29
+ timer.value = null
30
+ }, interval - timeSinceLastExec)
31
+ }
32
+ }) as T
33
+
34
+ const cancel = () => {
35
+ if (timer.value) {
36
+ clearTimeout(timer.value)
37
+ timer.value = null
38
+ }
39
+ }
40
+
41
+ onUnmounted(() => {
42
+ cancel()
43
+ })
44
+
45
+ return { throttledFn, cancel }
46
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * 虚拟滚动组合函数
3
+ */
4
+
5
+ import { ref, computed, onMounted, onUnmounted, type Ref } from 'vue'
6
+
7
+ interface VirtualScrollOptions {
8
+ itemHeight: number
9
+ bufferSize?: number
10
+ }
11
+
12
+ export function useVirtualScroll(
13
+ containerRef: Ref<HTMLElement | null>,
14
+ totalCount: Ref<number>,
15
+ options: VirtualScrollOptions
16
+ ) {
17
+ const { itemHeight, bufferSize = 5 } = options
18
+
19
+ const scrollTop = ref(0)
20
+ const containerHeight = ref(0)
21
+
22
+ const visibleCount = computed(() => {
23
+ return Math.ceil(containerHeight.value / itemHeight) + bufferSize * 2
24
+ })
25
+
26
+ const startIndex = computed(() => {
27
+ const rawStart = Math.floor(scrollTop.value / itemHeight) - bufferSize
28
+ return Math.max(0, rawStart)
29
+ })
30
+
31
+ const endIndex = computed(() => {
32
+ return Math.min(totalCount.value, startIndex.value + visibleCount.value)
33
+ })
34
+
35
+ const offsetY = computed(() => {
36
+ return startIndex.value * itemHeight
37
+ })
38
+
39
+ const totalHeight = computed(() => {
40
+ return totalCount.value * itemHeight
41
+ })
42
+
43
+ const visibleItems = computed(() => {
44
+ const items: { index: number; style: { transform: string } }[] = []
45
+ for (let i = startIndex.value; i < endIndex.value; i++) {
46
+ items.push({
47
+ index: i,
48
+ style: {
49
+ transform: `translateY(${i * itemHeight}px)`
50
+ }
51
+ })
52
+ }
53
+ return items
54
+ })
55
+
56
+ const scrollToIndex = (index: number) => {
57
+ if (containerRef.value) {
58
+ containerRef.value.scrollTop = index * itemHeight
59
+ }
60
+ }
61
+
62
+ const handleScroll = () => {
63
+ if (containerRef.value) {
64
+ scrollTop.value = containerRef.value.scrollTop
65
+ }
66
+ }
67
+
68
+ const updateContainerHeight = () => {
69
+ if (containerRef.value) {
70
+ containerHeight.value = containerRef.value.clientHeight
71
+ }
72
+ }
73
+
74
+ let resizeObserver: ResizeObserver | null = null
75
+
76
+ onMounted(() => {
77
+ if (containerRef.value) {
78
+ containerRef.value.addEventListener('scroll', handleScroll, { passive: true })
79
+
80
+ resizeObserver = new ResizeObserver(() => {
81
+ updateContainerHeight()
82
+ })
83
+ resizeObserver.observe(containerRef.value)
84
+ updateContainerHeight()
85
+ }
86
+ })
87
+
88
+ onUnmounted(() => {
89
+ if (containerRef.value) {
90
+ containerRef.value.removeEventListener('scroll', handleScroll)
91
+ }
92
+ if (resizeObserver) {
93
+ resizeObserver.disconnect()
94
+ }
95
+ })
96
+
97
+ return {
98
+ startIndex,
99
+ endIndex,
100
+ offsetY,
101
+ totalHeight,
102
+ visibleItems,
103
+ scrollToIndex,
104
+ updateContainerHeight
105
+ }
106
+ }
@@ -0,0 +1,4 @@
1
+ import { createApp } from 'vue'
2
+ import App from './App.vue'
3
+
4
+ createApp(App).mount('#app')
@@ -0,0 +1,127 @@
1
+ /**
2
+ * 事件分发器
3
+ * 负责组件间事件通信
4
+ */
5
+
6
+ type EventHandler = (payload?: unknown) => void
7
+
8
+ interface QueuedEvent {
9
+ event: string
10
+ payload?: unknown
11
+ }
12
+
13
+ export class EventDispatcher {
14
+ private listeners: Map<string, Set<EventHandler>> = new Map()
15
+ private eventQueue: QueuedEvent[] = []
16
+ private isFlushing = false
17
+ private maxQueueSize = 100
18
+
19
+ /**
20
+ * 分发事件
21
+ */
22
+ emit(event: string, payload?: unknown): void {
23
+ const handlers = this.listeners.get(event)
24
+ if (handlers) {
25
+ handlers.forEach(handler => {
26
+ try {
27
+ handler(payload)
28
+ } catch (e) {
29
+ console.error(`Error in event handler for ${event}:`, e)
30
+ }
31
+ })
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 监听事件
37
+ * 返回取消监听函数
38
+ */
39
+ on(event: string, handler: EventHandler): () => void {
40
+ if (!this.listeners.has(event)) {
41
+ this.listeners.set(event, new Set())
42
+ }
43
+ this.listeners.get(event)!.add(handler)
44
+
45
+ return () => {
46
+ this.listeners.get(event)?.delete(handler)
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 监听一次事件
52
+ */
53
+ once(event: string, handler: EventHandler): void {
54
+ const wrapper: EventHandler = (payload) => {
55
+ this.off(event, wrapper)
56
+ handler(payload)
57
+ }
58
+ this.on(event, wrapper)
59
+ }
60
+
61
+ /**
62
+ * 取消监听
63
+ */
64
+ off(event: string, handler: EventHandler): void {
65
+ this.listeners.get(event)?.delete(handler)
66
+ }
67
+
68
+ /**
69
+ * 将事件加入队列
70
+ */
71
+ enqueue(event: string, payload?: unknown): void {
72
+ if (this.eventQueue.length >= this.maxQueueSize) {
73
+ console.warn('Event queue is full, dropping oldest event')
74
+ this.eventQueue.shift()
75
+ }
76
+ this.eventQueue.push({ event, payload })
77
+ }
78
+
79
+ /**
80
+ * 刷新队列中的所有事件
81
+ */
82
+ flush(): void {
83
+ if (this.isFlushing) return
84
+
85
+ this.isFlushing = true
86
+ try {
87
+ while (this.eventQueue.length > 0) {
88
+ const { event, payload } = this.eventQueue.shift()!
89
+ this.emit(event, payload)
90
+ }
91
+ } finally {
92
+ this.isFlushing = false
93
+ }
94
+ }
95
+
96
+ /**
97
+ * 获取队列长度
98
+ */
99
+ getQueueLength(): number {
100
+ return this.eventQueue.length
101
+ }
102
+
103
+ /**
104
+ * 清空队列
105
+ */
106
+ clearQueue(): void {
107
+ this.eventQueue = []
108
+ }
109
+
110
+ /**
111
+ * 清除所有监听器
112
+ */
113
+ clearAll(): void {
114
+ this.listeners.clear()
115
+ this.eventQueue = []
116
+ }
117
+ }
118
+
119
+ // 单例实例
120
+ let instance: EventDispatcher | null = null
121
+
122
+ export function getEventDispatcher(): EventDispatcher {
123
+ if (!instance) {
124
+ instance = new EventDispatcher()
125
+ }
126
+ return instance
127
+ }