agentic-flow 1.9.4 → 1.10.1
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 +246 -0
- package/dist/proxy/adaptive-proxy.js +224 -0
- package/dist/proxy/anthropic-to-gemini.js +2 -2
- package/dist/proxy/http2-proxy-optimized.js +191 -0
- package/dist/proxy/http2-proxy.js +381 -0
- package/dist/proxy/http3-proxy-old.js +331 -0
- package/dist/proxy/http3-proxy.js +51 -0
- package/dist/proxy/websocket-proxy.js +406 -0
- package/dist/utils/adaptive-pool-sizing.js +414 -0
- package/dist/utils/auth.js +52 -0
- package/dist/utils/circular-rate-limiter.js +391 -0
- package/dist/utils/compression-middleware.js +149 -0
- package/dist/utils/connection-pool.js +184 -0
- package/dist/utils/dynamic-compression.js +298 -0
- package/dist/utils/http2-multiplexing.js +319 -0
- package/dist/utils/lazy-auth.js +311 -0
- package/dist/utils/rate-limiter.js +48 -0
- package/dist/utils/response-cache.js +211 -0
- package/dist/utils/server-push.js +251 -0
- package/dist/utils/streaming-optimizer.js +141 -0
- package/dist/utils/zero-copy-buffer.js +286 -0
- package/docs/.claude-flow/metrics/performance.json +3 -3
- package/docs/.claude-flow/metrics/task-metrics.json +3 -3
- package/docs/DOCKER-VERIFICATION.md +207 -0
- package/docs/ISSUE-55-VALIDATION.md +171 -0
- package/docs/NPX_AGENTDB_SETUP.md +175 -0
- package/docs/OPTIMIZATIONS.md +460 -0
- package/docs/PHASE2-IMPLEMENTATION-SUMMARY.md +275 -0
- package/docs/PHASE2-PHASE3-COMPLETE-SUMMARY.md +453 -0
- package/docs/PHASE3-IMPLEMENTATION-SUMMARY.md +357 -0
- package/docs/PUBLISH_GUIDE.md +438 -0
- package/docs/README.md +217 -0
- package/docs/RELEASE-v1.10.0-COMPLETE.md +382 -0
- package/docs/archive/.agentdb-instructions.md +66 -0
- package/docs/archive/AGENT-BOOSTER-STATUS.md +292 -0
- package/docs/archive/CHANGELOG-v1.3.0.md +120 -0
- package/docs/archive/COMPLETION_REPORT_v1.7.1.md +335 -0
- package/docs/archive/IMPLEMENTATION_SUMMARY_v1.7.1.md +241 -0
- package/docs/archive/SUPABASE-INTEGRATION-COMPLETE.md +357 -0
- package/docs/archive/TESTING_QUICK_START.md +223 -0
- package/docs/archive/TOOL-EMULATION-INTEGRATION-ISSUE.md +669 -0
- package/docs/archive/VALIDATION_v1.7.1.md +234 -0
- package/docs/issues/ISSUE-xenova-transformers-dependency.md +380 -0
- package/docs/releases/PUBLISH_CHECKLIST_v1.10.0.md +396 -0
- package/docs/releases/PUBLISH_SUMMARY_v1.7.1.md +198 -0
- package/docs/releases/RELEASE_NOTES_v1.10.0.md +464 -0
- package/docs/releases/RELEASE_NOTES_v1.7.0.md +297 -0
- package/docs/releases/RELEASE_v1.7.1.md +327 -0
- package/package.json +1 -1
- package/scripts/claude +31 -0
- package/validation/docker-npm-validation.sh +170 -0
- package/validation/simple-npm-validation.sh +131 -0
- package/validation/test-gemini-exclusiveMinimum-fix.ts +142 -0
- package/validation/test-gemini-models.ts +200 -0
- package/validation/validate-v1.10.0-docker.sh +296 -0
- package/wasm/reasoningbank/reasoningbank_wasm_bg.js +2 -2
- package/wasm/reasoningbank/reasoningbank_wasm_bg.wasm +0 -0
- package/docs/INDEX.md +0 -279
- package/docs/guides/.claude-flow/metrics/agent-metrics.json +0 -1
- package/docs/guides/.claude-flow/metrics/performance.json +0 -9
- package/docs/guides/.claude-flow/metrics/task-metrics.json +0 -10
- package/docs/router/.claude-flow/metrics/agent-metrics.json +0 -1
- package/docs/router/.claude-flow/metrics/performance.json +0 -9
- package/docs/router/.claude-flow/metrics/task-metrics.json +0 -10
- /package/docs/{TEST-V1.7.8.Dockerfile → docker-tests/TEST-V1.7.8.Dockerfile} +0 -0
- /package/docs/{TEST-V1.7.9-NODE20.Dockerfile → docker-tests/TEST-V1.7.9-NODE20.Dockerfile} +0 -0
- /package/docs/{TEST-V1.7.9.Dockerfile → docker-tests/TEST-V1.7.9.Dockerfile} +0 -0
- /package/docs/{v1.7.1-QUICK-START.md → guides/QUICK-START-v1.7.1.md} +0 -0
- /package/docs/{INTEGRATION-COMPLETE.md → integration-docs/INTEGRATION-COMPLETE.md} +0 -0
- /package/docs/{LANDING-PAGE-PROVIDER-CONTENT.md → providers/LANDING-PAGE-PROVIDER-CONTENT.md} +0 -0
- /package/docs/{PROVIDER-FALLBACK-GUIDE.md → providers/PROVIDER-FALLBACK-GUIDE.md} +0 -0
- /package/docs/{PROVIDER-FALLBACK-SUMMARY.md → providers/PROVIDER-FALLBACK-SUMMARY.md} +0 -0
- /package/docs/{QUIC_FINAL_STATUS.md → quic/QUIC_FINAL_STATUS.md} +0 -0
- /package/docs/{README_QUIC_PHASE1.md → quic/README_QUIC_PHASE1.md} +0 -0
- /package/docs/{AGENTDB_TESTING.md → testing/AGENTDB_TESTING.md} +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Proxy for LLM Streaming
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Bidirectional: Full-duplex communication
|
|
6
|
+
* - Mobile-friendly: Better for unstable connections
|
|
7
|
+
* - Lower overhead: No HTTP headers per message
|
|
8
|
+
* - Reconnection: Automatic retry on disconnect
|
|
9
|
+
* - Universal support: Works everywhere (browsers, mobile, desktop)
|
|
10
|
+
*
|
|
11
|
+
* Use Case: Fallback for unreliable connections (mobile, poor WiFi)
|
|
12
|
+
*/
|
|
13
|
+
import { WebSocketServer } from 'ws';
|
|
14
|
+
import { createServer } from 'http';
|
|
15
|
+
import { logger } from '../utils/logger.js';
|
|
16
|
+
export class WebSocketProxy {
|
|
17
|
+
wss;
|
|
18
|
+
server;
|
|
19
|
+
config;
|
|
20
|
+
clients = new Map();
|
|
21
|
+
pingInterval;
|
|
22
|
+
activeConnections = 0;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.server = createServer();
|
|
26
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
27
|
+
this.setupHandlers();
|
|
28
|
+
this.setupHeartbeat();
|
|
29
|
+
logger.info('WebSocket proxy created', {
|
|
30
|
+
port: config.port,
|
|
31
|
+
pingInterval: config.pingInterval || 30000
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
setupHandlers() {
|
|
35
|
+
this.wss.on('connection', (ws, req) => {
|
|
36
|
+
const clientIp = req.socket.remoteAddress;
|
|
37
|
+
// Check connection limit (DoS protection)
|
|
38
|
+
const maxConnections = this.config.maxConnections || 1000;
|
|
39
|
+
if (this.activeConnections >= maxConnections) {
|
|
40
|
+
logger.warn('Connection limit reached', {
|
|
41
|
+
activeConnections: this.activeConnections,
|
|
42
|
+
maxConnections
|
|
43
|
+
});
|
|
44
|
+
ws.close(1008, 'Server at capacity');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.activeConnections++;
|
|
48
|
+
logger.info('WebSocket client connected', {
|
|
49
|
+
clientIp,
|
|
50
|
+
activeConnections: this.activeConnections
|
|
51
|
+
});
|
|
52
|
+
// Initialize client state
|
|
53
|
+
this.clients.set(ws, {
|
|
54
|
+
ws,
|
|
55
|
+
isAlive: true,
|
|
56
|
+
lastPing: new Date(),
|
|
57
|
+
requests: 0
|
|
58
|
+
});
|
|
59
|
+
// Handle incoming messages
|
|
60
|
+
ws.on('message', async (data) => {
|
|
61
|
+
const client = this.clients.get(ws);
|
|
62
|
+
if (!client)
|
|
63
|
+
return;
|
|
64
|
+
client.requests++;
|
|
65
|
+
try {
|
|
66
|
+
const message = JSON.parse(data.toString());
|
|
67
|
+
logger.debug('WebSocket message received', { type: message.type });
|
|
68
|
+
if (message.type === 'ping') {
|
|
69
|
+
// Respond to ping
|
|
70
|
+
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
|
|
71
|
+
client.isAlive = true;
|
|
72
|
+
client.lastPing = new Date();
|
|
73
|
+
}
|
|
74
|
+
else if (message.type === 'streaming_request') {
|
|
75
|
+
// Handle LLM streaming request
|
|
76
|
+
await this.handleStreamingRequest(ws, message.data);
|
|
77
|
+
}
|
|
78
|
+
else if (message.type === 'non_streaming_request') {
|
|
79
|
+
// Handle non-streaming request
|
|
80
|
+
await this.handleNonStreamingRequest(ws, message.data);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
ws.send(JSON.stringify({
|
|
84
|
+
type: 'error',
|
|
85
|
+
error: `Unknown message type: ${message.type}`
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.error('WebSocket message error', { error: error.message });
|
|
91
|
+
ws.send(JSON.stringify({
|
|
92
|
+
type: 'error',
|
|
93
|
+
error: error.message
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Handle pong responses
|
|
98
|
+
ws.on('pong', () => {
|
|
99
|
+
const client = this.clients.get(ws);
|
|
100
|
+
if (client) {
|
|
101
|
+
client.isAlive = true;
|
|
102
|
+
client.lastPing = new Date();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Set connection timeout
|
|
106
|
+
const connectionTimeout = this.config.connectionTimeout || 300000; // 5 minutes
|
|
107
|
+
const timeoutHandle = setTimeout(() => {
|
|
108
|
+
logger.warn('Connection timeout', { clientIp });
|
|
109
|
+
ws.close(1000, 'Connection timeout');
|
|
110
|
+
}, connectionTimeout);
|
|
111
|
+
// Handle close
|
|
112
|
+
ws.on('close', () => {
|
|
113
|
+
logger.info('WebSocket client disconnected', { clientIp });
|
|
114
|
+
this.clients.delete(ws);
|
|
115
|
+
this.activeConnections--;
|
|
116
|
+
clearTimeout(timeoutHandle);
|
|
117
|
+
});
|
|
118
|
+
// Handle errors
|
|
119
|
+
ws.on('error', (error) => {
|
|
120
|
+
logger.error('WebSocket error', { clientIp, error: error.message });
|
|
121
|
+
this.clients.delete(ws);
|
|
122
|
+
this.activeConnections--;
|
|
123
|
+
});
|
|
124
|
+
// Send initial handshake
|
|
125
|
+
ws.send(JSON.stringify({
|
|
126
|
+
type: 'connected',
|
|
127
|
+
protocols: ['anthropic-messages-v1'],
|
|
128
|
+
features: ['streaming', 'non-streaming', 'ping-pong']
|
|
129
|
+
}));
|
|
130
|
+
});
|
|
131
|
+
this.wss.on('error', (error) => {
|
|
132
|
+
logger.error('WebSocket server error', { error: error.message });
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
setupHeartbeat() {
|
|
136
|
+
const interval = this.config.pingInterval || 30000; // 30 seconds
|
|
137
|
+
const timeout = this.config.pingTimeout || 60000; // 60 seconds
|
|
138
|
+
this.pingInterval = setInterval(() => {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
this.clients.forEach((client, ws) => {
|
|
141
|
+
// Check if client responded to last ping
|
|
142
|
+
if (!client.isAlive) {
|
|
143
|
+
const timeSinceLastPing = now - client.lastPing.getTime();
|
|
144
|
+
if (timeSinceLastPing > timeout) {
|
|
145
|
+
logger.warn('Client did not respond to ping, terminating', {
|
|
146
|
+
timeSinceLastPing
|
|
147
|
+
});
|
|
148
|
+
ws.terminate();
|
|
149
|
+
this.clients.delete(ws);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Send ping
|
|
154
|
+
client.isAlive = false;
|
|
155
|
+
ws.ping();
|
|
156
|
+
});
|
|
157
|
+
}, interval);
|
|
158
|
+
}
|
|
159
|
+
async handleStreamingRequest(ws, anthropicReq) {
|
|
160
|
+
try {
|
|
161
|
+
logger.info('WebSocket streaming request', {
|
|
162
|
+
model: anthropicReq.model,
|
|
163
|
+
messageCount: anthropicReq.messages?.length
|
|
164
|
+
});
|
|
165
|
+
// Convert to Gemini format
|
|
166
|
+
const geminiReq = this.convertAnthropicToGemini(anthropicReq);
|
|
167
|
+
// Send streaming start event
|
|
168
|
+
ws.send(JSON.stringify({
|
|
169
|
+
type: 'message_start',
|
|
170
|
+
message: {
|
|
171
|
+
id: `msg_${Date.now()}`,
|
|
172
|
+
role: 'assistant',
|
|
173
|
+
model: anthropicReq.model || 'gemini-2.0-flash-exp'
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
176
|
+
// Forward to Gemini
|
|
177
|
+
const geminiBaseUrl = this.config.geminiBaseUrl || 'https://generativelanguage.googleapis.com/v1beta';
|
|
178
|
+
const url = `${geminiBaseUrl}/models/gemini-2.0-flash-exp:streamGenerateContent?key=${this.config.geminiApiKey}&alt=sse`;
|
|
179
|
+
const response = await fetch(url, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify(geminiReq)
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
const error = await response.text();
|
|
186
|
+
throw new Error(`Gemini API error: ${error}`);
|
|
187
|
+
}
|
|
188
|
+
const reader = response.body?.getReader();
|
|
189
|
+
if (!reader) {
|
|
190
|
+
throw new Error('No response body');
|
|
191
|
+
}
|
|
192
|
+
const decoder = new TextDecoder();
|
|
193
|
+
let chunkCount = 0;
|
|
194
|
+
while (true) {
|
|
195
|
+
const { done, value } = await reader.read();
|
|
196
|
+
if (done)
|
|
197
|
+
break;
|
|
198
|
+
const chunk = decoder.decode(value);
|
|
199
|
+
chunkCount++;
|
|
200
|
+
// Parse Gemini SSE and send as WebSocket messages
|
|
201
|
+
const lines = chunk.split('\n').filter(line => line.trim());
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
if (line.startsWith('data: ')) {
|
|
204
|
+
try {
|
|
205
|
+
const jsonStr = line.substring(6);
|
|
206
|
+
const parsed = JSON.parse(jsonStr);
|
|
207
|
+
const candidate = parsed.candidates?.[0];
|
|
208
|
+
const text = candidate?.content?.parts?.[0]?.text;
|
|
209
|
+
if (text) {
|
|
210
|
+
// Send text delta
|
|
211
|
+
ws.send(JSON.stringify({
|
|
212
|
+
type: 'content_block_delta',
|
|
213
|
+
delta: { type: 'text_delta', text }
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
if (candidate?.finishReason) {
|
|
217
|
+
// Send completion
|
|
218
|
+
ws.send(JSON.stringify({
|
|
219
|
+
type: 'message_stop',
|
|
220
|
+
stop_reason: 'end_turn'
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
logger.debug('Failed to parse stream chunk', { line });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
logger.info('WebSocket stream complete', { totalChunks: chunkCount });
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
logger.error('WebSocket streaming error', { error: error.message });
|
|
234
|
+
ws.send(JSON.stringify({
|
|
235
|
+
type: 'error',
|
|
236
|
+
error: error.message
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async handleNonStreamingRequest(ws, anthropicReq) {
|
|
241
|
+
try {
|
|
242
|
+
logger.info('WebSocket non-streaming request', {
|
|
243
|
+
model: anthropicReq.model,
|
|
244
|
+
messageCount: anthropicReq.messages?.length
|
|
245
|
+
});
|
|
246
|
+
// Convert to Gemini format
|
|
247
|
+
const geminiReq = this.convertAnthropicToGemini(anthropicReq);
|
|
248
|
+
// Forward to Gemini
|
|
249
|
+
const geminiBaseUrl = this.config.geminiBaseUrl || 'https://generativelanguage.googleapis.com/v1beta';
|
|
250
|
+
const url = `${geminiBaseUrl}/models/gemini-2.0-flash-exp:generateContent?key=${this.config.geminiApiKey}`;
|
|
251
|
+
const response = await fetch(url, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { 'Content-Type': 'application/json' },
|
|
254
|
+
body: JSON.stringify(geminiReq)
|
|
255
|
+
});
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
const error = await response.text();
|
|
258
|
+
throw new Error(`Gemini API error: ${error}`);
|
|
259
|
+
}
|
|
260
|
+
const geminiRes = await response.json();
|
|
261
|
+
const anthropicRes = this.convertGeminiToAnthropic(geminiRes);
|
|
262
|
+
// Send complete response
|
|
263
|
+
ws.send(JSON.stringify({
|
|
264
|
+
type: 'message_complete',
|
|
265
|
+
message: anthropicRes
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
logger.error('WebSocket non-streaming error', { error: error.message });
|
|
270
|
+
ws.send(JSON.stringify({
|
|
271
|
+
type: 'error',
|
|
272
|
+
error: error.message
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
convertAnthropicToGemini(anthropicReq) {
|
|
277
|
+
const contents = [];
|
|
278
|
+
let systemPrefix = '';
|
|
279
|
+
if (anthropicReq.system) {
|
|
280
|
+
systemPrefix = `System: ${anthropicReq.system}\n\n`;
|
|
281
|
+
}
|
|
282
|
+
for (let i = 0; i < anthropicReq.messages.length; i++) {
|
|
283
|
+
const msg = anthropicReq.messages[i];
|
|
284
|
+
let text;
|
|
285
|
+
if (typeof msg.content === 'string') {
|
|
286
|
+
text = msg.content;
|
|
287
|
+
}
|
|
288
|
+
else if (Array.isArray(msg.content)) {
|
|
289
|
+
text = msg.content
|
|
290
|
+
.filter((block) => block.type === 'text')
|
|
291
|
+
.map((block) => block.text)
|
|
292
|
+
.join('\n');
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
text = '';
|
|
296
|
+
}
|
|
297
|
+
if (i === 0 && msg.role === 'user' && systemPrefix) {
|
|
298
|
+
text = systemPrefix + text;
|
|
299
|
+
}
|
|
300
|
+
contents.push({
|
|
301
|
+
role: msg.role === 'assistant' ? 'model' : 'user',
|
|
302
|
+
parts: [{ text }]
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const geminiReq = { contents };
|
|
306
|
+
if (anthropicReq.temperature !== undefined || anthropicReq.max_tokens !== undefined) {
|
|
307
|
+
geminiReq.generationConfig = {};
|
|
308
|
+
if (anthropicReq.temperature !== undefined) {
|
|
309
|
+
geminiReq.generationConfig.temperature = anthropicReq.temperature;
|
|
310
|
+
}
|
|
311
|
+
if (anthropicReq.max_tokens !== undefined) {
|
|
312
|
+
geminiReq.generationConfig.maxOutputTokens = anthropicReq.max_tokens;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return geminiReq;
|
|
316
|
+
}
|
|
317
|
+
convertGeminiToAnthropic(geminiRes) {
|
|
318
|
+
const candidate = geminiRes.candidates?.[0];
|
|
319
|
+
if (!candidate) {
|
|
320
|
+
throw new Error('No candidates in Gemini response');
|
|
321
|
+
}
|
|
322
|
+
const content = candidate.content;
|
|
323
|
+
const parts = content?.parts || [];
|
|
324
|
+
let rawText = '';
|
|
325
|
+
for (const part of parts) {
|
|
326
|
+
if (part.text) {
|
|
327
|
+
rawText += part.text;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
id: `msg_${Date.now()}`,
|
|
332
|
+
type: 'message',
|
|
333
|
+
role: 'assistant',
|
|
334
|
+
model: 'gemini-2.0-flash-exp',
|
|
335
|
+
content: [
|
|
336
|
+
{
|
|
337
|
+
type: 'text',
|
|
338
|
+
text: rawText
|
|
339
|
+
}
|
|
340
|
+
],
|
|
341
|
+
stop_reason: 'end_turn',
|
|
342
|
+
usage: {
|
|
343
|
+
input_tokens: geminiRes.usageMetadata?.promptTokenCount || 0,
|
|
344
|
+
output_tokens: geminiRes.usageMetadata?.candidatesTokenCount || 0
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
start() {
|
|
349
|
+
return new Promise((resolve) => {
|
|
350
|
+
this.server.listen(this.config.port, () => {
|
|
351
|
+
logger.info('WebSocket proxy started', {
|
|
352
|
+
port: this.config.port,
|
|
353
|
+
url: `ws://localhost:${this.config.port}`
|
|
354
|
+
});
|
|
355
|
+
console.log(`\n✅ WebSocket Proxy running at ws://localhost:${this.config.port}`);
|
|
356
|
+
console.log(` Protocol: WebSocket (fallback for unreliable connections)`);
|
|
357
|
+
console.log(` Features: Bidirectional, Mobile-friendly, Auto-reconnect\n`);
|
|
358
|
+
resolve();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
stop() {
|
|
363
|
+
return new Promise((resolve) => {
|
|
364
|
+
// Clear ping interval
|
|
365
|
+
if (this.pingInterval) {
|
|
366
|
+
clearInterval(this.pingInterval);
|
|
367
|
+
}
|
|
368
|
+
// Close all client connections
|
|
369
|
+
this.clients.forEach((client, ws) => {
|
|
370
|
+
ws.close(1000, 'Server shutting down');
|
|
371
|
+
});
|
|
372
|
+
this.clients.clear();
|
|
373
|
+
// Close WebSocket server
|
|
374
|
+
this.wss.close(() => {
|
|
375
|
+
// Close HTTP server
|
|
376
|
+
this.server.close(() => {
|
|
377
|
+
logger.info('WebSocket proxy stopped');
|
|
378
|
+
resolve();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// CLI entry point
|
|
385
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
386
|
+
const port = parseInt(process.env.PORT || '8080');
|
|
387
|
+
const geminiApiKey = process.env.GOOGLE_GEMINI_API_KEY;
|
|
388
|
+
if (!geminiApiKey) {
|
|
389
|
+
console.error('❌ Error: GOOGLE_GEMINI_API_KEY environment variable required');
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
const proxy = new WebSocketProxy({
|
|
393
|
+
port,
|
|
394
|
+
geminiApiKey,
|
|
395
|
+
geminiBaseUrl: process.env.GEMINI_BASE_URL,
|
|
396
|
+
pingInterval: 30000,
|
|
397
|
+
pingTimeout: 60000
|
|
398
|
+
});
|
|
399
|
+
proxy.start();
|
|
400
|
+
// Graceful shutdown
|
|
401
|
+
process.on('SIGINT', async () => {
|
|
402
|
+
console.log('\n🛑 Shutting down WebSocket proxy...');
|
|
403
|
+
await proxy.stop();
|
|
404
|
+
process.exit(0);
|
|
405
|
+
});
|
|
406
|
+
}
|