agentgui 1.0.930 → 1.0.932
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/AGENTS.md +35 -12
- package/database.js +31 -2
- package/lib/http-handler.js +11 -25
- package/lib/routes-registry.js +4 -48
- package/lib/server-startup.js +3 -11
- package/lib/ws-setup.js +2 -1
- package/package.json +3 -3
- package/server.js +7 -1
- package/site/app/js/app.js +64 -68
- package/site/app/js/backend.js +1 -1
- package/static/lib/xstate.umd.min.js +1 -1
- package/lib/db-queries-chunks.js +0 -195
- package/lib/db-queries-chunks2.js +0 -82
- package/lib/db-queries-cleanup.js +0 -74
- package/lib/db-queries-del.js +0 -141
- package/lib/db-queries-events.js +0 -68
- package/lib/db-queries-import.js +0 -133
- package/lib/db-queries-messages.js +0 -102
- package/lib/db-queries-sessions.js +0 -112
- package/lib/db-queries-streams.js +0 -100
- package/lib/db-queries.js +0 -89
- package/lib/jsonl-parser.js +0 -190
- package/lib/jsonl-watcher.js +0 -64
- package/lib/routes-agent-actions.js +0 -61
- package/lib/routes-auth-config.js +0 -30
- package/lib/routes-conversations.js +0 -96
- package/lib/routes-debug.js +0 -119
- package/lib/routes-messages.js +0 -139
- package/lib/routes-runs.js +0 -156
- package/lib/routes-scripts.js +0 -135
- package/lib/routes-sessions.js +0 -144
- package/lib/routes-threads.js +0 -100
- package/lib/routes-util.js +0 -110
- package/lib/ws-handlers-conv.js +0 -138
- package/lib/ws-handlers-conv2.js +0 -169
- package/lib/ws-handlers-msg.js +0 -121
- package/lib/ws-handlers-queue.js +0 -56
- package/lib/ws-handlers-run.js +0 -182
- package/lib/ws-handlers-scripts.js +0 -66
- package/lib/ws-handlers-session.js +0 -105
- package/lib/ws-handlers-session2.js +0 -85
- package/lib/ws-legacy-handlers.js +0 -51
- package/static/app.js +0 -261
- package/static/css/app-shell.css +0 -419
- package/static/css/brand-bible.css +0 -591
- package/static/css/colors_and_type.css +0 -568
- package/static/css/gmail-skin.css +0 -663
- package/static/css/main.css +0 -4015
- package/static/css/tools-popup.css +0 -472
- package/static/index.html +0 -418
- package/static/js/agent-auth.js +0 -146
- package/static/js/app-shortcuts.js +0 -30
- package/static/js/audio-recorder-processor.js +0 -18
- package/static/js/client-agents.js +0 -155
- package/static/js/client-cache.js +0 -171
- package/static/js/client-conv.js +0 -198
- package/static/js/client-events.js +0 -164
- package/static/js/client-exec.js +0 -160
- package/static/js/client-helpers.js +0 -199
- package/static/js/client-load.js +0 -175
- package/static/js/client-render.js +0 -132
- package/static/js/client-scroll.js +0 -178
- package/static/js/client-status.js +0 -167
- package/static/js/client-streaming.js +0 -117
- package/static/js/client-streaming2.js +0 -116
- package/static/js/client-streaming3.js +0 -153
- package/static/js/client-streaming4.js +0 -194
- package/static/js/client-ui-controls.js +0 -170
- package/static/js/client-ui.js +0 -128
- package/static/js/client-ui2.js +0 -160
- package/static/js/client-url.js +0 -93
- package/static/js/client-utils.js +0 -174
- package/static/js/client-ws-msg.js +0 -88
- package/static/js/client-ws.js +0 -161
- package/static/js/client.js +0 -145
- package/static/js/codec.js +0 -4
- package/static/js/conv-list-machine.js +0 -145
- package/static/js/conv-list-renderer.js +0 -198
- package/static/js/conv-machine.js +0 -110
- package/static/js/conv-sidebar-actions.js +0 -188
- package/static/js/conv-sidebar-clone.js +0 -91
- package/static/js/conversations.js +0 -116
- package/static/js/dialogs-types.js +0 -111
- package/static/js/dialogs.js +0 -53
- package/static/js/event-filter-config.js +0 -36
- package/static/js/event-processor.js +0 -181
- package/static/js/features.js +0 -187
- package/static/js/image-loader-element.js +0 -76
- package/static/js/image-loader.js +0 -146
- package/static/js/prompt-machine.js +0 -108
- package/static/js/recording-machine.js +0 -49
- package/static/js/script-runner.js +0 -192
- package/static/js/state-barrier.js +0 -105
- package/static/js/streaming-renderer-dispatch.js +0 -144
- package/static/js/streaming-renderer-events.js +0 -163
- package/static/js/streaming-renderer-events2.js +0 -125
- package/static/js/streaming-renderer-params.js +0 -38
- package/static/js/streaming-renderer-render-misc.js +0 -107
- package/static/js/streaming-renderer-render.js +0 -181
- package/static/js/streaming-renderer-render2.js +0 -149
- package/static/js/streaming-renderer-render3.js +0 -142
- package/static/js/streaming-renderer-static.js +0 -181
- package/static/js/streaming-renderer-static2.js +0 -140
- package/static/js/streaming-renderer-stream.js +0 -170
- package/static/js/streaming-renderer-text.js +0 -185
- package/static/js/streaming-renderer-tools.js +0 -189
- package/static/js/streaming-renderer-tools2.js +0 -92
- package/static/js/streaming-renderer.js +0 -200
- package/static/js/syntax-highlighter-render.js +0 -72
- package/static/js/syntax-highlighter.js +0 -131
- package/static/js/terminal-machine.js +0 -51
- package/static/js/terminal.js +0 -178
- package/static/js/ui-components-rendering.js +0 -62
- package/static/js/ui-components.js +0 -88
- package/static/js/websocket-manager.js +0 -107
- package/static/js/ws-client.js +0 -87
- package/static/js/ws-core.js +0 -162
- package/static/js/ws-latency.js +0 -88
- package/static/js/ws-machine.js +0 -68
- package/static/lib/msgpackr.min.js +0 -2
- package/static/theme.js +0 -74
- package/static/vendor/highlight-js.css +0 -10
- package/static/vendor/highlight.min.js +0 -1244
- package/static/vendor/prism-dark.css +0 -129
- package/static/vendor/rippleui.css +0 -35
- package/static/vendor/xterm-addon-fit.min.js +0 -8
- package/static/vendor/xterm.css +0 -8
- package/static/vendor/xterm.min.js +0 -8
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
class UIComponents {
|
|
2
|
-
static createModal(config = {}) {
|
|
3
|
-
const { title = 'Dialog', content = '', buttons = [], onClose = null, size = 'medium' } = config;
|
|
4
|
-
const modal = document.createElement('div');
|
|
5
|
-
modal.className = 'modal-overlay';
|
|
6
|
-
modal.dataset.modal = 'true';
|
|
7
|
-
const sizeClasses = { 'small': 'max-w-sm', 'medium': 'max-w-md', 'large': 'max-w-2xl' };
|
|
8
|
-
modal.innerHTML = `<div class="modal-content card ${sizeClasses[size] || sizeClasses['medium']}"><div class="card-header flex justify-between items-center"><h2 class="text-xl font-bold">${UIComponents.escapeHtml(title)}</h2><button class="btn btn-ghost btn-sm modal-close">×</button></div><div class="card-body modal-body">${typeof content === 'string' ? UIComponents.escapeHtml(content) : ''}</div><div class="card-footer flex gap-2 justify-end">${buttons.map(btn => `<button class="btn btn-${btn.variant || 'secondary'}" data-action="${btn.action || 'close'}">${UIComponents.escapeHtml(btn.label)}</button>`).join('')}</div></div>`;
|
|
9
|
-
const closeBtn = modal.querySelector('.modal-close');
|
|
10
|
-
closeBtn.addEventListener('click', () => { modal.remove(); if (onClose) onClose(); });
|
|
11
|
-
modal.querySelectorAll('[data-action]').forEach(btn => {
|
|
12
|
-
btn.addEventListener('click', (e) => { const action = e.target.dataset.action; if (action === 'close') { modal.remove(); if (onClose) onClose(); } });
|
|
13
|
-
});
|
|
14
|
-
modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); if (onClose) onClose(); } });
|
|
15
|
-
return modal;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
static createTabs(config = {}) {
|
|
19
|
-
const { tabs = [], activeTab = 0, onChange = null } = config;
|
|
20
|
-
const container = document.createElement('div');
|
|
21
|
-
container.className = 'tabs';
|
|
22
|
-
const tabButtons = document.createElement('div');
|
|
23
|
-
tabButtons.className = 'tab-buttons flex border-b';
|
|
24
|
-
const tabContent = document.createElement('div');
|
|
25
|
-
tabContent.className = 'tab-content mt-4';
|
|
26
|
-
tabs.forEach((tab, index) => {
|
|
27
|
-
const btn = document.createElement('button');
|
|
28
|
-
btn.className = `tab tab-underline tab-button ${index === activeTab ? 'tab-active' : ''}`;
|
|
29
|
-
btn.textContent = tab.label;
|
|
30
|
-
btn.dataset.tabIndex = index;
|
|
31
|
-
btn.addEventListener('click', () => {
|
|
32
|
-
tabButtons.querySelectorAll('.tab-button').forEach((b, i) => b.classList.toggle('tab-active', i === index));
|
|
33
|
-
tabContent.querySelectorAll('.tab-pane').forEach((pane, i) => { pane.style.display = i === index ? 'block' : 'none'; });
|
|
34
|
-
if (onChange) onChange(index);
|
|
35
|
-
});
|
|
36
|
-
tabButtons.appendChild(btn);
|
|
37
|
-
const pane = document.createElement('div');
|
|
38
|
-
pane.className = 'tab-pane';
|
|
39
|
-
pane.style.display = index === activeTab ? 'block' : 'none';
|
|
40
|
-
pane.innerHTML = typeof tab.content === 'string' ? tab.content : '';
|
|
41
|
-
tabContent.appendChild(pane);
|
|
42
|
-
});
|
|
43
|
-
container.appendChild(tabButtons);
|
|
44
|
-
container.appendChild(tabContent);
|
|
45
|
-
return container;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
static createAlert(config = {}) {
|
|
49
|
-
const { message = '', type = 'info', duration = 5000, dismissible = true } = config;
|
|
50
|
-
const alert = document.createElement('div');
|
|
51
|
-
const typeClasses = { 'info': 'alert-info', 'success': 'alert-success', 'warning': 'alert-warning', 'error': 'alert-error' };
|
|
52
|
-
alert.className = `alert ${typeClasses[type] || typeClasses['info']}`;
|
|
53
|
-
alert.innerHTML = `<div class="flex justify-between items-center"><span>${UIComponents.escapeHtml(message)}</span>${dismissible ? '<button class="text-current hover:opacity-75">×</button>' : ''}</div>`;
|
|
54
|
-
if (dismissible) alert.querySelector('button').addEventListener('click', () => alert.remove());
|
|
55
|
-
if (duration > 0) setTimeout(() => alert.remove(), duration);
|
|
56
|
-
return alert;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
static createSpinner(config = {}) {
|
|
60
|
-
const { size = 'medium', text = 'Loading...' } = config;
|
|
61
|
-
const sizeClasses = { 'small': 'spinner-xs', 'medium': 'spinner-sm', 'large': 'spinner-md' };
|
|
62
|
-
const container = document.createElement('div');
|
|
63
|
-
container.className = 'flex items-center gap-3 justify-center p-4';
|
|
64
|
-
container.innerHTML = `<div class="spinner-simple spinner-primary ${sizeClasses[size] || sizeClasses['medium']}"></div><span class="text-base-content">${UIComponents.escapeHtml(text)}</span>`;
|
|
65
|
-
return container;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
static createProgressBar(config = {}) {
|
|
69
|
-
const { percentage = 0, label = '', showLabel = true } = config;
|
|
70
|
-
const container = document.createElement('div');
|
|
71
|
-
container.className = 'progress-container';
|
|
72
|
-
let html = '';
|
|
73
|
-
if (label && showLabel) html += `<div class="flex justify-between mb-2 text-sm"><span>${UIComponents.escapeHtml(label)}</span><span>${Math.round(percentage)}%</span></div>`;
|
|
74
|
-
html += `<progress class="progress progress-primary progress-xs w-full" value="${Math.min(100, Math.max(0, percentage))}" max="100"></progress>`;
|
|
75
|
-
container.innerHTML = html;
|
|
76
|
-
return container;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
static createCollapsible(config = {}) {
|
|
80
|
-
const { title = 'Details', content = '', isOpen = false } = config;
|
|
81
|
-
const container = document.createElement('div');
|
|
82
|
-
container.className = 'collapsible';
|
|
83
|
-
container.innerHTML = `<details ${isOpen ? 'open' : ''}><summary class="cursor-pointer font-semibold hover:bg-base-200 px-2 py-1 rounded transition-colors">${UIComponents.escapeHtml(title)}</summary><div class="content mt-2 ml-4">${typeof content === 'string' ? content : ''}</div></details>`;
|
|
84
|
-
return container;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = UIComponents;
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
Object.assign(WebSocketManager.prototype, {
|
|
2
|
-
sendMessage(data) {
|
|
3
|
-
if (!data || typeof data !== 'object') throw new Error('Invalid message data');
|
|
4
|
-
if (data.type === 'subscribe') {
|
|
5
|
-
const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
|
|
6
|
-
this.activeSubscriptions.add(key);
|
|
7
|
-
} else if (data.type === 'unsubscribe') {
|
|
8
|
-
const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
|
|
9
|
-
this.activeSubscriptions.delete(key);
|
|
10
|
-
}
|
|
11
|
-
if (!this.isConnected) { this.bufferMessage(data); return false; }
|
|
12
|
-
try {
|
|
13
|
-
this.ws.send(window._codec ? window._codec.encode(data) : msgpackr.pack(data));
|
|
14
|
-
this.stats.totalMessagesSent++;
|
|
15
|
-
return true;
|
|
16
|
-
} catch (error) { this.stats.totalErrors++; this.bufferMessage(data); return false; }
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
bufferMessage(data) {
|
|
20
|
-
if (this.messageBuffer.length >= this.config.maxBufferedMessages) this.messageBuffer.shift();
|
|
21
|
-
this.messageBuffer.push(data);
|
|
22
|
-
this.emit('message_buffered', { bufferLength: this.messageBuffer.length });
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
flushMessageBuffer() {
|
|
26
|
-
if (this.messageBuffer.length === 0) return;
|
|
27
|
-
const messages = [...this.messageBuffer];
|
|
28
|
-
this.messageBuffer = [];
|
|
29
|
-
for (const message of messages) {
|
|
30
|
-
try {
|
|
31
|
-
this.ws.send(window._codec ? window._codec.encode(message) : msgpackr.pack(message));
|
|
32
|
-
this.stats.totalMessagesSent++;
|
|
33
|
-
} catch (error) { this.bufferMessage(message); }
|
|
34
|
-
}
|
|
35
|
-
this.emit('buffer_flushed', { count: messages.length });
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
subscribeToSession(sessionId) { return this.sendMessage({ type: 'subscribe', sessionId, timestamp: Date.now() }); },
|
|
39
|
-
subscribeToConversation(conversationId) { return this.sendMessage({ type: 'subscribe', conversationId, timestamp: Date.now() }); },
|
|
40
|
-
|
|
41
|
-
resubscribeAll() {
|
|
42
|
-
for (const key of this.activeSubscriptions) {
|
|
43
|
-
const colonIdx = key.indexOf(':');
|
|
44
|
-
const type = key.substring(0, colonIdx);
|
|
45
|
-
const id = key.substring(colonIdx + 1);
|
|
46
|
-
const msg = { type: 'subscribe', timestamp: Date.now() };
|
|
47
|
-
if (type === 'session') msg.sessionId = id; else msg.conversationId = id;
|
|
48
|
-
try { this.ws.send(window._codec ? window._codec.encode(msg) : msgpackr.pack(msg)); this.stats.totalMessagesSent++; } catch (_) {}
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
unsubscribeFromSession(sessionId) { return this.sendMessage({ type: 'unsubscribe', sessionId, timestamp: Date.now() }); },
|
|
53
|
-
|
|
54
|
-
requestSessionHistory(sessionId, limit = 1000, offset = 0) {
|
|
55
|
-
return new Promise((resolve, reject) => {
|
|
56
|
-
const requestId = 'history-' + Date.now() + '-' + Math.random();
|
|
57
|
-
const timeout = setTimeout(() => { this.requestMap.delete(requestId); this.stats.totalTimeouts++; reject(new Error('History request timeout')); }, this.config.messageTimeout);
|
|
58
|
-
this.requestMap.set(requestId, { type: 'history', resolve: (d) => { clearTimeout(timeout); resolve(d); }, reject });
|
|
59
|
-
this.sendMessage({ type: 'request_history', requestId, sessionId, limit, offset, timestamp: Date.now() });
|
|
60
|
-
});
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
getLastSeq(sessionId) { return this.lastSeqBySession[sessionId] || -1; },
|
|
64
|
-
|
|
65
|
-
setConnectionState(state) {
|
|
66
|
-
this.connectionState = state;
|
|
67
|
-
this.emit('state_change', { state, timestamp: Date.now() });
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
disconnect() {
|
|
71
|
-
this.isManuallyDisconnected = true;
|
|
72
|
-
this.reconnectCount = 0;
|
|
73
|
-
this.stopHeartbeat();
|
|
74
|
-
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
|
75
|
-
if (this.ws) this.ws.close();
|
|
76
|
-
this.messageBuffer = [];
|
|
77
|
-
this.requestMap.clear();
|
|
78
|
-
this.setConnectionState('disconnected');
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
getStatus() {
|
|
82
|
-
return { isConnected: this.isConnected, isConnecting: this.isConnecting, connectionState: this.connectionState, reconnectCount: this.reconnectCount, bufferLength: this.messageBuffer.length, latency: { ...this.latency, samples: undefined }, stats: { ...this.stats } };
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
on(event, callback) { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(callback); },
|
|
86
|
-
off(event, callback) { if (!this.listeners[event]) return; const index = this.listeners[event].indexOf(callback); if (index > -1) this.listeners[event].splice(index, 1); },
|
|
87
|
-
emit(event, data) { if (!this.listeners[event]) return; this.listeners[event].forEach((cb) => { try { cb(data); } catch (error) {} }); },
|
|
88
|
-
|
|
89
|
-
destroy() {
|
|
90
|
-
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', this._onVisibilityChange);
|
|
91
|
-
if (typeof window !== 'undefined') window.removeEventListener('online', this._onOnline);
|
|
92
|
-
this.disconnect();
|
|
93
|
-
this.listeners = {};
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
subscribeToVoiceList(callback) {
|
|
97
|
-
if (!this.voiceListListeners.includes(callback)) this.voiceListListeners.push(callback);
|
|
98
|
-
if (this.cachedVoiceList !== null) callback(this.cachedVoiceList);
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
unsubscribeFromVoiceList(callback) {
|
|
102
|
-
const idx = this.voiceListListeners.indexOf(callback);
|
|
103
|
-
if (idx > -1) this.voiceListListeners.splice(idx, 1);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = WebSocketManager;
|
package/static/js/ws-client.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
class WsClient {
|
|
2
|
-
constructor(wsManager) {
|
|
3
|
-
this._ws = wsManager;
|
|
4
|
-
this._pending = new Map();
|
|
5
|
-
this._installed = false;
|
|
6
|
-
this._connectPromise = null;
|
|
7
|
-
this._install();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
_install() {
|
|
11
|
-
if (this._installed) return;
|
|
12
|
-
this._installed = true;
|
|
13
|
-
this._ws.on('message', (data) => {
|
|
14
|
-
if (data.r && this._pending.has(data.r)) {
|
|
15
|
-
const p = this._pending.get(data.r);
|
|
16
|
-
this._pending.delete(data.r);
|
|
17
|
-
clearTimeout(p.timer);
|
|
18
|
-
if (data.e) {
|
|
19
|
-
p.reject(Object.assign(new Error(data.e.m || 'RPC error'), { code: data.e.c }));
|
|
20
|
-
} else {
|
|
21
|
-
p.resolve(data.d);
|
|
22
|
-
}
|
|
23
|
-
return; // consumed — don't re-emit
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
this._ws.on('disconnected', () => this.cancelAll());
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
_ensureConnected() {
|
|
30
|
-
if (this._ws.isConnected) return Promise.resolve();
|
|
31
|
-
if (this._connectPromise) return this._connectPromise;
|
|
32
|
-
this._connectPromise = this._ws.connect().then(() => {
|
|
33
|
-
this._connectPromise = null;
|
|
34
|
-
}).catch(() => {
|
|
35
|
-
this._connectPromise = null;
|
|
36
|
-
});
|
|
37
|
-
return this._connectPromise;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
_id() {
|
|
41
|
-
let id = '';
|
|
42
|
-
for (let i = 0; i < 8; i++) id += ((Math.random() * 16) | 0).toString(16);
|
|
43
|
-
return id;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
request(method, params = {}, timeout = 30000) {
|
|
47
|
-
return this._ensureConnected().then(() => {
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
const r = this._id();
|
|
50
|
-
const timer = setTimeout(() => {
|
|
51
|
-
this._pending.delete(r);
|
|
52
|
-
reject(new Error(`RPC timeout: ${method}`));
|
|
53
|
-
}, timeout);
|
|
54
|
-
this._pending.set(r, { resolve, reject, timer });
|
|
55
|
-
this._ws.sendMessage({ r, m: method, p: params });
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
rpc(method, params) {
|
|
61
|
-
return this.request(method, params);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
cancelAll() {
|
|
65
|
-
for (const [, p] of this._pending) {
|
|
66
|
-
clearTimeout(p.timer);
|
|
67
|
-
p.reject(new Error('Connection lost'));
|
|
68
|
-
}
|
|
69
|
-
this._pending.clear();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
get pendingCount() {
|
|
73
|
-
return this._pending.size;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
window.WsClient = WsClient;
|
|
78
|
-
|
|
79
|
-
window.wsManager = new WebSocketManager();
|
|
80
|
-
window.wsClient = new WsClient(window.wsManager);
|
|
81
|
-
window.wsManager.connect().catch(function() {});
|
|
82
|
-
|
|
83
|
-
import('./codec.js').then(codec => {
|
|
84
|
-
window._codec = codec;
|
|
85
|
-
}).catch(e => {
|
|
86
|
-
console.error('[ws-client] Failed to load codec, using msgpackr fallback:', e);
|
|
87
|
-
});
|
package/static/js/ws-core.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
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
|
-
}
|
package/static/js/ws-latency.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
Object.assign(WebSocketManager.prototype, {
|
|
2
|
-
startHeartbeat() {
|
|
3
|
-
this.stopHeartbeat();
|
|
4
|
-
this.heartbeatTimer = setInterval(() => { this.ping(); }, this.config.heartbeatInterval);
|
|
5
|
-
},
|
|
6
|
-
|
|
7
|
-
stopHeartbeat() {
|
|
8
|
-
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
9
|
-
},
|
|
10
|
-
|
|
11
|
-
_reportLatency() {
|
|
12
|
-
this.emit('latency', { current: this.latency.current, avg: this.latency.avg, jitter: this.latency.jitter, quality: this.latency.quality, trend: this.latency.trend });
|
|
13
|
-
},
|
|
14
|
-
|
|
15
|
-
_handleVisibilityChange() {
|
|
16
|
-
if (document.visibilityState === 'visible' && !this.isConnected && !this.isManuallyDisconnected) this.connect().catch(() => {});
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
_handleOnline() {
|
|
20
|
-
if (!this.isConnected && !this.isManuallyDisconnected) this.connect().catch(() => {});
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
ping() {
|
|
24
|
-
if (!this.isConnected) return;
|
|
25
|
-
this.latency.pingCounter++;
|
|
26
|
-
const id = this.latency.pingCounter;
|
|
27
|
-
const pongTimer = setTimeout(() => {
|
|
28
|
-
this.latency.missedPongs++;
|
|
29
|
-
this.emit('latency', { current: null, avg: this.latency.avg, jitter: this.latency.jitter, quality: 'poor', missed: true });
|
|
30
|
-
if (this.latency.missedPongs >= 3) { this.onClose(); }
|
|
31
|
-
}, this.config.pongTimeout);
|
|
32
|
-
this.requestMap.set('ping:' + id, { type: 'ping', sentAt: Date.now(), pongTimer });
|
|
33
|
-
try {
|
|
34
|
-
this.ws.send(window._codec ? window._codec.encode({ type: 'ping', id }) : msgpackr.pack({ type: 'ping', id }));
|
|
35
|
-
} catch (_) { clearTimeout(pongTimer); this.requestMap.delete('ping:' + id); }
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
_handlePong(data) {
|
|
39
|
-
const key = 'ping:' + data.id;
|
|
40
|
-
const pending = this.requestMap.get(key);
|
|
41
|
-
if (!pending) return;
|
|
42
|
-
clearTimeout(pending.pongTimer);
|
|
43
|
-
this.requestMap.delete(key);
|
|
44
|
-
this.latency.missedPongs = 0;
|
|
45
|
-
const rtt = Date.now() - pending.sentAt;
|
|
46
|
-
this._recordLatency(rtt);
|
|
47
|
-
this._checkDegradation();
|
|
48
|
-
this._reportLatency();
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
_recordLatency(rtt) {
|
|
52
|
-
const samples = this.latency.samples;
|
|
53
|
-
samples.push(rtt);
|
|
54
|
-
if (samples.length > this.config.latencyWindowSize) samples.shift();
|
|
55
|
-
this.latency.current = rtt;
|
|
56
|
-
this.latency.avg = Math.round(samples.reduce((a, b) => a + b, 0) / samples.length);
|
|
57
|
-
const jitterSamples = samples.slice(1).map((v, i) => Math.abs(v - samples[i]));
|
|
58
|
-
this.latency.jitter = jitterSamples.length ? Math.round(jitterSamples.reduce((a, b) => a + b, 0) / jitterSamples.length) : 0;
|
|
59
|
-
this.latency.quality = this._qualityTier(this.latency.avg);
|
|
60
|
-
if (this._latencyEma === null) this._latencyEma = rtt;
|
|
61
|
-
else this._latencyEma = 0.3 * rtt + 0.7 * this._latencyEma;
|
|
62
|
-
this.latency.predicted = Math.round(this._latencyEma);
|
|
63
|
-
this.stats.avgLatency = this.latency.avg;
|
|
64
|
-
this._trendHistory.push(rtt);
|
|
65
|
-
if (this._trendHistory.length > 5) this._trendHistory.shift();
|
|
66
|
-
if (this._trendHistory.length >= 3) {
|
|
67
|
-
const h = this._trendHistory;
|
|
68
|
-
const slope = (h[h.length - 1] - h[0]) / h.length;
|
|
69
|
-
this.latency.trend = slope > 5 ? 'rising' : slope < -5 ? 'falling' : 'stable';
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
_qualityTier(avg) {
|
|
74
|
-
if (avg < 50) return 'excellent';
|
|
75
|
-
if (avg < 150) return 'good';
|
|
76
|
-
if (avg < 300) return 'fair';
|
|
77
|
-
return 'poor';
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
_checkDegradation() {
|
|
81
|
-
const tier = this.latency.quality;
|
|
82
|
-
const prev = this._lastQualityTier;
|
|
83
|
-
if (tier !== prev) {
|
|
84
|
-
this._lastQualityTier = tier;
|
|
85
|
-
this.emit('quality_change', { quality: tier, latency: this.latency.avg });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
});
|
package/static/js/ws-machine.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
const { createMachine, createActor, assign } = XState;
|
|
3
|
-
|
|
4
|
-
const wsMachine = createMachine({
|
|
5
|
-
id: 'websocket',
|
|
6
|
-
initial: 'disconnected',
|
|
7
|
-
context: {
|
|
8
|
-
reconnectCount: 0,
|
|
9
|
-
lastConnectedAt: null,
|
|
10
|
-
lastDisconnectedAt: null,
|
|
11
|
-
manualDisconnect: false,
|
|
12
|
-
},
|
|
13
|
-
states: {
|
|
14
|
-
disconnected: {
|
|
15
|
-
entry: assign({ lastDisconnectedAt: () => Date.now() }),
|
|
16
|
-
on: {
|
|
17
|
-
CONNECT: { target: 'connecting', guard: ({ context }) => !context.manualDisconnect },
|
|
18
|
-
MANUAL_DISCONNECT: { actions: assign({ manualDisconnect: true }) },
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
connecting: {
|
|
22
|
-
on: {
|
|
23
|
-
OPEN: {
|
|
24
|
-
target: 'connected',
|
|
25
|
-
actions: assign({ reconnectCount: 0, lastConnectedAt: () => Date.now() }),
|
|
26
|
-
},
|
|
27
|
-
ERROR: 'reconnecting',
|
|
28
|
-
CLOSE: 'reconnecting',
|
|
29
|
-
MANUAL_DISCONNECT: {
|
|
30
|
-
target: 'disconnected',
|
|
31
|
-
actions: assign({ manualDisconnect: true }),
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
connected: {
|
|
36
|
-
entry: assign({ lastConnectedAt: () => Date.now() }),
|
|
37
|
-
on: {
|
|
38
|
-
CLOSE: 'reconnecting',
|
|
39
|
-
ERROR: 'reconnecting',
|
|
40
|
-
MANUAL_DISCONNECT: {
|
|
41
|
-
target: 'disconnected',
|
|
42
|
-
actions: assign({ manualDisconnect: true, reconnectCount: 0 }),
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
reconnecting: {
|
|
47
|
-
entry: assign(({ context }) => ({ reconnectCount: context.reconnectCount + 1 })),
|
|
48
|
-
on: {
|
|
49
|
-
CONNECT: 'connecting',
|
|
50
|
-
MANUAL_DISCONNECT: {
|
|
51
|
-
target: 'disconnected',
|
|
52
|
-
actions: assign({ manualDisconnect: true, reconnectCount: 0 }),
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
function createWsActor() {
|
|
60
|
-
const actor = createActor(wsMachine);
|
|
61
|
-
actor.start();
|
|
62
|
-
return actor;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (typeof window !== 'undefined') {
|
|
66
|
-
window.createWsActor = createWsActor;
|
|
67
|
-
window.wsMachine = wsMachine;
|
|
68
|
-
}
|