cdp-tunnel 2.5.21 → 2.6.0

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.
@@ -18,10 +18,23 @@ const { execSync, spawn: spawnProcess } = require('child_process');
18
18
  const { CONFIG, BROWSER_ID, shouldLog } = require('./modules/config');
19
19
  const { logCDP, logEvent, clearLog, logStatus, logConnectionEvent, flushAllLogs, logDisconnect } = require('./modules/logger');
20
20
 
21
+ try {
22
+ const { validateApiKey } = require('./saas/auth');
23
+ var HAS_SAAS = true;
24
+ } catch (e) {
25
+ var HAS_SAAS = false;
26
+ }
27
+
21
28
  const PORT = CONFIG.PORT;
22
29
  const CONFIG_DIR = path.join(os.homedir(), '.cdp-tunnel');
23
- const EXTENSION_STATE_FILE = path.join(CONFIG_DIR, 'extension-state.json');
24
- const PLUGIN_EVER_CONNECTED_FILE = path.join(CONFIG_DIR, 'plugin-ever-connected');
30
+ const INSTANCE_DIR = path.join(CONFIG_DIR, 'instances', PORT.toString());
31
+
32
+ if (!fs.existsSync(INSTANCE_DIR)) {
33
+ fs.mkdirSync(INSTANCE_DIR, { recursive: true });
34
+ }
35
+
36
+ const EXTENSION_STATE_FILE = path.join(INSTANCE_DIR, 'extension-state.json');
37
+ const PLUGIN_EVER_CONNECTED_FILE = path.join(INSTANCE_DIR, 'plugin-ever-connected');
25
38
  const SERVER_START_TIME = Date.now();
26
39
 
27
40
  let lastChromeRestartAttempt = 0;
@@ -142,25 +155,39 @@ const server = http.createServer((req, res) => handleHttpRequest(req, res));
142
155
  const pluginConnections = new Set();
143
156
  const clientConnections = new Set();
144
157
 
158
+ class PluginNamespace {
159
+ constructor() {
160
+ this.sessionToClientId = new Map();
161
+ this.pendingAttachRequests = new Map();
162
+ this.pendingAttachedEvents = new Map();
163
+ this.pendingTargetCreatedEvents = new Map();
164
+ this.targetIdToClientId = new Map();
165
+ this.browserContextToClientId = new Map();
166
+ this.clientIdToBrowserContext = new Map();
167
+ this.cachedTargets = [];
168
+ this.lastTargetsUpdate = 0;
169
+ this.cachedBrowserVersion = null;
170
+ this.discoveringClientIds = new Map();
171
+ }
172
+ }
173
+
174
+ const pluginNamespaces = new Map();
175
+
176
+ function getNamespace(pluginWs) {
177
+ if (!pluginNamespaces.has(pluginWs)) {
178
+ pluginNamespaces.set(pluginWs, new PluginNamespace());
179
+ }
180
+ return pluginNamespaces.get(pluginWs);
181
+ }
182
+
145
183
  const connectionPairs = new Map();
146
184
  const clientById = new Map();
147
- const sessionToClientId = new Map();
148
- const pendingAttachRequests = new Map();
149
185
  const clientIdToPlugin = new Map();
150
186
  const globalRequestIdMap = new Map();
151
- const targetIdToClientId = new Map();
152
- const pendingAttachedEvents = new Map();
153
- const pendingTargetCreatedEvents = new Map();
154
- const browserContextToClientId = new Map();
155
- const clientIdToBrowserContext = new Map();
156
187
  let globalRequestIdCounter = 0;
157
188
 
158
189
  const { version: PKG_VERSION } = require('../package.json');
159
190
 
160
- let cachedTargets = [];
161
- let lastTargetsUpdate = 0;
162
- let cachedBrowserVersion = null;
163
-
164
191
  console.log('='.repeat(60));
165
192
  console.log(` WebSocket CDP Proxy Server v${PKG_VERSION}`);
166
193
  console.log('='.repeat(60));
@@ -177,26 +204,24 @@ function getHost(req) {
177
204
  return req.headers.host || `localhost:${PORT}`;
178
205
  }
179
206
 
180
- /**
181
- * 生成 WebSocket 调试地址
182
- */
183
- function buildWebSocketDebuggerUrl(req) {
184
- return `ws://${getHost(req)}/devtools/browser/${BROWSER_ID}`;
185
- }
186
-
187
- function buildTargetWebSocketUrl(req, targetId) {
188
- return `ws://${getHost(req)}/devtools/page/${targetId}`;
207
+ function invalidateTargetsCache(pluginWs) {
208
+ if (pluginWs) {
209
+ getNamespace(pluginWs).lastTargetsUpdate = 0;
210
+ } else {
211
+ pluginNamespaces.forEach(ns => { ns.lastTargetsUpdate = 0; });
212
+ }
189
213
  }
190
214
 
191
- function invalidateTargetsCache() {
192
- lastTargetsUpdate = 0;
193
- }
215
+ async function requestVersionFromPlugin(pluginWs) {
216
+ if (!pluginWs) {
217
+ pluginWs = pluginConnections.values().next().value;
218
+ }
219
+ if (!pluginWs) return null;
194
220
 
195
- async function requestVersionFromPlugin() {
196
- if (cachedBrowserVersion) return cachedBrowserVersion;
221
+ const ns = getNamespace(pluginWs);
222
+ if (ns.cachedBrowserVersion) return ns.cachedBrowserVersion;
197
223
 
198
- const plugin = pluginConnections.values().next().value;
199
- if (!plugin || plugin.readyState !== WebSocket.OPEN) {
224
+ if (pluginWs.readyState !== WebSocket.OPEN) {
200
225
  return null;
201
226
  }
202
227
 
@@ -209,35 +234,40 @@ async function requestVersionFromPlugin() {
209
234
  const msg = JSON.parse(data.toString());
210
235
  if (msg.id === requestId && msg.result) {
211
236
  clearTimeout(timeout);
212
- plugin.off('message', handler);
237
+ pluginWs.off('message', handler);
213
238
  if (msg.result.product || msg.result.userAgent) {
214
- cachedBrowserVersion = msg.result;
239
+ ns.cachedBrowserVersion = msg.result;
215
240
  }
216
- resolve(cachedBrowserVersion || msg.result);
241
+ resolve(ns.cachedBrowserVersion || msg.result);
217
242
  }
218
243
  } catch (e) {}
219
244
  };
220
245
 
221
- plugin.on('message', handler);
222
- plugin.send(JSON.stringify({ id: requestId, method: 'Browser.getVersion' }));
246
+ pluginWs.on('message', handler);
247
+ pluginWs.send(JSON.stringify({ id: requestId, method: 'Browser.getVersion' }));
223
248
  });
224
249
  }
