agentgui 1.0.820 → 1.0.822
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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/static/index.html +4 -0
- package/static/js/conv-list-renderer.js +197 -0
- package/static/js/conv-sidebar-actions.js +184 -0
- package/static/js/conv-sidebar-clone.js +91 -0
- package/static/js/conversations.js +116 -736
- package/static/js/ui-components.js +88 -187
- package/static/js/websocket-manager.js +107 -642
- package/static/js/ws-core.js +162 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
class WebSocketManager {
|
|
2
|
+
constructor(config = {}) {
|
|
3
|
+
this.config = {
|
|
4
|
+
url: config.url || this.getWebSocketURL(),
|
|
5
|
+
reconnectDelays: config.reconnectDelays || [500, 1000, 2000, 4000, 8000, 15000, 30000],
|
|
6
|
+
maxReconnectDelay: config.maxReconnectDelay || 30000,
|
|
7
|
+
heartbeatInterval: config.heartbeatInterval || 15000,
|
|
8
|
+
messageTimeout: config.messageTimeout || 60000,
|
|
9
|
+
maxBufferedMessages: config.maxBufferedMessages || 1000,
|
|
10
|
+
pongTimeout: config.pongTimeout || 5000,
|
|
11
|
+
latencyWindowSize: config.latencyWindowSize || 10,
|
|
12
|
+
...config
|
|
13
|
+
};
|
|
14
|
+
this.ws = null;
|
|
15
|
+
this._isConnected = false;
|
|
16
|
+
this._isConnecting = false;
|
|
17
|
+
this._isManuallyDisconnected = false;
|
|
18
|
+
this._reconnectCount = 0;
|
|
19
|
+
this.isManuallyDisconnected = false;
|
|
20
|
+
this.reconnectCount = 0;
|
|
21
|
+
this.reconnectTimer = null;
|
|
22
|
+
this.messageBuffer = [];
|
|
23
|
+
this.requestMap = new Map();
|
|
24
|
+
this.heartbeatTimer = null;
|
|
25
|
+
this._connectionState = 'disconnected';
|
|
26
|
+
this.activeSubscriptions = new Set();
|
|
27
|
+
this._wsActor = typeof createWsActor === 'function' ? createWsActor() : null;
|
|
28
|
+
this.connectionEstablishedAt = 0;
|
|
29
|
+
this.cachedVoiceList = null;
|
|
30
|
+
this.voiceListListeners = [];
|
|
31
|
+
this.latency = { samples: [], current: 0, avg: 0, jitter: 0, quality: 'unknown', predicted: 0, predictedNext: 0, trend: 'stable', missedPongs: 0, pingCounter: 0 };
|
|
32
|
+
this._latencyEma = null;
|
|
33
|
+
this._trendHistory = [];
|
|
34
|
+
this._trendCount = 0;
|
|
35
|
+
this._reconnectedAt = 0;
|
|
36
|
+
this.stats = { totalConnections: 0, totalReconnects: 0, totalMessagesSent: 0, totalMessagesReceived: 0, totalErrors: 0, totalTimeouts: 0, avgLatency: 0, lastConnectedTime: null, connectionDuration: 0 };
|
|
37
|
+
this.lastSeqBySession = {};
|
|
38
|
+
this.listeners = {};
|
|
39
|
+
this._onVisibilityChange = this._handleVisibilityChange.bind(this);
|
|
40
|
+
this._onOnline = this._handleOnline.bind(this);
|
|
41
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', this._onVisibilityChange);
|
|
42
|
+
if (typeof window !== 'undefined') window.addEventListener('online', this._onOnline);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get isConnected() { return this._wsActor ? this._wsActor.getSnapshot().value === 'connected' : this._isConnected; }
|
|
46
|
+
set isConnected(v) { this._isConnected = v; }
|
|
47
|
+
get isConnecting() { return this._wsActor ? this._wsActor.getSnapshot().value === 'connecting' : this._isConnecting; }
|
|
48
|
+
set isConnecting(v) { this._isConnecting = v; }
|
|
49
|
+
get connectionState() { return this._wsActor ? this._wsActor.getSnapshot().value : this._connectionState; }
|
|
50
|
+
set connectionState(v) { this._connectionState = v; }
|
|
51
|
+
get isManuallyDisconnected() { return this._wsActor ? !!this._wsActor.getSnapshot().context.manualDisconnect : this._isManuallyDisconnected; }
|
|
52
|
+
set isManuallyDisconnected(v) { this._isManuallyDisconnected = v; if (this._wsActor && v) this._wsActor.send({ type: 'MANUAL_DISCONNECT' }); }
|
|
53
|
+
get reconnectCount() { return this._wsActor ? this._wsActor.getSnapshot().context.reconnectCount : this._reconnectCount; }
|
|
54
|
+
set reconnectCount(v) { this._reconnectCount = v; }
|
|
55
|
+
|
|
56
|
+
getWebSocketURL() {
|
|
57
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
58
|
+
const baseURL = window.__BASE_URL || '/gm';
|
|
59
|
+
let url = `${protocol}//${window.location.host}${baseURL}/sync`;
|
|
60
|
+
if (window.__WS_TOKEN) url += `?token=${encodeURIComponent(window.__WS_TOKEN)}`;
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async connect() {
|
|
65
|
+
if (this.isConnected || this.isConnecting) return this.ws;
|
|
66
|
+
this.isManuallyDisconnected = false;
|
|
67
|
+
this._isConnecting = true;
|
|
68
|
+
if (this._wsActor) this._wsActor.send({ type: 'CONNECT' });
|
|
69
|
+
this.setConnectionState('connecting');
|
|
70
|
+
try {
|
|
71
|
+
this.ws = new WebSocket(this.config.url);
|
|
72
|
+
this.ws.binaryType = 'arraybuffer';
|
|
73
|
+
this.ws.onopen = () => this.onOpen();
|
|
74
|
+
this.ws.onmessage = (event) => this.onMessage(event);
|
|
75
|
+
this.ws.onerror = (error) => this.onError(error);
|
|
76
|
+
this.ws.onclose = () => this.onClose();
|
|
77
|
+
return await this.waitForConnection(this.config.messageTimeout);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.isConnecting = false;
|
|
80
|
+
this.stats.totalErrors++;
|
|
81
|
+
this.scheduleReconnect();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
waitForConnection(timeout = 5000) {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const timer = setTimeout(() => reject(new Error('WebSocket connection timeout')), timeout);
|
|
89
|
+
const check = () => {
|
|
90
|
+
if (this.isConnected || this.ws?.readyState === WebSocket.OPEN) { clearTimeout(timer); resolve(this.ws); }
|
|
91
|
+
else setTimeout(check, 50);
|
|
92
|
+
};
|
|
93
|
+
check();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
onOpen() {
|
|
98
|
+
this._isConnected = true;
|
|
99
|
+
this._isConnecting = false;
|
|
100
|
+
if (this._wsActor) this._wsActor.send({ type: 'OPEN' });
|
|
101
|
+
this.connectionEstablishedAt = Date.now();
|
|
102
|
+
this._reconnectedAt = this.stats.totalConnections > 0 ? Date.now() : 0;
|
|
103
|
+
this.stats.totalConnections++;
|
|
104
|
+
this.stats.lastConnectedTime = Date.now();
|
|
105
|
+
this.latency.missedPongs = 0;
|
|
106
|
+
this.setConnectionState('connected');
|
|
107
|
+
this.flushMessageBuffer();
|
|
108
|
+
this.resubscribeAll();
|
|
109
|
+
this.startHeartbeat();
|
|
110
|
+
this.emit('connected', { timestamp: Date.now() });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async onMessage(event) {
|
|
114
|
+
try {
|
|
115
|
+
const buf = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
|
|
116
|
+
const parsed = window._codec ? window._codec.decode(buf) : msgpackr.unpack(new Uint8Array(buf));
|
|
117
|
+
const messages = Array.isArray(parsed) ? parsed : [parsed];
|
|
118
|
+
this.stats.totalMessagesReceived += messages.length;
|
|
119
|
+
for (const data of messages) {
|
|
120
|
+
if (data.type === 'pong') { this._handlePong(data); continue; }
|
|
121
|
+
if (data.type === 'voice_list') {
|
|
122
|
+
this.cachedVoiceList = data.voices || [];
|
|
123
|
+
for (const listener of this.voiceListListeners) { try { listener(this.cachedVoiceList); } catch (_) {} }
|
|
124
|
+
}
|
|
125
|
+
if (data.seq !== undefined && data.sessionId) {
|
|
126
|
+
this.lastSeqBySession[data.sessionId] = Math.max(this.lastSeqBySession[data.sessionId] || -1, data.seq);
|
|
127
|
+
}
|
|
128
|
+
this.emit('message', data);
|
|
129
|
+
if (data.type) this.emit('message:' + data.type, data);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) { this.stats.totalErrors++; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onError(error) {
|
|
135
|
+
this.stats.totalErrors++;
|
|
136
|
+
if (this._wsActor) this._wsActor.send({ type: 'ERROR' });
|
|
137
|
+
this.emit('error', { error, timestamp: Date.now() });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onClose() {
|
|
141
|
+
this._isConnected = false;
|
|
142
|
+
this._isConnecting = false;
|
|
143
|
+
if (this._wsActor) this._wsActor.send({ type: 'CLOSE' });
|
|
144
|
+
this.setConnectionState('disconnected');
|
|
145
|
+
this.stopHeartbeat();
|
|
146
|
+
if (this.stats.lastConnectedTime) this.stats.connectionDuration = Date.now() - this.stats.lastConnectedTime;
|
|
147
|
+
this.emit('disconnected', { timestamp: Date.now() });
|
|
148
|
+
if (!this.isManuallyDisconnected) this.scheduleReconnect();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
scheduleReconnect() {
|
|
152
|
+
if (this.isManuallyDisconnected || this.reconnectTimer) return;
|
|
153
|
+
const delays = this.config.reconnectDelays;
|
|
154
|
+
const baseDelay = this.reconnectCount < delays.length ? delays[this.reconnectCount] : this.config.maxReconnectDelay;
|
|
155
|
+
const delay = Math.round(baseDelay + Math.random() * 0.3 * baseDelay);
|
|
156
|
+
this.reconnectCount++;
|
|
157
|
+
this.stats.totalReconnects++;
|
|
158
|
+
this.setConnectionState('reconnecting');
|
|
159
|
+
this.emit('reconnecting', { delay, attempt: this.reconnectCount, nextAttemptAt: Date.now() + delay });
|
|
160
|
+
this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.connect().catch(() => {}); }, delay);
|
|
161
|
+
}
|
|
162
|
+
}
|