agentgui 1.0.260 → 1.0.262
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/CLAUDE.md +139 -0
- package/IPFS_DOWNLOADER.md +277 -0
- package/database.js +153 -0
- package/lib/ipfs-downloader.js +311 -0
- package/package.json +1 -1
- package/server.js +33 -5
- package/static/index.html +170 -16
- package/static/js/client.js +31 -17
- package/static/js/streaming-renderer.js +13 -0
- package/test-download-progress.js +223 -0
- package/test-websocket-broadcast.js +147 -0
- package/tests/ipfs-downloader.test.js +370 -0
package/static/js/client.js
CHANGED
|
@@ -494,7 +494,7 @@ class AgentGUIClient {
|
|
|
494
494
|
this.handleRateLimitClear(data);
|
|
495
495
|
break;
|
|
496
496
|
case 'model_download_progress':
|
|
497
|
-
this._handleModelDownloadProgress(data.progress);
|
|
497
|
+
this._handleModelDownloadProgress(data.progress || data);
|
|
498
498
|
break;
|
|
499
499
|
default:
|
|
500
500
|
break;
|
|
@@ -1188,9 +1188,10 @@ class AgentGUIClient {
|
|
|
1188
1188
|
const tTitle = hasRenderer && block.input ? StreamingRenderer.getToolTitle(tn, block.input) : '';
|
|
1189
1189
|
const iconHtml = hasRenderer && this.renderer ? `<span class="folded-tool-icon">${this.renderer.getToolIcon(tn)}</span>` : '';
|
|
1190
1190
|
const typeClass = hasRenderer && this.renderer ? this.renderer._getBlockTypeClass('tool_use') : 'block-type-tool_use';
|
|
1191
|
+
const toolColorClass = hasRenderer && this.renderer ? this.renderer._getToolColorClass(tn) : 'tool-color-default';
|
|
1191
1192
|
const nextBlock = blocks[blockIdx + 1];
|
|
1192
1193
|
const resultClass = nextBlock?.type === 'tool_result' ? (nextBlock.is_error ? 'has-error' : 'has-success') : '';
|
|
1193
|
-
html += `<details class="block-tool-use folded-tool ${typeClass} ${resultClass}"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}`;
|
|
1194
|
+
html += `<details class="block-tool-use folded-tool ${typeClass} ${toolColorClass} ${resultClass}"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}`;
|
|
1194
1195
|
pendingToolUseClose = true;
|
|
1195
1196
|
} else if (block.type === 'tool_result') {
|
|
1196
1197
|
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
@@ -2027,16 +2028,16 @@ class AgentGUIClient {
|
|
|
2027
2028
|
|
|
2028
2029
|
_handleModelDownloadProgress(progress) {
|
|
2029
2030
|
this._modelDownloadProgress = progress;
|
|
2030
|
-
|
|
2031
|
-
if (progress.error) {
|
|
2031
|
+
|
|
2032
|
+
if (progress.status === 'failed' || progress.error) {
|
|
2032
2033
|
this._modelDownloadInProgress = false;
|
|
2033
|
-
console.error('[Models] Download error:', progress.error);
|
|
2034
|
+
console.error('[Models] Download error:', progress.error || progress.status);
|
|
2034
2035
|
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
2035
2036
|
if (window._voiceProgressDialog) {
|
|
2036
2037
|
window._voiceProgressDialog.close();
|
|
2037
2038
|
window._voiceProgressDialog = null;
|
|
2038
2039
|
}
|
|
2039
|
-
const errorMsg = 'Failed to download voice models: ' + progress.error;
|
|
2040
|
+
const errorMsg = 'Failed to download voice models: ' + (progress.error || 'unknown error');
|
|
2040
2041
|
if (window.UIDialog) {
|
|
2041
2042
|
window.UIDialog.alert(errorMsg, 'Download Error');
|
|
2042
2043
|
} else {
|
|
@@ -2044,8 +2045,8 @@ class AgentGUIClient {
|
|
|
2044
2045
|
}
|
|
2045
2046
|
return;
|
|
2046
2047
|
}
|
|
2047
|
-
|
|
2048
|
-
if (progress.done) {
|
|
2048
|
+
|
|
2049
|
+
if (progress.done || progress.status === 'completed') {
|
|
2049
2050
|
this._modelDownloadInProgress = false;
|
|
2050
2051
|
console.log('[Models] Download complete');
|
|
2051
2052
|
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
@@ -2066,8 +2067,8 @@ class AgentGUIClient {
|
|
|
2066
2067
|
}
|
|
2067
2068
|
return;
|
|
2068
2069
|
}
|
|
2069
|
-
|
|
2070
|
-
if (progress.started || progress.downloading) {
|
|
2070
|
+
|
|
2071
|
+
if (progress.started || progress.downloading || progress.status === 'downloading' || progress.status === 'connecting') {
|
|
2071
2072
|
this._modelDownloadInProgress = true;
|
|
2072
2073
|
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
2073
2074
|
|
|
@@ -2075,13 +2076,26 @@ class AgentGUIClient {
|
|
|
2075
2076
|
window.__showVoiceDownloadProgress();
|
|
2076
2077
|
}
|
|
2077
2078
|
|
|
2078
|
-
if (window._voiceProgressDialog
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2079
|
+
if (window._voiceProgressDialog) {
|
|
2080
|
+
let displayText = 'Downloading models...';
|
|
2081
|
+
|
|
2082
|
+
if (progress.status === 'connecting') {
|
|
2083
|
+
displayText = 'Connecting to ' + (progress.currentGateway || 'gateway') + '...';
|
|
2084
|
+
} else if (progress.totalBytes > 0) {
|
|
2085
|
+
const downloaded = (progress.bytesDownloaded || 0) / 1024 / 1024;
|
|
2086
|
+
const total = progress.totalBytes / 1024 / 1024;
|
|
2087
|
+
const speed = progress.downloadSpeed ? (progress.downloadSpeed / 1024 / 1024).toFixed(2) : '0';
|
|
2088
|
+
const eta = progress.eta ? Math.ceil(progress.eta) + 's' : '...';
|
|
2089
|
+
const retryInfo = progress.retryCount > 0 ? ` (retry ${progress.retryCount})` : '';
|
|
2090
|
+
|
|
2091
|
+
displayText = `Downloading ${downloaded.toFixed(1)}MB / ${total.toFixed(1)}MB @ ${speed}MB/s (ETA: ${eta})${retryInfo}`;
|
|
2092
|
+
} else if (progress.file) {
|
|
2093
|
+
displayText = 'Loading ' + progress.file + '...';
|
|
2094
|
+
} else if (progress.completedFiles && progress.totalFiles) {
|
|
2095
|
+
displayText = `Downloaded ${progress.completedFiles}/${progress.totalFiles} files`;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
window._voiceProgressDialog.update(progress.percentComplete || 0, displayText);
|
|
2085
2099
|
}
|
|
2086
2100
|
}
|
|
2087
2101
|
}
|
|
@@ -422,6 +422,12 @@ class StreamingRenderer {
|
|
|
422
422
|
return validTypes.includes(blockType) ? `block-type-${blockType}` : 'block-type-generic';
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
_getToolColorClass(toolName) {
|
|
426
|
+
const n = (toolName || '').replace(/^mcp__[^_]+__/, '').toLowerCase();
|
|
427
|
+
const map = { read: 'read', write: 'write', edit: 'edit', bash: 'bash', glob: 'glob', grep: 'grep', webfetch: 'web', websearch: 'web', todowrite: 'todo', task: 'task', notebookedit: 'edit' };
|
|
428
|
+
return `tool-color-${map[n] || 'default'}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
425
431
|
containsHtmlTags(text) {
|
|
426
432
|
const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
|
|
427
433
|
return htmlPattern.test(text);
|
|
@@ -737,6 +743,7 @@ class StreamingRenderer {
|
|
|
737
743
|
details.className = 'block-tool-use folded-tool';
|
|
738
744
|
if (block.id) details.dataset.toolUseId = block.id;
|
|
739
745
|
details.classList.add(this._getBlockTypeClass('tool_use'));
|
|
746
|
+
details.classList.add(this._getToolColorClass(toolName));
|
|
740
747
|
const summary = document.createElement('summary');
|
|
741
748
|
summary.className = 'folded-tool-bar';
|
|
742
749
|
const displayName = this.getToolUseDisplayName(toolName);
|
|
@@ -1625,6 +1632,8 @@ class StreamingRenderer {
|
|
|
1625
1632
|
const fileName = event.path ? event.path.split('/').pop() : 'unknown';
|
|
1626
1633
|
const details = document.createElement('details');
|
|
1627
1634
|
details.className = 'block-tool-use folded-tool';
|
|
1635
|
+
details.classList.add(this._getBlockTypeClass('tool_use'));
|
|
1636
|
+
details.classList.add(this._getToolColorClass('Read'));
|
|
1628
1637
|
details.dataset.eventId = event.id || '';
|
|
1629
1638
|
details.dataset.eventType = 'file_read';
|
|
1630
1639
|
const summary = document.createElement('summary');
|
|
@@ -1656,6 +1665,8 @@ class StreamingRenderer {
|
|
|
1656
1665
|
const fileName = event.path ? event.path.split('/').pop() : 'unknown';
|
|
1657
1666
|
const details = document.createElement('details');
|
|
1658
1667
|
details.className = 'block-tool-use folded-tool';
|
|
1668
|
+
details.classList.add(this._getBlockTypeClass('tool_use'));
|
|
1669
|
+
details.classList.add(this._getToolColorClass('Write'));
|
|
1659
1670
|
details.dataset.eventId = event.id || '';
|
|
1660
1671
|
details.dataset.eventType = 'file_write';
|
|
1661
1672
|
const summary = document.createElement('summary');
|
|
@@ -1719,6 +1730,8 @@ class StreamingRenderer {
|
|
|
1719
1730
|
|
|
1720
1731
|
const details = document.createElement('details');
|
|
1721
1732
|
details.className = 'block-tool-use folded-tool';
|
|
1733
|
+
details.classList.add(this._getBlockTypeClass('tool_use'));
|
|
1734
|
+
details.classList.add(this._getToolColorClass('Bash'));
|
|
1722
1735
|
details.dataset.eventId = event.id || '';
|
|
1723
1736
|
details.dataset.eventType = 'command_execute';
|
|
1724
1737
|
const summary = document.createElement('summary');
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const testDir = path.join(os.tmpdir(), 'test-download-progress');
|
|
7
|
+
if (!fs.existsSync(testDir)) {
|
|
8
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const GATEWAYS = [
|
|
12
|
+
'https://ipfs.io',
|
|
13
|
+
'https://gateway.pinata.cloud',
|
|
14
|
+
'https://cloudflare-ipfs.com',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function downloadWithProgress(url, destination, onProgress = null) {
|
|
18
|
+
let bytesDownloaded = 0;
|
|
19
|
+
let totalBytes = 0;
|
|
20
|
+
let lastProgressTime = Date.now();
|
|
21
|
+
let lastProgressBytes = 0;
|
|
22
|
+
const speeds = [];
|
|
23
|
+
let retryCount = 0;
|
|
24
|
+
let gatewayIndex = 0;
|
|
25
|
+
|
|
26
|
+
const emitProgress = () => {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const deltaTime = (now - lastProgressTime) / 1000;
|
|
29
|
+
const deltaBytes = bytesDownloaded - lastProgressBytes;
|
|
30
|
+
const speed = deltaTime > 0 ? Math.round(deltaBytes / deltaTime) : 0;
|
|
31
|
+
|
|
32
|
+
if (speed > 0) {
|
|
33
|
+
speeds.push(speed);
|
|
34
|
+
if (speeds.length > 10) speeds.shift();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const avgSpeed = speeds.length > 0 ? Math.round(speeds.reduce((a, b) => a + b, 0) / speeds.length) : 0;
|
|
38
|
+
const eta = avgSpeed > 0 && totalBytes > bytesDownloaded ? Math.round((totalBytes - bytesDownloaded) / avgSpeed) : 0;
|
|
39
|
+
|
|
40
|
+
if (onProgress) {
|
|
41
|
+
onProgress({
|
|
42
|
+
bytesDownloaded,
|
|
43
|
+
bytesRemaining: Math.max(0, totalBytes - bytesDownloaded),
|
|
44
|
+
totalBytes,
|
|
45
|
+
downloadSpeed: avgSpeed,
|
|
46
|
+
eta,
|
|
47
|
+
retryCount,
|
|
48
|
+
currentGateway: url,
|
|
49
|
+
status: bytesDownloaded >= totalBytes ? 'completed' : 'downloading',
|
|
50
|
+
percentComplete: totalBytes > 0 ? Math.round((bytesDownloaded / totalBytes) * 100) : 0,
|
|
51
|
+
timestamp: now
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lastProgressTime = now;
|
|
56
|
+
lastProgressBytes = bytesDownloaded;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const dir = path.dirname(destination);
|
|
61
|
+
if (!fs.existsSync(dir)) {
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const attemptDownload = (gateway) => {
|
|
66
|
+
if (onProgress) {
|
|
67
|
+
onProgress({
|
|
68
|
+
bytesDownloaded: 0,
|
|
69
|
+
bytesRemaining: 0,
|
|
70
|
+
totalBytes: 0,
|
|
71
|
+
downloadSpeed: 0,
|
|
72
|
+
eta: 0,
|
|
73
|
+
retryCount,
|
|
74
|
+
currentGateway: gateway,
|
|
75
|
+
status: 'connecting',
|
|
76
|
+
percentComplete: 0,
|
|
77
|
+
timestamp: Date.now()
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
https.get(gateway, { timeout: 30000 }, (res) => {
|
|
82
|
+
if ([301, 302, 307, 308].includes(res.statusCode)) {
|
|
83
|
+
const location = res.headers.location;
|
|
84
|
+
if (location) return attemptDownload(location);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (res.statusCode !== 200) {
|
|
88
|
+
res.resume();
|
|
89
|
+
if (gatewayIndex < GATEWAYS.length - 1) {
|
|
90
|
+
retryCount++;
|
|
91
|
+
gatewayIndex++;
|
|
92
|
+
return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
|
|
93
|
+
}
|
|
94
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
totalBytes = parseInt(res.headers['content-length'], 10) || 0;
|
|
98
|
+
bytesDownloaded = 0;
|
|
99
|
+
lastProgressBytes = 0;
|
|
100
|
+
lastProgressTime = Date.now();
|
|
101
|
+
|
|
102
|
+
const file = fs.createWriteStream(destination);
|
|
103
|
+
let lastEmit = Date.now();
|
|
104
|
+
|
|
105
|
+
res.on('data', (chunk) => {
|
|
106
|
+
bytesDownloaded += chunk.length;
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (now - lastEmit >= 200) {
|
|
109
|
+
emitProgress();
|
|
110
|
+
lastEmit = now;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
res.on('end', () => {
|
|
115
|
+
emitProgress();
|
|
116
|
+
file.destroy();
|
|
117
|
+
resolve({ destination, bytesDownloaded, success: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
res.on('error', (err) => {
|
|
121
|
+
file.destroy();
|
|
122
|
+
fs.unlink(destination, () => {});
|
|
123
|
+
if (gatewayIndex < GATEWAYS.length - 1) {
|
|
124
|
+
retryCount++;
|
|
125
|
+
gatewayIndex++;
|
|
126
|
+
return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
|
|
127
|
+
}
|
|
128
|
+
reject(err);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
file.on('error', (err) => {
|
|
132
|
+
res.destroy();
|
|
133
|
+
fs.unlink(destination, () => {});
|
|
134
|
+
if (gatewayIndex < GATEWAYS.length - 1) {
|
|
135
|
+
retryCount++;
|
|
136
|
+
gatewayIndex++;
|
|
137
|
+
return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
|
|
138
|
+
}
|
|
139
|
+
reject(err);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
res.pipe(file);
|
|
143
|
+
}).on('timeout', () => {
|
|
144
|
+
if (gatewayIndex < GATEWAYS.length - 1) {
|
|
145
|
+
retryCount++;
|
|
146
|
+
gatewayIndex++;
|
|
147
|
+
return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
|
|
148
|
+
}
|
|
149
|
+
reject(new Error('Download timeout'));
|
|
150
|
+
}).on('error', (err) => {
|
|
151
|
+
if (gatewayIndex < GATEWAYS.length - 1) {
|
|
152
|
+
retryCount++;
|
|
153
|
+
gatewayIndex++;
|
|
154
|
+
return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
|
|
155
|
+
}
|
|
156
|
+
reject(err);
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
attemptDownload(GATEWAYS[0]);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let progressCount = 0;
|
|
165
|
+
let lastPrintTime = Date.now();
|
|
166
|
+
let minInterval = Infinity;
|
|
167
|
+
let maxInterval = 0;
|
|
168
|
+
const progressIntervals = [];
|
|
169
|
+
|
|
170
|
+
console.log('Starting download progress tracking test...\n');
|
|
171
|
+
|
|
172
|
+
downloadWithProgress(
|
|
173
|
+
'https://www.w3.org/WAI/WCAG21/Techniques/pdf/pdf-files/table-example.pdf',
|
|
174
|
+
path.join(testDir, 'test-file.pdf'),
|
|
175
|
+
(progress) => {
|
|
176
|
+
progressCount++;
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
const interval = now - lastPrintTime;
|
|
179
|
+
|
|
180
|
+
if (progressCount > 1) {
|
|
181
|
+
progressIntervals.push(interval);
|
|
182
|
+
minInterval = Math.min(minInterval, interval);
|
|
183
|
+
maxInterval = Math.max(maxInterval, interval);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(`[${progressCount}] Progress Update:
|
|
187
|
+
Status: ${progress.status}
|
|
188
|
+
Downloaded: ${(progress.bytesDownloaded / 1024).toFixed(1)}KB / ${(progress.totalBytes / 1024).toFixed(1)}KB
|
|
189
|
+
Speed: ${(progress.downloadSpeed / 1024).toFixed(2)}MB/s
|
|
190
|
+
ETA: ${progress.eta}s
|
|
191
|
+
Complete: ${progress.percentComplete}%
|
|
192
|
+
Retry Count: ${progress.retryCount}
|
|
193
|
+
Gateway: ${progress.currentGateway}
|
|
194
|
+
Interval: ${interval}ms\n`);
|
|
195
|
+
|
|
196
|
+
lastPrintTime = now;
|
|
197
|
+
}
|
|
198
|
+
).then((result) => {
|
|
199
|
+
const avgInterval = progressIntervals.length > 0 ? progressIntervals.reduce((a, b) => a + b, 0) / progressIntervals.length : 0;
|
|
200
|
+
console.log('\n=== Download Complete ===');
|
|
201
|
+
console.log(`Result: ${JSON.stringify(result, null, 2)}`);
|
|
202
|
+
console.log(`\nProgress Tracking Statistics:
|
|
203
|
+
Total Updates: ${progressCount}
|
|
204
|
+
Interval Range: ${minInterval}ms - ${maxInterval}ms
|
|
205
|
+
Average Interval: ${avgInterval.toFixed(0)}ms
|
|
206
|
+
Expected Interval: 200ms (should be 100-500ms range)`);
|
|
207
|
+
|
|
208
|
+
if (avgInterval >= 100 && avgInterval <= 500) {
|
|
209
|
+
console.log(' Status: PASS - Progress interval within acceptable range');
|
|
210
|
+
} else {
|
|
211
|
+
console.log(' Status: FAIL - Progress interval outside acceptable range');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (fs.existsSync(path.join(testDir, 'test-file.pdf'))) {
|
|
215
|
+
const stat = fs.statSync(path.join(testDir, 'test-file.pdf'));
|
|
216
|
+
console.log(`\nDownloaded file size: ${(stat.size / 1024).toFixed(1)}KB`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}).catch((err) => {
|
|
221
|
+
console.error('Download failed:', err.message);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const PORT = 8899;
|
|
7
|
+
const server = http.createServer();
|
|
8
|
+
const wss = new WebSocketServer({ server, clientTracking: true });
|
|
9
|
+
|
|
10
|
+
let broadcastedMessages = [];
|
|
11
|
+
let clientConnected = false;
|
|
12
|
+
|
|
13
|
+
wss.on('connection', (ws) => {
|
|
14
|
+
clientConnected = true;
|
|
15
|
+
console.log('[WS] Client connected');
|
|
16
|
+
|
|
17
|
+
ws.on('message', (data) => {
|
|
18
|
+
const msg = JSON.parse(data);
|
|
19
|
+
console.log('[WS] Client sent:', msg.type);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
ws.on('close', () => {
|
|
23
|
+
clientConnected = false;
|
|
24
|
+
console.log('[WS] Client disconnected');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function broadcastSync(event) {
|
|
29
|
+
if (wss.clients.size === 0) return;
|
|
30
|
+
const data = JSON.stringify(event);
|
|
31
|
+
broadcastedMessages.push(event);
|
|
32
|
+
for (const client of wss.clients) {
|
|
33
|
+
if (client.readyState === 1) {
|
|
34
|
+
client.send(data);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function broadcastModelProgress(progress) {
|
|
40
|
+
const broadcastData = {
|
|
41
|
+
type: 'model_download_progress',
|
|
42
|
+
modelId: progress.type || 'unknown',
|
|
43
|
+
bytesDownloaded: progress.bytesDownloaded || 0,
|
|
44
|
+
bytesRemaining: progress.bytesRemaining || 0,
|
|
45
|
+
totalBytes: progress.totalBytes || 0,
|
|
46
|
+
downloadSpeed: progress.downloadSpeed || 0,
|
|
47
|
+
eta: progress.eta || 0,
|
|
48
|
+
retryCount: progress.retryCount || 0,
|
|
49
|
+
currentGateway: progress.currentGateway || '',
|
|
50
|
+
status: progress.status || (progress.done ? 'completed' : progress.downloading ? 'downloading' : 'paused'),
|
|
51
|
+
percentComplete: progress.percentComplete || 0,
|
|
52
|
+
completedFiles: progress.completedFiles || 0,
|
|
53
|
+
totalFiles: progress.totalFiles || 0,
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
...progress
|
|
56
|
+
};
|
|
57
|
+
broadcastSync(broadcastData);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
server.listen(PORT, () => {
|
|
61
|
+
console.log(`[Server] Listening on ws://localhost:${PORT}`);
|
|
62
|
+
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
console.log('\n[Test] Simulating model download progress...\n');
|
|
65
|
+
|
|
66
|
+
broadcastModelProgress({
|
|
67
|
+
type: 'stt',
|
|
68
|
+
started: true,
|
|
69
|
+
downloading: true,
|
|
70
|
+
completedFiles: 0,
|
|
71
|
+
totalFiles: 10
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
broadcastModelProgress({
|
|
76
|
+
type: 'stt',
|
|
77
|
+
bytesDownloaded: 5242880,
|
|
78
|
+
bytesRemaining: 20971520,
|
|
79
|
+
totalBytes: 26214400,
|
|
80
|
+
downloadSpeed: 1048576,
|
|
81
|
+
eta: 20,
|
|
82
|
+
retryCount: 0,
|
|
83
|
+
currentGateway: 'https://huggingface.co/',
|
|
84
|
+
status: 'downloading',
|
|
85
|
+
percentComplete: 20,
|
|
86
|
+
completedFiles: 2,
|
|
87
|
+
totalFiles: 10
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
broadcastModelProgress({
|
|
92
|
+
type: 'stt',
|
|
93
|
+
bytesDownloaded: 15728640,
|
|
94
|
+
bytesRemaining: 10485760,
|
|
95
|
+
totalBytes: 26214400,
|
|
96
|
+
downloadSpeed: 2097152,
|
|
97
|
+
eta: 5,
|
|
98
|
+
retryCount: 0,
|
|
99
|
+
currentGateway: 'https://huggingface.co/',
|
|
100
|
+
status: 'downloading',
|
|
101
|
+
percentComplete: 60,
|
|
102
|
+
completedFiles: 6,
|
|
103
|
+
totalFiles: 10
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
broadcastModelProgress({
|
|
108
|
+
type: 'stt',
|
|
109
|
+
started: true,
|
|
110
|
+
done: true,
|
|
111
|
+
downloading: false,
|
|
112
|
+
completedFiles: 10,
|
|
113
|
+
totalFiles: 10,
|
|
114
|
+
status: 'completed'
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
console.log('\n[Test] Broadcasting complete. Results:\n');
|
|
119
|
+
console.log(`Broadcasted messages: ${broadcastedMessages.length}`);
|
|
120
|
+
console.log(`Client connected: ${clientConnected}`);
|
|
121
|
+
|
|
122
|
+
console.log('\nMessage types:');
|
|
123
|
+
broadcastedMessages.forEach((msg, idx) => {
|
|
124
|
+
console.log(` [${idx + 1}] Type: ${msg.type}`);
|
|
125
|
+
console.log(` Status: ${msg.status}`);
|
|
126
|
+
console.log(` Complete: ${msg.percentComplete || msg.completedFiles}%`);
|
|
127
|
+
console.log(` Speed: ${msg.downloadSpeed ? (msg.downloadSpeed / 1024 / 1024).toFixed(2) + 'MB/s' : 'N/A'}`);
|
|
128
|
+
console.log(` ETA: ${msg.eta || 0}s`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const requiredFields = ['modelId', 'bytesDownloaded', 'bytesRemaining', 'downloadSpeed', 'eta', 'retryCount', 'currentGateway', 'status'];
|
|
132
|
+
const allFieldsPresent = broadcastedMessages.every(msg =>
|
|
133
|
+
requiredFields.every(field => field in msg)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
console.log(`\nAll required fields present: ${allFieldsPresent ? 'PASS' : 'FAIL'}`);
|
|
137
|
+
console.log(`Message count >= 3: ${broadcastedMessages.length >= 3 ? 'PASS' : 'FAIL'}`);
|
|
138
|
+
|
|
139
|
+
server.close(() => {
|
|
140
|
+
process.exit(allFieldsPresent && broadcastedMessages.length >= 3 ? 0 : 1);
|
|
141
|
+
});
|
|
142
|
+
}, 500);
|
|
143
|
+
}, 500);
|
|
144
|
+
}, 500);
|
|
145
|
+
}, 500);
|
|
146
|
+
}, 500);
|
|
147
|
+
});
|