225
250
 
226
- async function requestTargetsFromPlugin() {
251
+ async function requestTargetsFromPlugin(pluginWs) {
252
+ if (!pluginWs) {
253
+ pluginWs = pluginConnections.values().next().value;
254
+ }
255
+ if (!pluginWs) return [];
256
+
257
+ const ns = getNamespace(pluginWs);
227
258
  const now = Date.now();
228
- if (now - lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && cachedTargets.length > 0) {
229
- return cachedTargets;
259
+ if (now - ns.lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && ns.cachedTargets.length > 0) {
260
+ return ns.cachedTargets;
230
261
  }
231
262
 
232
- const plugin = pluginConnections.values().next().value;
233
- if (!plugin || plugin.readyState !== WebSocket.OPEN) {
234
- return cachedTargets;
263
+ if (pluginWs.readyState !== WebSocket.OPEN) {
264
+ return ns.cachedTargets;
235
265
  }
236
266
 
237
267
  return new Promise((resolve) => {
238
268
  const requestId = `targets_${Date.now()}`;
239
269
  const timeout = setTimeout(() => {
240
- resolve(cachedTargets);
270
+ resolve(ns.cachedTargets);
241
271
  }, CONFIG.TARGETS_REQUEST_TIMEOUT);
242
272
 
243
273
  const handler = (data) => {
@@ -245,36 +275,79 @@ async function requestTargetsFromPlugin() {
245
275
  const msg = JSON.parse(data.toString());
246
276
  if (msg.id === requestId && msg.result?.targetInfos) {
247
277
  clearTimeout(timeout);
248
- plugin.off('message', handler);
249
- cachedTargets = msg.result.targetInfos;
250
- lastTargetsUpdate = now;
251
- resolve(cachedTargets);
278
+ pluginWs.off('message', handler);
279
+ ns.cachedTargets = msg.result.targetInfos;
280
+ ns.lastTargetsUpdate = now;
281
+ resolve(ns.cachedTargets);
252
282
  }
253
283
  } catch (e) {}
254
284
  };
255
285
 
256
- plugin.on('message', handler);
257
- plugin.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
286
+ pluginWs.on('message', handler);
287
+ pluginWs.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
258
288
  });
259
289
  }
260
290
 
291
+ /**
292
+ * 生成指定 plugin 的 browser WS URL
293
+ */
294
+ function buildBrowserWsUrl(pluginId) {
295
+ const host = CONFIG.EXTERNAL_HOST || `localhost:${PORT}`;
296
+ return `ws://${host}/devtools/browser/${pluginId}`;
297
+ }
298
+
261
299
  /**
262
300
  * 处理 HTTP 请求
263
301
  */
302
+ function resolvePluginFromUrl(url) {
303
+ const parts = url.pathname.split('/').filter(Boolean);
304
+ if (parts.length >= 3) {
305
+ const pluginId = parts[2];
306
+ for (const pluginWs of pluginConnections) {
307
+ if (pluginWs.pluginId === pluginId) return pluginWs;
308
+ }
309
+ }
310
+ return pluginConnections.values().next().value || null;
311
+ }
312
+
264
313
  async function handleHttpRequest(req, res) {
265
314
  const url = new URL(req.url, `http://localhost:${PORT}`);
266
315
 
267
- if (url.pathname === '/json/version' || url.pathname === '/json/version/') {
268
- const ver = await requestVersionFromPlugin();
316
+ if (url.pathname === '/json/browsers' || url.pathname === '/json/browsers/') {
317
+ const browsers = [];
318
+ for (const pluginWs of pluginConnections) {
319
+ if (pluginWs.readyState !== WebSocket.OPEN) continue;
320
+ const ns = getNamespace(pluginWs);
321
+ browsers.push({
322
+ pluginId: pluginWs.pluginId,
323
+ pluginName: pluginWs.pluginName || 'My Browser',
324
+ userId: pluginWs.userId || null,
325
+ browserName: ns.cachedBrowserVersion?.Browser || 'Unknown',
326
+ targets: ns.cachedTargets.length,
327
+ connected: true,
328
+ connectedAt: pluginWs.connectedAt,
329
+ webSocketDebuggerUrl: buildBrowserWsUrl(pluginWs.pluginId)
330
+ });
331
+ }
332
+ res.writeHead(200, { 'Content-Type': 'application/json' });
333
+ res.end(JSON.stringify(browsers));
334
+ return;
335
+ }
336
+
337
+ if (url.pathname === '/json/version' || url.pathname === '/json/version/' ||
338
+ url.pathname.match(/^\/json\/version\/[^/]+$/)) {
339
+ const pluginWs = resolvePluginFromUrl(url);
340
+ const ver = await requestVersionFromPlugin(pluginWs);
269
341
  const userAgent = ver?.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36';
270
342
  const product = ver?.product || 'Chrome/131.0.6778.86';
343
+ const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
271
344
  const payload = {
272
345
  Browser: `${product} (cdp-tunnel/${PKG_VERSION})`,
273
346
  'Protocol-Version': ver?.protocolVersion || '1.3',
274
347
  'User-Agent': userAgent,
275
348
  'V8-Version': ver?.jsVersion || '',
276
349
  'WebKit-Version': '537.36',
277
- webSocketDebuggerUrl: buildWebSocketDebuggerUrl(req)
350
+ webSocketDebuggerUrl: `ws://${getHost(req)}/devtools/browser/${browserId}`
278
351
  };
279
352
  res.writeHead(200, { 'Content-Type': 'application/json' });
280
353
  res.end(JSON.stringify(payload));
@@ -282,16 +355,19 @@ async function handleHttpRequest(req, res) {
282
355
  }
283
356
 
284
357
  if (url.pathname === '/json' || url.pathname === '/json/' ||
285
- url.pathname === '/json/list' || url.pathname === '/json/list/') {
286
- const targets = await requestTargetsFromPlugin();
358
+ url.pathname === '/json/list' || url.pathname === '/json/list/' ||
359
+ url.pathname.match(/^\/json\/list\/[^/]+$/)) {
360
+ const pluginWs = resolvePluginFromUrl(url);
361
+ const targets = await requestTargetsFromPlugin(pluginWs);
362
+ const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
287
363
  const targetList = targets
288
364
  .filter(t => {
289
365
  if (t.type !== 'page') return false;
290
- const url = t.url || '';
291
- if (url.startsWith('chrome://') ||
292
- url.startsWith('chrome-extension://') ||
293
- url.startsWith('devtools://') ||
294
- url.startsWith('edge://')) {
366
+ const tUrl = t.url || '';
367
+ if (tUrl.startsWith('chrome://') ||
368
+ tUrl.startsWith('chrome-extension://') ||
369
+ tUrl.startsWith('devtools://') ||
370
+ tUrl.startsWith('edge://')) {
295
371
  return false;
296
372
  }
297
373
  return true;
@@ -305,7 +381,7 @@ async function handleHttpRequest(req, res) {
305
381
  title: t.title || '',
306
382
  type: t.type,
307
383
  url: t.url || '',
308
- webSocketDebuggerUrl: buildTargetWebSocketUrl(req, t.targetId)
384
+ webSocketDebuggerUrl: `ws://${getHost(req)}/devtools/page/${t.targetId}`
309
385
  }));
310
386
  res.writeHead(200, { 'Content-Type': 'application/json' });
311
387
  res.end(JSON.stringify(targetList));
@@ -322,8 +398,10 @@ async function handleHttpRequest(req, res) {
322
398
  server.on('upgrade', (req, socket, head) => {
323
399
  const url = new URL(req.url, `http://localhost:${PORT}`);
324
400
  const path = url.pathname;
401
+ const pathParts = path.split('/').filter(Boolean);
325
402
  const isPlugin = path === '/plugin';
326
403
  const isClient = path === '/client' ||
404
+ path.startsWith('/client/') ||
327
405
  path.startsWith('/client-') ||
328
406
  path.startsWith('/devtools/browser/') ||
329
407
  path.startsWith('/devtools/page/');
@@ -341,6 +419,7 @@ server.on('upgrade', (req, socket, head) => {
341
419
  wss.on('connection', (ws, req) => {
342
420
  const url = new URL(req.url, `http://localhost:${PORT}`);
343
421
  const path = url.pathname;
422
+ const pathParts = path.split('/').filter(Boolean);
344
423
 
345
424
  const clientInfo = {
346
425
  ip: req.socket.remoteAddress,
@@ -348,10 +427,16 @@ wss.on('connection', (ws, req) => {
348
427
  };
349
428
 
350
429
  if (path === '/plugin') {
351
- handlePluginConnection(ws, clientInfo);
352
- } else if (path === '/client' || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
430
+ handlePluginConnection(ws, clientInfo, req);
431
+ } else if (path === '/client' || path.startsWith('/client/') || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
353
432
  const customClientId = path.startsWith('/client-') ? path.replace('/client-', '') : null;
354
- handleClientConnection(ws, clientInfo, customClientId);
433
+ let targetPluginId = null;
434
+ if (pathParts[0] === 'client' && pathParts[1]) {
435
+ targetPluginId = pathParts[1];
436
+ } else if (pathParts[0] === 'devtools' && pathParts[1] === 'browser' && pathParts[2]) {
437
+ targetPluginId = pathParts[2];
438
+ }
439
+ handleClientConnection(ws, clientInfo, customClientId, targetPluginId);
355
440
  } else if (path.startsWith('/devtools/page/')) {
356
441
  const targetId = path.replace('/devtools/page/', '');
357
442
  handlePageConnection(ws, clientInfo, targetId);
@@ -362,21 +447,26 @@ wss.on('connection', (ws, req) => {
362
447
  });
363
448
 
364
449
  function cleanupClient(ws, id, reason) {
365
- const sessionsToClean = [];
366
- for (const [sessionId, clientId] of sessionToClientId.entries()) {
367
- if (clientId === id) {
368
- sessionsToClean.push(sessionId);
369
- sessionToClientId.delete(sessionId);
450
+ const pluginWs = ws.pairedPlugin || clientIdToPlugin.get(id);
451
+ const ns = pluginWs ? getNamespace(pluginWs) : null;
452
+
453
+ if (ns) {
454
+ const sessionsToClean = [];
455
+ for (const [sessionId, clientId] of ns.sessionToClientId.entries()) {
456
+ if (clientId === id) {
457
+ sessionsToClean.push(sessionId);
458
+ ns.sessionToClientId.delete(sessionId);
459
+ }
370
460
  }
371
461
  }
372
462
 
373
463
  clientConnections.delete(ws);
374
464
  clientById.delete(id);
465
+ clientIdToPlugin.delete(id);
375
466
 
376
467
  logConnectionEvent('CLIENT_DISCONNECTED', {
377
468
  id,
378
469
  reason,
379
- sessionsCleaned: sessionsToClean.length,
380
470
  totalPlugins: pluginConnections.size,
381
471
  totalClients: clientConnections.size
382
472
  });
@@ -384,7 +474,6 @@ function cleanupClient(ws, id, reason) {
384
474
  logDisconnect('CLIENT_CLEANUP', {
385
475
  clientId: id,
386
476
  reason,
387
- sessionsLost: sessionsToClean.length,
388
477
  cdpMethodsUsed: ws.cdpTrace ? [...new Set(ws.cdpTrace)] : [],
389
478
  uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
390
479
  remainingClients: clientConnections.size,
@@ -401,20 +490,23 @@ function cleanupClient(ws, id, reason) {
401
490
  safeSend(ws.pairedPlugin, JSON.stringify({
402
491
  type: 'client-disconnected',
403
492
  clientId: id,
404
- sessions: sessionsToClean
493
+ sessions: []
405
494
  }), 'plugin');
406
495
  }
407
496
 
408
497
  broadcastClientList();
409
498
 
410
- for (const [tId, cId] of targetIdToClientId.entries()) {
411
- if (cId === id) targetIdToClientId.delete(tId);
412
- }
413
- for (const [bcId, cId] of browserContextToClientId.entries()) {
414
- if (cId === id) browserContextToClientId.delete(bcId);
415
- }
416
- if (clientIdToBrowserContext.has(id)) {
417
- clientIdToBrowserContext.delete(id);
499
+ if (ns) {
500
+ for (const [tId, cId] of ns.targetIdToClientId.entries()) {
501
+ if (cId === id) ns.targetIdToClientId.delete(tId);
502
+ }
503
+ for (const [bcId, cId] of ns.browserContextToClientId.entries()) {
504
+ if (cId === id) ns.browserContextToClientId.delete(bcId);
505
+ }
506
+ if (ns.clientIdToBrowserContext.has(id)) {
507
+ ns.clientIdToBrowserContext.delete(id);
508
+ }
509
+ ns.discoveringClientIds.delete(id);
418
510
  }
419
511
  for (const [gId, mapping] of globalRequestIdMap.entries()) {
420
512
  if (mapping.clientId === id) globalRequestIdMap.delete(gId);
@@ -446,7 +538,9 @@ function sendPendingRequestErrors(pluginWs) {
446
538
  }
447
539
 
448
540
  function cleanupPlugin(ws, id, reason) {
541
+ const ns = getNamespace(ws);
449
542
  pluginConnections.delete(ws);
543
+ pluginNamespaces.delete(ws);
450
544
 
451
545
  if (pluginConnections.size === 0) {
452
546
  updateExtensionState(false);
@@ -478,8 +572,8 @@ function cleanupPlugin(ws, id, reason) {
478
572
  remainingPlugins: pluginConnections.size,
479
573
  affectedClients,
480
574
  uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
481
- activeSessions: sessionToClientId.size,
482
- pendingRequests: pendingAttachRequests.size
575
+ activeSessions: ns.sessionToClientId.size,
576
+ pendingRequests: ns.pendingAttachRequests.size
483
577
  });
484
578
 
485
579
  if (ws.pairedClientId) {
@@ -490,35 +584,46 @@ function cleanupPlugin(ws, id, reason) {
490
584
  /**
491
585
  * 处理 Chrome 扩展连接
492
586
  */
493
- function handlePluginConnection(ws, clientInfo) {
587
+ function handlePluginConnection(ws, clientInfo, request) {
588
+ const req = request;
494
589
  const id = generateId('plugin');
495
590
 
496
- if (pluginConnections.size > 0) {
497
- const toRemove = [];
498
- pluginConnections.forEach(oldWs => {
499
- if (oldWs !== ws) {
500
- if (oldWs.readyState === WebSocket.OPEN) {
501
- oldWs.send(JSON.stringify({ type: 'server-restart' }));
502
- oldWs.close(1001, 'Server restarted');
591
+ ws.pluginId = 'browser_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
592
+
593
+ try {
594
+ const url = new URL(req.url, `http://localhost`);
595
+ const apiKey = url.searchParams.get('key');
596
+ const desiredPluginId = url.searchParams.get('pluginId');
597
+
598
+ if (desiredPluginId && /^browser_[a-zA-Z0-9_]+$/.test(desiredPluginId)) {
599
+ let conflict = false;
600
+ for (const existing of pluginConnections) {
601
+ if (existing.pluginId === desiredPluginId && existing !== ws) {
602
+ conflict = true;
603
+ break;
503
604
  }
504
- toRemove.push(oldWs);
505
605
  }
506
- });
507
- toRemove.forEach(oldWs => {
508
- pluginConnections.delete(oldWs);
509
- if (shouldLog('info')) {
510
- console.log(`[PLUGIN] Removed old connection: ${oldWs.id}`);
606
+ if (!conflict) {
607
+ ws.pluginId = desiredPluginId;
511
608
  }
512
- });
609
+ }
610
+
611
+ if (HAS_SAAS && apiKey) {
612
+ const keyInfo = validateApiKey(apiKey);
613
+ if (keyInfo) {
614
+ ws.userId = keyInfo.userId;
615
+ ws.apiKeyId = keyInfo.keyId;
616
+ logConnectionEvent('PLUGIN_AUTHED', `userId=${keyInfo.userId} keyName=${keyInfo.keyName}`);
617
+ } else {
618
+ logConnectionEvent('PLUGIN_AUTH_FAIL', 'Invalid API key');
619
+ ws.close(4001, 'Invalid API key');
620
+ return;
621
+ }
622
+ }
623
+ } catch (e) {
624
+ logConnectionEvent('PLUGIN_AUTH_ERR', e.message);
513
625
  }
514
626
 
515
- sessionToClientId.clear();
516
- pendingAttachRequests.clear();
517
- connectionPairs.clear();
518
- clientConnections.forEach(clientWs => {
519
- clientWs.pairedPlugin = null;
520
- });
521
-
522
627
  pluginConnections.add(ws);
523
628
 
524
629
  const pluginType = 'plugin';
@@ -626,8 +731,8 @@ function handlePluginConnection(ws, clientInfo) {
626
731
  if (!match) {
627
732
  console.log(` ↳ Run "cdp-tunnel update" or reload the extension to sync versions`);
628
733
  }
629
- cachedBrowserVersion = null;
630
- requestVersionFromPlugin();
734
+ getNamespace(ws).cachedBrowserVersion = null;
735
+ requestVersionFromPlugin(ws);
631
736
  return;
632
737
  }
633
738
 
@@ -645,9 +750,9 @@ function handlePluginConnection(ws, clientInfo) {
645
750
  if (parsed.method.startsWith('CDPTunnel.')) {
646
751
  console.log(`[EXT DEBUG] ${parsed.method}: ${JSON.stringify(parsed.params)}`);
647
752
  }
648
- // 对于 Target 事件,始终广播给所有客户端
649
753
  const targetEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
650
754
  if (targetEvents.includes(parsed.method)) {
755
+ const ns = getNamespace(ws);
651
756
  const cdpMsg = {
652
757
  method: parsed.method,
653
758
  params: parsed.params,
@@ -662,19 +767,19 @@ function handlePluginConnection(ws, clientInfo) {
662
767
  const targetId = parsed.params?.targetInfo?.targetId;
663
768
  const openerId = parsed.params?.targetInfo?.openerId;
664
769
  if (openerId && targetId) {
665
- const openerClientId = targetIdToClientId.get(openerId);
770
+ const openerClientId = ns.targetIdToClientId.get(openerId);
666
771
  if (openerClientId) {
667
- targetIdToClientId.set(targetId, openerClientId);
772
+ ns.targetIdToClientId.set(targetId, openerClientId);
668
773
  console.log(`[TARGET CREATED with opener] targetId=${targetId?.substring(0,8) || 'none'} openerId=${openerId?.substring(0,8) || 'none'} -> clientId=${openerClientId}`);
669
774
  }
670
775
  }
671
776
  }
672
777
 
673
- rewriteBrowserContextId(cdpMsg);
778
+ rewriteBrowserContextId(cdpMsg, ws);
674
779
  const cdpData = JSON.stringify(cdpMsg);
675
780
 
676
781
  const targetId = parsed.params?.targetInfo?.targetId;
677
- const eventClientId = targetId ? targetIdToClientId.get(targetId) : null;
782
+ const eventClientId = targetId ? ns.targetIdToClientId.get(targetId) : null;
678
783
 
679
784
  if (eventClientId) {
680
785
  const clientWs = clientById.get(eventClientId);
@@ -683,32 +788,50 @@ function handlePluginConnection(ws, clientInfo) {
683
788
  console.log(`[TARGET EVENT ROUTED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> clientId=${eventClientId}`);
684
789
  }
685
790
  } else if (targetId && (parsed.method === 'Target.targetCreated' || parsed.method === 'Target.attachedToTarget')) {
686
- const pendingMap = parsed.method === 'Target.targetCreated' ? pendingTargetCreatedEvents : pendingAttachedEvents;
687
- pendingMap.set(targetId, { parsed: JSON.parse(JSON.stringify(parsed)), cdpData });
688
- console.log(`[TARGET EVENT PENDING] ${parsed.method} targetId=${targetId?.substring(0,8)} (cached, waiting for createTarget response)`);
791
+ const pendingMap = parsed.method === 'Target.targetCreated' ? ns.pendingTargetCreatedEvents : ns.pendingAttachedEvents;
792
+
793
+ let routedToDiscoverer = false;
794
+ if (ns.discoveringClientIds.size > 0) {
795
+ for (const [discClientId, timestamp] of ns.discoveringClientIds) {
796
+ if (Date.now() - timestamp < 30000) {
797
+ const discWs = clientById.get(discClientId);
798
+ if (discWs && discWs.readyState === WebSocket.OPEN) {
799
+ discWs.send(cdpData);
800
+ console.log(`[TARGET EVENT DISCOVERED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> discovering client=${discClientId}`);
801
+ routedToDiscoverer = true;
802
+ }
803
+ } else {
804
+ ns.discoveringClientIds.delete(discClientId);
805
+ }
806
+ }
807
+ }
808
+
809
+ if (!routedToDiscoverer) {
810
+ pendingMap.set(targetId, { parsed: JSON.parse(JSON.stringify(parsed)), cdpData });
811
+ console.log(`[TARGET EVENT PENDING] ${parsed.method} targetId=${targetId?.substring(0,8)} (cached, waiting for createTarget response)`);
812
+ }
689
813
  } else {
690
814
  console.log(`[TARGET EVENT DROPPED] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (no owner, dropped for isolation)`);
691
815
  }
692
816
  }
693
817
 
694
- // 对于 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
695
818
  if (parsed.method === 'Target.attachedToTarget') {
819
+ const ns = getNamespace(ws);
696
820
  const targetId = parsed.params?.targetInfo?.targetId;
697
821
  const sessionId = parsed.params?.sessionId;
698
822
 
699
823
  if (targetId && sessionId) {
700
824
  const clientId = ws.pairedClientId;
701
825
  if (clientId) {
702
- sessionToClientId.set(sessionId, clientId);
826
+ ns.sessionToClientId.set(sessionId, clientId);
703
827
  console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> clientId=${clientId?.substring(0,8) || 'none'}`);
704
828
  } else {
705
- sessionToClientId.set(sessionId, targetId);
829
+ ns.sessionToClientId.set(sessionId, targetId);
706
830
  console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> targetId=${targetId?.substring(0,8) || 'none'} (no pairedClientId)`);
707
831
  }
708
832
  }
709
833
  }
710
834
 
711
- // 如果不是 Target 事件,按照原来的逻辑发送
712
835
  if (!targetEvents.includes(parsed.method)) {
713
836
  const cdpMsg = {
714
837
  method: parsed.method,
@@ -744,45 +867,42 @@ function handlePluginConnection(ws, clientInfo) {
744
867
  if (parsed && parsed.id !== undefined) {
745
868
  const globalId = parsed.id;
746
869
  const mapping = globalRequestIdMap.get(globalId);
870
+ const ns = getNamespace(ws);
747
871
  console.log(`[RESPONSE DEBUG] globalId=${globalId} hasMapping=${!!mapping} sessionId=${parsed.sessionId?.substring(0,8) || 'none'} method=${parsed.method || 'response'}`);
748
872
  if (mapping) {
749
873
  const clientWs = clientById.get(mapping.clientId);
750
874
  if (clientWs && clientWs.readyState === WebSocket.OPEN) {
751
- // 如果是 Target.createBrowserContext 响应,记录 browserContextId -> clientId 映射
752
875
  if (mapping.isCreateBrowserContext && parsed.result?.browserContextId) {
753
876
  const browserContextId = parsed.result.browserContextId;
754
- browserContextToClientId.set(browserContextId, mapping.clientId);
755
- clientIdToBrowserContext.set(mapping.clientId, browserContextId);
877
+ ns.browserContextToClientId.set(browserContextId, mapping.clientId);
878
+ ns.clientIdToBrowserContext.set(mapping.clientId, browserContextId);
756
879
  console.log(`[BROWSER CONTEXT MAPPED] browserContextId=${browserContextId} -> clientId=${mapping.clientId}`);
757
880
  }
758
881
 
759
- // 如果是 Target.attachToTarget 响应,建立 sessionId -> clientId 映射
760
882
  if (parsed.result?.sessionId && mapping.method === 'Target.attachToTarget') {
761
- sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
883
+ ns.sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
762
884
  console.log(`[SESSION MAPPED from attach response] sessionId=${parsed.result.sessionId?.substring(0,8)} -> clientId=${mapping.clientId?.substring(0,8)}`);
763
885
  }
764
886
 
765
- // 如果是 Target.createTarget 响应,先发送缓存的 Target.attachedToTarget 事件
766
- // 然后再发送响应
767
887
  if (mapping.isCreateTarget && parsed.result?.targetId) {
768
888
  const targetId = parsed.result.targetId;
769
- targetIdToClientId.set(targetId, mapping.clientId);
770
- console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${targetIdToClientId.size}`);
889
+ ns.targetIdToClientId.set(targetId, mapping.clientId);
890
+ console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${ns.targetIdToClientId.size}`);
771
891
 
772
- const cachedCreated = pendingTargetCreatedEvents.get(targetId);
892
+ const cachedCreated = ns.pendingTargetCreatedEvents.get(targetId);
773
893
  if (cachedCreated) {
774
894
  clientWs.send(cachedCreated.cdpData);
775
895
  console.log(`[TARGET CREATED EVENT] Sent cached Target.targetCreated to client: ${mapping.clientId}`);
776
- pendingTargetCreatedEvents.delete(targetId);
896
+ ns.pendingTargetCreatedEvents.delete(targetId);
777
897
  }
778
898
 
779
- const cachedEvent = pendingAttachedEvents.get(targetId);
899
+ const cachedEvent = ns.pendingAttachedEvents.get(targetId);
780
900
  if (cachedEvent) {
781
901
  if (cachedEvent.parsed.sessionId) {
782
- sessionToClientId.set(cachedEvent.parsed.sessionId, mapping.clientId);
902
+ ns.sessionToClientId.set(cachedEvent.parsed.sessionId, mapping.clientId);
783
903
  }
784
904
  console.log(`[SESSION MAPPED from cached] sessionId=${cachedEvent.parsed.sessionId?.substring(0,8) || 'none'} -> clientId=${mapping.clientId} (targetId=${targetId})`);
785
- pendingAttachedEvents.delete(targetId);
905
+ ns.pendingAttachedEvents.delete(targetId);
786
906
 
787
907
  const cdpMsg = {
788
908
  method: cachedEvent.parsed.method,
@@ -796,37 +916,35 @@ function handlePluginConnection(ws, clientInfo) {
796
916
  const newTargetInfo = cachedCreated?.parsed?.params?.targetInfo
797
917
  || cachedEvent?.parsed?.params?.targetInfo;
798
918
  if (newTargetInfo) {
799
- const exists = cachedTargets.some(t => t.targetId === targetId);
919
+ const exists = ns.cachedTargets.some(t => t.targetId === targetId);
800
920
  if (!exists) {
801
- cachedTargets.push(newTargetInfo);
921
+ ns.cachedTargets.push(newTargetInfo);
802
922
  }
803
923
  } else {
804
- invalidateTargetsCache();
924
+ invalidateTargetsCache(ws);
805
925
  }
806
926
  }
807
- // 过滤 Target.getTargets 响应,只返回该客户端拥有的 target
808
927
  if (mapping.isGetTargets && parsed.result && parsed.result.targetInfos) {
809
928
  const clientId = mapping.clientId;
929
+ const pluginWsForGetTargets = ws;
810
930
  parsed.result.targetInfos = parsed.result.targetInfos.filter(t => {
811
931
  if (t.type !== 'page') return true;
812
- const ownerClient = targetIdToClientId.get(t.targetId);
932
+ const ownerClient = ns.targetIdToClientId.get(t.targetId);
933
+ if (!ownerClient) return true;
813
934
  return ownerClient === clientId;
814
935
  });
815
936
  console.log(`[GET TARGETS FILTERED] client=${clientId} returned ${parsed.result.targetInfos.filter(t => t.type === 'page').length} page targets`);
816
937
  }
817
- // 清理 Target.closeTarget 成功后的映射
818
938
  if (parsed.result && parsed.result.success !== undefined && mapping.method === 'Target.closeTarget') {
819
939
  if (mapping.closeTargetId) {
820
- targetIdToClientId.delete(mapping.closeTargetId);
940
+ ns.targetIdToClientId.delete(mapping.closeTargetId);
821
941
  console.log(`[CLOSE TARGET CLEANUP] removed targetId=${mapping.closeTargetId?.substring(0,8)} from mapping`);
822
942
  }
823
- invalidateTargetsCache();
943
+ invalidateTargetsCache(ws);
824
944
  }
825
945
 
826
- // 然后发送响应给客户端
827
946
  const originalId = mapping.originalId;
828
947
  parsed.id = originalId;
829
- // 如果请求有 sessionId,但响应没有,添加 sessionId
830
948
  if (mapping.sessionId && !parsed.sessionId) {
831
949
  parsed.sessionId = mapping.sessionId;
832
950
  }
@@ -844,7 +962,8 @@ function handlePluginConnection(ws, clientInfo) {
844
962
 
845
963
  // 2. sessionId 路由:消息属于特定 session(事件,没有 id)
846
964
  if (parsed && parsed.sessionId) {
847
- const targetClientId = sessionToClientId.get(parsed.sessionId);
965
+ const ns = getNamespace(ws);
966
+ const targetClientId = ns.sessionToClientId.get(parsed.sessionId);
848
967
  console.log(`[SESSION ROUTE] sessionId=${parsed.sessionId?.substring(0,8) || 'none'} -> clientId=${targetClientId || 'not found'}`);
849
968
  if (targetClientId) {
850
969
  const clientWs = clientById.get(targetClientId);
@@ -888,7 +1007,6 @@ function handlePluginConnection(ws, clientInfo) {
888
1007
  cleanupPlugin(ws, id, `close:${code}`);
889
1008
  });
890
1009
 
891
- // 错误处理
892
1010
  ws.on('error', (error) => {
893
1011
  console.error(`[PLUGIN ERROR] ${id}:`, error.message);
894
1012
 
@@ -900,6 +1018,7 @@ function handlePluginConnection(ws, clientInfo) {
900
1018
  });
901
1019
 
902
1020
  pluginConnections.delete(ws);
1021
+ pluginNamespaces.delete(ws);
903
1022
 
904
1023
  clientConnections.forEach(clientWs => {
905
1024
  if (clientWs.pairedPlugin === ws) {
@@ -922,6 +1041,7 @@ function handlePluginConnection(ws, clientInfo) {
922
1041
  type: 'connected',
923
1042
  role: 'plugin',
924
1043
  id: id,
1044
+ pluginId: ws.pluginId,
925
1045
  fresh: (Date.now() - SERVER_START_TIME) < 5000,
926
1046
  timestamp: Date.now()
927
1047
  }));
@@ -930,11 +1050,11 @@ function handlePluginConnection(ws, clientInfo) {
930
1050
  /**
931
1051
  * 处理 CDP 客户端连接 (Playwright/Puppeteer)
932
1052
  */
933
- function handleClientConnection(ws, clientInfo, customClientId = null) {
1053
+ function handleClientConnection(ws, clientInfo, customClientId = null, targetPluginId = null) {
934
1054
  clientConnections.add(ws);
935
1055
  const id = customClientId || generateId('client');
936
1056
  if (shouldLog('info')) {
937
- console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}`);
1057
+ console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}${targetPluginId ? ` targetPlugin=${targetPluginId}` : ''}`);
938
1058
  console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
939
1059
  console.log(` - Total client connections: ${clientConnections.size}`);
940
1060
  }
@@ -947,7 +1067,6 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
947
1067
  totalClients: clientConnections.size
948
1068
  });
949
1069
 
950
- // 检查是否有可用的 plugin 连接
951
1070
  if (pluginConnections.size === 0) {
952
1071
  if (shouldLog('warn')) {
953
1072
  console.log(` - WARNING: No plugin connections available!`);
@@ -965,27 +1084,36 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
965
1084
  }
966
1085
  }
967
1086
  } else {
968
- // 多客户端模式: 所有客户端共享同一个 plugin
969
- // 每个 clientId 对应不同的 tab
970
- const pluginWs = pluginConnections.values().next().value;
1087
+ let pluginWs;
1088
+ if (targetPluginId) {
1089
+ pluginWs = [...pluginConnections].find(p => p.pluginId === targetPluginId);
1090
+ if (!pluginWs) {
1091
+ if (shouldLog('warn')) {
1092
+ console.log(` - WARNING: Plugin ${targetPluginId} not found!`);
1093
+ }
1094
+ ws.close(4004, `Plugin ${targetPluginId} not found`);
1095
+ return;
1096
+ }
1097
+ } else {
1098
+ pluginWs = pluginConnections.values().next().value;
1099
+ }
971
1100
  if (pluginWs) {
972
1101
  connectionPairs.set(id, pluginWs);
973
1102
  ws.pairedPlugin = pluginWs;
1103
+ ws.targetPluginId = pluginWs.pluginId;
974
1104
  clientIdToPlugin.set(id, pluginWs);
975
1105
 
976
1106
  if (shouldLog('info')) {
977
- console.log(` - Paired with plugin: ${pluginWs.id} (shared mode)`);
1107
+ console.log(` - Paired with plugin: ${pluginWs.id} (pluginId=${pluginWs.pluginId})`);
978
1108
  }
979
1109
 
980
1110
  logConnectionEvent('CLIENT_PAIRED', { clientId: id, pluginId: pluginWs.id });
981
1111
 
982
- // 通知 Plugin 新客户端已连接
983
1112
  pluginWs.send(JSON.stringify({
984
1113
  type: 'client-connected',
985
1114
  clientId: id
986
1115
  }));
987
1116
 
988
- // 发送当前所有客户端列表
989
1117
  broadcastClientList();
990
1118
  }
991
1119
  }
@@ -1063,8 +1191,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
1063
1191
  }
1064
1192
  if (parsed && parsed.id !== undefined) {
1065
1193
  if (parsed.method === 'Target.closeTarget') {
1194
+ const pluginWs = ws.pairedPlugin;
1195
+ const ns = pluginWs ? getNamespace(pluginWs) : null;
1066
1196
  const targetId = parsed.params?.targetId;
1067
- const ownerClient = targetId ? targetIdToClientId.get(targetId) : null;
1197
+ const ownerClient = (ns && targetId) ? ns.targetIdToClientId.get(targetId) : null;
1068
1198
  if (ownerClient && ownerClient !== id) {
1069
1199
  console.log(`[BLOCKED] ${parsed.method} targetId=${targetId?.substring(0,8)} owner=${ownerClient?.substring(0,8)} requester=${id?.substring(0,8)} — not owner`);
1070
1200
  const errMsg = JSON.stringify({
@@ -1080,8 +1210,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
1080
1210
  currentMapping.closeTargetId = targetId;
1081
1211
  }
1082
1212
  } else if (parsed.method === 'Target.attachToTarget') {
1213
+ const pluginWs = ws.pairedPlugin;
1214
+ const ns = pluginWs ? getNamespace(pluginWs) : null;
1083
1215
  const targetId = parsed.params?.targetId;
1084
- const ownerClient = targetId ? targetIdToClientId.get(targetId) : null;
1216
+ const ownerClient = (ns && targetId) ? ns.targetIdToClientId.get(targetId) : null;
1085
1217
  if (ownerClient && ownerClient !== id) {
1086
1218
  console.log(`[BLOCKED] ${parsed.method} targetId=${targetId?.substring(0,8)} owner=${ownerClient?.substring(0,8)} requester=${id?.substring(0,8)} — not owner`);
1087
1219
  const errMsg = JSON.stringify({
@@ -1109,6 +1241,14 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
1109
1241
  }
1110
1242
  }
1111
1243
 
1244
+ if (parsed && (parsed.method === 'Target.setDiscoverTargets' || parsed.method === 'Target.setAutoAttach')) {
1245
+ const ns = ws.pairedPlugin ? getNamespace(ws.pairedPlugin) : null;
1246
+ if (ns) {
1247
+ ns.discoveringClientIds.set(id, Date.now());
1248
+ console.log(`[DISCOVERING] client=${id} method=${parsed.method}`);
1249
+ }
1250
+ }
1251
+
1112
1252
  if (parsed && parsed.method === 'Browser.close') {
1113
1253
  if (shouldLog('info')) {
1114
1254
  console.log(`\n[BROWSER CLOSE] Client ${id} requested Browser.close, forwarding to plugin`);
@@ -1173,7 +1313,17 @@ function handlePageConnection(ws, clientInfo, targetId) {
1173
1313
  ws.lastActivityTime = Date.now();
1174
1314
  clientById.set(id, ws);
1175
1315
 
1176
- const plugin = pluginConnections.values().next().value;
1316
+ let plugin = null;
1317
+ for (const p of pluginConnections) {
1318
+ const ns = getNamespace(p);
1319
+ if (ns.targetIdToClientId.has(targetId)) {
1320
+ plugin = p;
1321
+ break;
1322
+ }
1323
+ }
1324
+ if (!plugin) {
1325
+ plugin = pluginConnections.values().next().value;
1326
+ }
1177
1327
  if (plugin && plugin.readyState === WebSocket.OPEN) {
1178
1328
  ws.pairedPlugin = plugin;
1179
1329
  if (shouldLog('info')) {
@@ -1209,7 +1359,7 @@ function handlePageConnection(ws, clientInfo, targetId) {
1209
1359
  // 对于全局 Target 事件,需要广播给所有客户端
1210
1360
  const broadcastEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
1211
1361
  if (broadcastEvents.includes(msg.method)) {
1212
- rewriteBrowserContextId(cdpMsg);
1362
+ rewriteBrowserContextId(cdpMsg, ws.pairedPlugin);
1213
1363
  console.log(`[PLUGIN -> ALL CLIENTS] Broadcasting ${msg.method}`);
1214
1364
  broadcastToClients(JSON.stringify(cdpMsg), null);
1215
1365
  } else {
@@ -1313,23 +1463,24 @@ function handlePageConnection(ws, clientInfo, targetId) {
1313
1463
  * 插件总是报告 'default',但 Playwright 期望自己创建的 context ID
1314
1464
  * 通过 openerId 找到对应的 clientId,再找到该 client 的 browserContextId
1315
1465
  */
1316
- function rewriteBrowserContextId(cdpMsg) {
1466
+ function rewriteBrowserContextId(cdpMsg, pluginWs) {
1317
1467
  const targetInfo = cdpMsg.params?.targetInfo;
1318
1468
  if (!targetInfo || targetInfo.browserContextId !== 'default') {
1319
1469
  return cdpMsg;
1320
1470
  }
1321
1471
 
1472
+ const ns = pluginWs ? getNamespace(pluginWs) : null;
1322
1473
  let clientId = null;
1323
1474
 
1324
- if (targetInfo.openerId) {
1325
- clientId = targetIdToClientId.get(targetInfo.openerId);
1475
+ if (targetInfo.openerId && ns) {
1476
+ clientId = ns.targetIdToClientId.get(targetInfo.openerId);
1326
1477
  }
1327
- if (!clientId && targetInfo.targetId) {
1328
- clientId = targetIdToClientId.get(targetInfo.targetId);
1478
+ if (!clientId && targetInfo.targetId && ns) {
1479
+ clientId = ns.targetIdToClientId.get(targetInfo.targetId);
1329
1480
  }
1330
1481
 
1331
- if (clientId) {
1332
- const contextId = clientIdToBrowserContext.get(clientId);
1482
+ if (clientId && ns) {
1483
+ const contextId = ns.clientIdToBrowserContext.get(clientId);
1333
1484
  if (contextId) {
1334
1485
  console.log(`[CONTEXT REWRITE] targetId=${targetInfo.targetId?.substring(0,8) || 'none'} browserContextId: 'default' -> '${contextId}' (via openerId=${targetInfo.openerId?.substring(0,8) || 'none'}, clientId=${clientId})`);
1335
1486
  targetInfo.browserContextId = contextId;
@@ -1605,6 +1756,13 @@ setInterval(() => {
1605
1756
  };
1606
1757
  });
1607
1758
 
1759
+ let totalSessions = 0;
1760
+ let totalPendingAttach = 0;
1761
+ pluginNamespaces.forEach(ns => {
1762
+ totalSessions += ns.sessionToClientId.size;
1763
+ totalPendingAttach += ns.pendingAttachRequests.size;
1764
+ });
1765
+
1608
1766
  logStatus({
1609
1767
  timestamp: now,
1610
1768
  plugins: pluginConnections.size,
@@ -1614,8 +1772,8 @@ setInterval(() => {
1614
1772
  pairs: connectionPairs.size,
1615
1773
  pluginDetails: pluginList,
1616
1774
  clientDetails: clientList,
1617
- sessions: sessionToClientId.size,
1618
- pendingAttach: pendingAttachRequests.size
1775
+ sessions: totalSessions,
1776
+ pendingAttach: totalPendingAttach
1619
1777
  });
1620
1778
  }, CONFIG.STATUS_PRINT_INTERVAL);
1621
1779