cdp-tunnel 2.5.21 → 2.5.22

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,38 @@ 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
+ }
171
+ }
172
+
173
+ const pluginNamespaces = new Map();
174
+
175
+ function getNamespace(pluginWs) {
176
+ if (!pluginNamespaces.has(pluginWs)) {
177
+ pluginNamespaces.set(pluginWs, new PluginNamespace());
178
+ }
179
+ return pluginNamespaces.get(pluginWs);
180
+ }
181
+
145
182
  const connectionPairs = new Map();
146
183
  const clientById = new Map();
147
- const sessionToClientId = new Map();
148
- const pendingAttachRequests = new Map();
149
184
  const clientIdToPlugin = new Map();
150
185
  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
186
  let globalRequestIdCounter = 0;
157
187
 
158
188
  const { version: PKG_VERSION } = require('../package.json');
159
189
 
160
- let cachedTargets = [];
161
- let lastTargetsUpdate = 0;
162
- let cachedBrowserVersion = null;
163
-
164
190
  console.log('='.repeat(60));
165
191
  console.log(` WebSocket CDP Proxy Server v${PKG_VERSION}`);
166
192
  console.log('='.repeat(60));
@@ -177,26 +203,24 @@ function getHost(req) {
177
203
  return req.headers.host || `localhost:${PORT}`;
178
204
  }
179
205
 
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}`;
206
+ function invalidateTargetsCache(pluginWs) {
207
+ if (pluginWs) {
208
+ getNamespace(pluginWs).lastTargetsUpdate = 0;
209
+ } else {
210
+ pluginNamespaces.forEach(ns => { ns.lastTargetsUpdate = 0; });
211
+ }
189
212
  }
190
213
 
191
- function invalidateTargetsCache() {
192
- lastTargetsUpdate = 0;
193
- }
214
+ async function requestVersionFromPlugin(pluginWs) {
215
+ if (!pluginWs) {
216
+ pluginWs = pluginConnections.values().next().value;
217
+ }
218
+ if (!pluginWs) return null;
194
219
 
195
- async function requestVersionFromPlugin() {
196
- if (cachedBrowserVersion) return cachedBrowserVersion;
220
+ const ns = getNamespace(pluginWs);
221
+ if (ns.cachedBrowserVersion) return ns.cachedBrowserVersion;
197
222
 
198
- const plugin = pluginConnections.values().next().value;
199
- if (!plugin || plugin.readyState !== WebSocket.OPEN) {
223
+ if (pluginWs.readyState !== WebSocket.OPEN) {
200
224
  return null;
201
225
  }
202
226
 
@@ -209,35 +233,40 @@ async function requestVersionFromPlugin() {
209
233
  const msg = JSON.parse(data.toString());
210
234
  if (msg.id === requestId && msg.result) {
211
235
  clearTimeout(timeout);
212
- plugin.off('message', handler);
236
+ pluginWs.off('message', handler);
213
237
  if (msg.result.product || msg.result.userAgent) {
214
- cachedBrowserVersion = msg.result;
238
+ ns.cachedBrowserVersion = msg.result;
215
239
  }
216
- resolve(cachedBrowserVersion || msg.result);
240
+ resolve(ns.cachedBrowserVersion || msg.result);
217
241
  }
218
242
  } catch (e) {}
219
243
  };
220
244
 
221
- plugin.on('message', handler);
222
- plugin.send(JSON.stringify({ id: requestId, method: 'Browser.getVersion' }));
245
+ pluginWs.on('message', handler);
246
+ pluginWs.send(JSON.stringify({ id: requestId, method: 'Browser.getVersion' }));
223
247
  });
224
248
  }
225
249
 
226
- async function requestTargetsFromPlugin() {
250
+ async function requestTargetsFromPlugin(pluginWs) {
251
+ if (!pluginWs) {
252
+ pluginWs = pluginConnections.values().next().value;
253
+ }
254
+ if (!pluginWs) return [];
255
+
256
+ const ns = getNamespace(pluginWs);
227
257
  const now = Date.now();
228
- if (now - lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && cachedTargets.length > 0) {
229
- return cachedTargets;
258
+ if (now - ns.lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && ns.cachedTargets.length > 0) {
259
+ return ns.cachedTargets;
230
260
  }
231
261
 
232
- const plugin = pluginConnections.values().next().value;
233
- if (!plugin || plugin.readyState !== WebSocket.OPEN) {
234
- return cachedTargets;
262
+ if (pluginWs.readyState !== WebSocket.OPEN) {
263
+ return ns.cachedTargets;
235
264
  }
236
265
 
237
266
  return new Promise((resolve) => {
238
267
  const requestId = `targets_${Date.now()}`;
239
268
  const timeout = setTimeout(() => {
240
- resolve(cachedTargets);
269
+ resolve(ns.cachedTargets);
241
270
  }, CONFIG.TARGETS_REQUEST_TIMEOUT);
242
271
 
243
272
  const handler = (data) => {
@@ -245,36 +274,79 @@ async function requestTargetsFromPlugin() {
245
274
  const msg = JSON.parse(data.toString());
246
275
  if (msg.id === requestId && msg.result?.targetInfos) {
247
276
  clearTimeout(timeout);
248
- plugin.off('message', handler);
249
- cachedTargets = msg.result.targetInfos;
250
- lastTargetsUpdate = now;
251
- resolve(cachedTargets);
277
+ pluginWs.off('message', handler);
278
+ ns.cachedTargets = msg.result.targetInfos;
279
+ ns.lastTargetsUpdate = now;
280
+ resolve(ns.cachedTargets);
252
281
  }
253
282
  } catch (e) {}
254
283
  };
255
284
 
256
- plugin.on('message', handler);
257
- plugin.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
285
+ pluginWs.on('message', handler);
286
+ pluginWs.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
258
287
  });
259
288
  }
260
289
 
290
+ /**
291
+ * 生成指定 plugin 的 browser WS URL
292
+ */
293
+ function buildBrowserWsUrl(pluginId) {
294
+ const host = CONFIG.EXTERNAL_HOST || `localhost:${PORT}`;
295
+ return `ws://${host}/devtools/browser/${pluginId}`;
296
+ }
297
+
261
298
  /**
262
299
  * 处理 HTTP 请求
263
300
  */
301
+ function resolvePluginFromUrl(url) {
302
+ const parts = url.pathname.split('/').filter(Boolean);
303
+ if (parts.length >= 3) {
304
+ const pluginId = parts[2];
305
+ for (const pluginWs of pluginConnections) {
306
+ if (pluginWs.pluginId === pluginId) return pluginWs;
307
+ }
308
+ }
309
+ return pluginConnections.values().next().value || null;
310
+ }
311
+
264
312
  async function handleHttpRequest(req, res) {
265
313
  const url = new URL(req.url, `http://localhost:${PORT}`);
266
314
 
267
- if (url.pathname === '/json/version' || url.pathname === '/json/version/') {
268
- const ver = await requestVersionFromPlugin();
315
+ if (url.pathname === '/json/browsers' || url.pathname === '/json/browsers/') {
316
+ const browsers = [];
317
+ for (const pluginWs of pluginConnections) {
318
+ if (pluginWs.readyState !== WebSocket.OPEN) continue;
319
+ const ns = getNamespace(pluginWs);
320
+ browsers.push({
321
+ pluginId: pluginWs.pluginId,
322
+ pluginName: pluginWs.pluginName || 'My Browser',
323
+ userId: pluginWs.userId || null,
324
+ browserName: ns.cachedBrowserVersion?.Browser || 'Unknown',
325
+ targets: ns.cachedTargets.length,
326
+ connected: true,
327
+ connectedAt: pluginWs.connectedAt,
328
+ webSocketDebuggerUrl: buildBrowserWsUrl(pluginWs.pluginId)
329
+ });
330
+ }
331
+ res.writeHead(200, { 'Content-Type': 'application/json' });
332
+ res.end(JSON.stringify(browsers));
333
+ return;
334
+ }
335
+
336
+ if (url.pathname === '/json/version' || url.pathname === '/json/version/' ||
337
+ url.pathname.match(/^\/json\/version\/[^/]+$/)) {
338
+ const pluginWs = resolvePluginFromUrl(url);
339
+ const ver = await requestVersionFromPlugin(pluginWs);
269
340
  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
341
  const product = ver?.product || 'Chrome/131.0.6778.86';
342
+ const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
271
343
  const payload = {
272
344
  Browser: `${product} (cdp-tunnel/${PKG_VERSION})`,
273
345
  'Protocol-Version': ver?.protocolVersion || '1.3',
274
346
  'User-Agent': userAgent,
275
347
  'V8-Version': ver?.jsVersion || '',
276
348
  'WebKit-Version': '537.36',
277
- webSocketDebuggerUrl: buildWebSocketDebuggerUrl(req)
349
+ webSocketDebuggerUrl: `ws://${getHost(req)}/devtools/browser/${browserId}`
278
350
  };
279
351
  res.writeHead(200, { 'Content-Type': 'application/json' });
280
352
  res.end(JSON.stringify(payload));
@@ -282,16 +354,19 @@ async function handleHttpRequest(req, res) {
282
354
  }
283
355
 
284
356
  if (url.pathname === '/json' || url.pathname === '/json/' ||
285
- url.pathname === '/json/list' || url.pathname === '/json/list/') {
286
- const targets = await requestTargetsFromPlugin();
357
+ url.pathname === '/json/list' || url.pathname === '/json/list/' ||
358
+ url.pathname.match(/^\/json\/list\/[^/]+$/)) {
359
+ const pluginWs = resolvePluginFromUrl(url);
360
+ const targets = await requestTargetsFromPlugin(pluginWs);
361
+ const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
287
362
  const targetList = targets
288
363
  .filter(t => {
289
364
  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://')) {
365
+ const tUrl = t.url || '';
366
+ if (tUrl.startsWith('chrome://') ||
367
+ tUrl.startsWith('chrome-extension://') ||
368
+ tUrl.startsWith('devtools://') ||
369
+ tUrl.startsWith('edge://')) {
295
370
  return false;
296
371
  }
297
372
  return true;
@@ -305,7 +380,7 @@ async function handleHttpRequest(req, res) {
305
380
  title: t.title || '',
306
381
  type: t.type,
307
382
  url: t.url || '',
308
- webSocketDebuggerUrl: buildTargetWebSocketUrl(req, t.targetId)
383
+ webSocketDebuggerUrl: `ws://${getHost(req)}/devtools/page/${t.targetId}`
309
384
  }));
310
385
  res.writeHead(200, { 'Content-Type': 'application/json' });
311
386
  res.end(JSON.stringify(targetList));
@@ -322,8 +397,10 @@ async function handleHttpRequest(req, res) {
322
397
  server.on('upgrade', (req, socket, head) => {
323
398
  const url = new URL(req.url, `http://localhost:${PORT}`);
324
399
  const path = url.pathname;
400
+ const pathParts = path.split('/').filter(Boolean);
325
401
  const isPlugin = path === '/plugin';
326
402
  const isClient = path === '/client' ||
403
+ path.startsWith('/client/') ||
327
404
  path.startsWith('/client-') ||
328
405
  path.startsWith('/devtools/browser/') ||
329
406
  path.startsWith('/devtools/page/');
@@ -341,6 +418,7 @@ server.on('upgrade', (req, socket, head) => {
341
418
  wss.on('connection', (ws, req) => {
342
419
  const url = new URL(req.url, `http://localhost:${PORT}`);
343
420
  const path = url.pathname;
421
+ const pathParts = path.split('/').filter(Boolean);
344
422
 
345
423
  const clientInfo = {
346
424
  ip: req.socket.remoteAddress,
@@ -348,10 +426,16 @@ wss.on('connection', (ws, req) => {
348
426
  };
349
427
 
350
428
  if (path === '/plugin') {
351
- handlePluginConnection(ws, clientInfo);
352
- } else if (path === '/client' || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
429
+ handlePluginConnection(ws, clientInfo, req);
430
+ } else if (path === '/client' || path.startsWith('/client/') || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
353
431
  const customClientId = path.startsWith('/client-') ? path.replace('/client-', '') : null;
354
- handleClientConnection(ws, clientInfo, customClientId);
432
+ let targetPluginId = null;
433
+ if (pathParts[0] === 'client' && pathParts[1]) {
434
+ targetPluginId = pathParts[1];
435
+ } else if (pathParts[0] === 'devtools' && pathParts[1] === 'browser' && pathParts[2]) {
436
+ targetPluginId = pathParts[2];
437
+ }
438
+ handleClientConnection(ws, clientInfo, customClientId, targetPluginId);
355
439
  } else if (path.startsWith('/devtools/page/')) {
356
440
  const targetId = path.replace('/devtools/page/', '');
357
441
  handlePageConnection(ws, clientInfo, targetId);
@@ -362,21 +446,26 @@ wss.on('connection', (ws, req) => {
362
446
  });
363
447
 
364
448
  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);
449
+ const pluginWs = ws.pairedPlugin || clientIdToPlugin.get(id);
450
+ const ns = pluginWs ? getNamespace(pluginWs) : null;
451
+
452
+ if (ns) {
453
+ const sessionsToClean = [];
454
+ for (const [sessionId, clientId] of ns.sessionToClientId.entries()) {
455
+ if (clientId === id) {
456
+ sessionsToClean.push(sessionId);
457
+ ns.sessionToClientId.delete(sessionId);
458
+ }
370
459
  }
371
460
  }
372
461
 
373
462
  clientConnections.delete(ws);
374
463
  clientById.delete(id);
464
+ clientIdToPlugin.delete(id);
375
465
 
376
466
  logConnectionEvent('CLIENT_DISCONNECTED', {
377
467
  id,
378
468
  reason,
379
- sessionsCleaned: sessionsToClean.length,
380
469
  totalPlugins: pluginConnections.size,
381
470
  totalClients: clientConnections.size
382
471
  });
@@ -384,7 +473,6 @@ function cleanupClient(ws, id, reason) {
384
473
  logDisconnect('CLIENT_CLEANUP', {
385
474
  clientId: id,
386
475
  reason,
387
- sessionsLost: sessionsToClean.length,
388
476
  cdpMethodsUsed: ws.cdpTrace ? [...new Set(ws.cdpTrace)] : [],
389
477
  uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
390
478
  remainingClients: clientConnections.size,
@@ -401,20 +489,22 @@ function cleanupClient(ws, id, reason) {
401
489
  safeSend(ws.pairedPlugin, JSON.stringify({
402
490
  type: 'client-disconnected',
403
491
  clientId: id,
404
- sessions: sessionsToClean
492
+ sessions: []
405
493
  }), 'plugin');
406
494
  }
407
495
 
408
496
  broadcastClientList();
409
497
 
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);
498
+ if (ns) {
499
+ for (const [tId, cId] of ns.targetIdToClientId.entries()) {
500
+ if (cId === id) ns.targetIdToClientId.delete(tId);
501
+ }
502
+ for (const [bcId, cId] of ns.browserContextToClientId.entries()) {
503
+ if (cId === id) ns.browserContextToClientId.delete(bcId);
504
+ }
505
+ if (ns.clientIdToBrowserContext.has(id)) {
506
+ ns.clientIdToBrowserContext.delete(id);
507
+ }
418
508
  }
419
509
  for (const [gId, mapping] of globalRequestIdMap.entries()) {
420
510
  if (mapping.clientId === id) globalRequestIdMap.delete(gId);
@@ -446,7 +536,9 @@ function sendPendingRequestErrors(pluginWs) {
446
536
  }
447
537
 
448
538
  function cleanupPlugin(ws, id, reason) {
539
+ const ns = getNamespace(ws);
449
540
  pluginConnections.delete(ws);
541
+ pluginNamespaces.delete(ws);
450
542
 
451
543
  if (pluginConnections.size === 0) {
452
544
  updateExtensionState(false);
@@ -478,8 +570,8 @@ function cleanupPlugin(ws, id, reason) {
478
570
  remainingPlugins: pluginConnections.size,
479
571
  affectedClients,
480
572
  uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
481
- activeSessions: sessionToClientId.size,
482
- pendingRequests: pendingAttachRequests.size
573
+ activeSessions: ns.sessionToClientId.size,
574
+ pendingRequests: ns.pendingAttachRequests.size
483
575
  });
484
576
 
485
577
  if (ws.pairedClientId) {
@@ -490,35 +582,46 @@ function cleanupPlugin(ws, id, reason) {
490
582
  /**
491
583
  * 处理 Chrome 扩展连接
492
584
  */
493
- function handlePluginConnection(ws, clientInfo) {
585
+ function handlePluginConnection(ws, clientInfo, request) {
586
+ const req = request;
494
587
  const id = generateId('plugin');
495
588
 
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');
589
+ ws.pluginId = 'browser_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
590
+
591
+ try {
592
+ const url = new URL(req.url, `http://localhost`);
593
+ const apiKey = url.searchParams.get('key');
594
+ const desiredPluginId = url.searchParams.get('pluginId');
595
+
596
+ if (desiredPluginId && /^browser_[a-zA-Z0-9_]+$/.test(desiredPluginId)) {
597
+ let conflict = false;
598
+ for (const existing of pluginConnections) {
599
+ if (existing.pluginId === desiredPluginId && existing !== ws) {
600
+ conflict = true;
601
+ break;
503
602
  }
504
- toRemove.push(oldWs);
505
603
  }
506
- });
507
- toRemove.forEach(oldWs => {
508
- pluginConnections.delete(oldWs);
509
- if (shouldLog('info')) {
510
- console.log(`[PLUGIN] Removed old connection: ${oldWs.id}`);
604
+ if (!conflict) {
605
+ ws.pluginId = desiredPluginId;
511
606
  }
512
- });
607
+ }
608
+
609
+ if (HAS_SAAS && apiKey) {
610
+ const keyInfo = validateApiKey(apiKey);
611
+ if (keyInfo) {
612
+ ws.userId = keyInfo.userId;
613
+ ws.apiKeyId = keyInfo.keyId;
614
+ logConnectionEvent('PLUGIN_AUTHED', `userId=${keyInfo.userId} keyName=${keyInfo.keyName}`);
615
+ } else {
616
+ logConnectionEvent('PLUGIN_AUTH_FAIL', 'Invalid API key');
617
+ ws.close(4001, 'Invalid API key');
618
+ return;
619
+ }
620
+ }
621
+ } catch (e) {
622
+ logConnectionEvent('PLUGIN_AUTH_ERR', e.message);
513
623
  }
514
624
 
515
- sessionToClientId.clear();
516
- pendingAttachRequests.clear();
517
- connectionPairs.clear();
518
- clientConnections.forEach(clientWs => {
519
- clientWs.pairedPlugin = null;
520
- });
521
-
522
625
  pluginConnections.add(ws);
523
626
 
524
627
  const pluginType = 'plugin';
@@ -626,8 +729,8 @@ function handlePluginConnection(ws, clientInfo) {
626
729
  if (!match) {
627
730
  console.log(` ↳ Run "cdp-tunnel update" or reload the extension to sync versions`);
628
731
  }
629
- cachedBrowserVersion = null;
630
- requestVersionFromPlugin();
732
+ getNamespace(ws).cachedBrowserVersion = null;
733
+ requestVersionFromPlugin(ws);
631
734
  return;
632
735
  }
633
736
 
@@ -645,9 +748,9 @@ function handlePluginConnection(ws, clientInfo) {
645
748
  if (parsed.method.startsWith('CDPTunnel.')) {
646
749
  console.log(`[EXT DEBUG] ${parsed.method}: ${JSON.stringify(parsed.params)}`);
647
750
  }
648
- // 对于 Target 事件,始终广播给所有客户端
649
751
  const targetEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
650
752
  if (targetEvents.includes(parsed.method)) {
753
+ const ns = getNamespace(ws);
651
754
  const cdpMsg = {
652
755
  method: parsed.method,
653
756
  params: parsed.params,
@@ -662,19 +765,19 @@ function handlePluginConnection(ws, clientInfo) {
662
765
  const targetId = parsed.params?.targetInfo?.targetId;
663
766
  const openerId = parsed.params?.targetInfo?.openerId;
664
767
  if (openerId && targetId) {
665
- const openerClientId = targetIdToClientId.get(openerId);
768
+ const openerClientId = ns.targetIdToClientId.get(openerId);
666
769
  if (openerClientId) {
667
- targetIdToClientId.set(targetId, openerClientId);
770
+ ns.targetIdToClientId.set(targetId, openerClientId);
668
771
  console.log(`[TARGET CREATED with opener] targetId=${targetId?.substring(0,8) || 'none'} openerId=${openerId?.substring(0,8) || 'none'} -> clientId=${openerClientId}`);
669
772
  }
670
773
  }
671
774
  }
672
775
 
673
- rewriteBrowserContextId(cdpMsg);
776
+ rewriteBrowserContextId(cdpMsg, ws);
674
777
  const cdpData = JSON.stringify(cdpMsg);
675
778
 
676
779
  const targetId = parsed.params?.targetInfo?.targetId;
677
- const eventClientId = targetId ? targetIdToClientId.get(targetId) : null;
780
+ const eventClientId = targetId ? ns.targetIdToClientId.get(targetId) : null;
678
781
 
679
782
  if (eventClientId) {
680
783
  const clientWs = clientById.get(eventClientId);
@@ -683,7 +786,7 @@ function handlePluginConnection(ws, clientInfo) {
683
786
  console.log(`[TARGET EVENT ROUTED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> clientId=${eventClientId}`);
684
787
  }
685
788
  } else if (targetId && (parsed.method === 'Target.targetCreated' || parsed.method === 'Target.attachedToTarget')) {
686
- const pendingMap = parsed.method === 'Target.targetCreated' ? pendingTargetCreatedEvents : pendingAttachedEvents;
789
+ const pendingMap = parsed.method === 'Target.targetCreated' ? ns.pendingTargetCreatedEvents : ns.pendingAttachedEvents;
687
790
  pendingMap.set(targetId, { parsed: JSON.parse(JSON.stringify(parsed)), cdpData });
688
791
  console.log(`[TARGET EVENT PENDING] ${parsed.method} targetId=${targetId?.substring(0,8)} (cached, waiting for createTarget response)`);
689
792
  } else {
@@ -691,24 +794,23 @@ function handlePluginConnection(ws, clientInfo) {
691
794
  }
692
795
  }
693
796
 
694
- // 对于 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
695
797
  if (parsed.method === 'Target.attachedToTarget') {
798
+ const ns = getNamespace(ws);
696
799
  const targetId = parsed.params?.targetInfo?.targetId;
697
800
  const sessionId = parsed.params?.sessionId;
698
801
 
699
802
  if (targetId && sessionId) {
700
803
  const clientId = ws.pairedClientId;
701
804
  if (clientId) {
702
- sessionToClientId.set(sessionId, clientId);
805
+ ns.sessionToClientId.set(sessionId, clientId);
703
806
  console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> clientId=${clientId?.substring(0,8) || 'none'}`);
704
807
  } else {
705
- sessionToClientId.set(sessionId, targetId);
808
+ ns.sessionToClientId.set(sessionId, targetId);
706
809
  console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> targetId=${targetId?.substring(0,8) || 'none'} (no pairedClientId)`);
707
810
  }
708
811
  }
709
812
  }
710
813
 
711
- // 如果不是 Target 事件,按照原来的逻辑发送
712
814
  if (!targetEvents.includes(parsed.method)) {
713
815
  const cdpMsg = {
714
816
  method: parsed.method,
@@ -744,45 +846,42 @@ function handlePluginConnection(ws, clientInfo) {
744
846
  if (parsed && parsed.id !== undefined) {
745
847
  const globalId = parsed.id;
746
848
  const mapping = globalRequestIdMap.get(globalId);
849
+ const ns = getNamespace(ws);
747
850
  console.log(`[RESPONSE DEBUG] globalId=${globalId} hasMapping=${!!mapping} sessionId=${parsed.sessionId?.substring(0,8) || 'none'} method=${parsed.method || 'response'}`);
748
851
  if (mapping) {
749
852
  const clientWs = clientById.get(mapping.clientId);
750
853
  if (clientWs && clientWs.readyState === WebSocket.OPEN) {
751
- // 如果是 Target.createBrowserContext 响应,记录 browserContextId -> clientId 映射
752
854
  if (mapping.isCreateBrowserContext && parsed.result?.browserContextId) {
753
855
  const browserContextId = parsed.result.browserContextId;
754
- browserContextToClientId.set(browserContextId, mapping.clientId);
755
- clientIdToBrowserContext.set(mapping.clientId, browserContextId);
856
+ ns.browserContextToClientId.set(browserContextId, mapping.clientId);
857
+ ns.clientIdToBrowserContext.set(mapping.clientId, browserContextId);
756
858
  console.log(`[BROWSER CONTEXT MAPPED] browserContextId=${browserContextId} -> clientId=${mapping.clientId}`);
757
859
  }
758
860
 
759
- // 如果是 Target.attachToTarget 响应,建立 sessionId -> clientId 映射
760
861
  if (parsed.result?.sessionId && mapping.method === 'Target.attachToTarget') {
761
- sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
862
+ ns.sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
762
863
  console.log(`[SESSION MAPPED from attach response] sessionId=${parsed.result.sessionId?.substring(0,8)} -> clientId=${mapping.clientId?.substring(0,8)}`);
763
864
  }
764
865
 
765
- // 如果是 Target.createTarget 响应,先发送缓存的 Target.attachedToTarget 事件
766
- // 然后再发送响应
767
866
  if (mapping.isCreateTarget && parsed.result?.targetId) {
768
867
  const targetId = parsed.result.targetId;
769
- targetIdToClientId.set(targetId, mapping.clientId);
770
- console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${targetIdToClientId.size}`);
868
+ ns.targetIdToClientId.set(targetId, mapping.clientId);
869
+ console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${ns.targetIdToClientId.size}`);
771
870
 
772
- const cachedCreated = pendingTargetCreatedEvents.get(targetId);
871
+ const cachedCreated = ns.pendingTargetCreatedEvents.get(targetId);
773
872
  if (cachedCreated) {
774
873
  clientWs.send(cachedCreated.cdpData);
775
874
  console.log(`[TARGET CREATED EVENT] Sent cached Target.targetCreated to client: ${mapping.clientId}`);
776
- pendingTargetCreatedEvents.delete(targetId);
875
+ ns.pendingTargetCreatedEvents.delete(targetId);
777
876
  }
778
877
 
779
- const cachedEvent = pendingAttachedEvents.get(targetId);
878
+ const cachedEvent = ns.pendingAttachedEvents.get(targetId);
780
879
  if (cachedEvent) {
781
880
  if (cachedEvent.parsed.sessionId) {
782
- sessionToClientId.set(cachedEvent.parsed.sessionId, mapping.clientId);
881
+ ns.sessionToClientId.set(cachedEvent.parsed.sessionId, mapping.clientId);
783
882
  }
784
883
  console.log(`[SESSION MAPPED from cached] sessionId=${cachedEvent.parsed.sessionId?.substring(0,8) || 'none'} -> clientId=${mapping.clientId} (targetId=${targetId})`);
785
- pendingAttachedEvents.delete(targetId);
884
+ ns.pendingAttachedEvents.delete(targetId);
786
885
 
787
886
  const cdpMsg = {
788
887
  method: cachedEvent.parsed.method,
@@ -796,37 +895,33 @@ function handlePluginConnection(ws, clientInfo) {
796
895
  const newTargetInfo = cachedCreated?.parsed?.params?.targetInfo
797
896
  || cachedEvent?.parsed?.params?.targetInfo;
798
897
  if (newTargetInfo) {
799
- const exists = cachedTargets.some(t => t.targetId === targetId);
898
+ const exists = ns.cachedTargets.some(t => t.targetId === targetId);
800
899
  if (!exists) {
801
- cachedTargets.push(newTargetInfo);
900
+ ns.cachedTargets.push(newTargetInfo);
802
901
  }
803
902
  } else {
804
- invalidateTargetsCache();
903
+ invalidateTargetsCache(ws);
805
904
  }
806
905
  }
807
- // 过滤 Target.getTargets 响应,只返回该客户端拥有的 target
808
906
  if (mapping.isGetTargets && parsed.result && parsed.result.targetInfos) {
809
907
  const clientId = mapping.clientId;
810
908
  parsed.result.targetInfos = parsed.result.targetInfos.filter(t => {
811
909
  if (t.type !== 'page') return true;
812
- const ownerClient = targetIdToClientId.get(t.targetId);
910
+ const ownerClient = ns.targetIdToClientId.get(t.targetId);
813
911
  return ownerClient === clientId;
814
912
  });
815
913
  console.log(`[GET TARGETS FILTERED] client=${clientId} returned ${parsed.result.targetInfos.filter(t => t.type === 'page').length} page targets`);
816
914
  }
817
- // 清理 Target.closeTarget 成功后的映射
818
915
  if (parsed.result && parsed.result.success !== undefined && mapping.method === 'Target.closeTarget') {
819
916
  if (mapping.closeTargetId) {
820
- targetIdToClientId.delete(mapping.closeTargetId);
917
+ ns.targetIdToClientId.delete(mapping.closeTargetId);
821
918
  console.log(`[CLOSE TARGET CLEANUP] removed targetId=${mapping.closeTargetId?.substring(0,8)} from mapping`);
822
919
  }
823
- invalidateTargetsCache();
920
+ invalidateTargetsCache(ws);
824
921
  }
825
922
 
826
- // 然后发送响应给客户端
827
923
  const originalId = mapping.originalId;
828
924
  parsed.id = originalId;
829
- // 如果请求有 sessionId,但响应没有,添加 sessionId
830
925
  if (mapping.sessionId && !parsed.sessionId) {
831
926
  parsed.sessionId = mapping.sessionId;
832
927
  }
@@ -844,7 +939,8 @@ function handlePluginConnection(ws, clientInfo) {
844
939
 
845
940
  // 2. sessionId 路由:消息属于特定 session(事件,没有 id)
846
941
  if (parsed && parsed.sessionId) {
847
- const targetClientId = sessionToClientId.get(parsed.sessionId);
942
+ const ns = getNamespace(ws);
943
+ const targetClientId = ns.sessionToClientId.get(parsed.sessionId);
848
944
  console.log(`[SESSION ROUTE] sessionId=${parsed.sessionId?.substring(0,8) || 'none'} -> clientId=${targetClientId || 'not found'}`);
849
945
  if (targetClientId) {
850
946
  const clientWs = clientById.get(targetClientId);
@@ -888,7 +984,6 @@ function handlePluginConnection(ws, clientInfo) {
888
984
  cleanupPlugin(ws, id, `close:${code}`);
889
985
  });
890
986
 
891
- // 错误处理
892
987
  ws.on('error', (error) => {
893
988
  console.error(`[PLUGIN ERROR] ${id}:`, error.message);
894
989
 
@@ -900,6 +995,7 @@ function handlePluginConnection(ws, clientInfo) {
900
995
  });
901
996
 
902
997
  pluginConnections.delete(ws);
998
+ pluginNamespaces.delete(ws);
903
999
 
904
1000
  clientConnections.forEach(clientWs => {
905
1001
  if (clientWs.pairedPlugin === ws) {
@@ -922,6 +1018,7 @@ function handlePluginConnection(ws, clientInfo) {
922
1018
  type: 'connected',
923
1019
  role: 'plugin',
924
1020
  id: id,
1021
+ pluginId: ws.pluginId,
925
1022
  fresh: (Date.now() - SERVER_START_TIME) < 5000,
926
1023
  timestamp: Date.now()
927
1024
  }));
@@ -930,11 +1027,11 @@ function handlePluginConnection(ws, clientInfo) {
930
1027
  /**
931
1028
  * 处理 CDP 客户端连接 (Playwright/Puppeteer)
932
1029
  */
933
- function handleClientConnection(ws, clientInfo, customClientId = null) {
1030
+ function handleClientConnection(ws, clientInfo, customClientId = null, targetPluginId = null) {
934
1031
  clientConnections.add(ws);
935
1032
  const id = customClientId || generateId('client');
936
1033
  if (shouldLog('info')) {
937
- console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}`);
1034
+ console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}${targetPluginId ? ` targetPlugin=${targetPluginId}` : ''}`);
938
1035
  console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
939
1036
  console.log(` - Total client connections: ${clientConnections.size}`);
940
1037
  }
@@ -947,7 +1044,6 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
947
1044
  totalClients: clientConnections.size
948
1045
  });
949
1046
 
950
- // 检查是否有可用的 plugin 连接
951
1047
  if (pluginConnections.size === 0) {
952
1048
  if (shouldLog('warn')) {
953
1049
  console.log(` - WARNING: No plugin connections available!`);
@@ -965,27 +1061,36 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
965
1061
  }
966
1062
  }
967
1063
  } else {
968
- // 多客户端模式: 所有客户端共享同一个 plugin
969
- // 每个 clientId 对应不同的 tab
970
- const pluginWs = pluginConnections.values().next().value;
1064
+ let pluginWs;
1065
+ if (targetPluginId) {
1066
+ pluginWs = [...pluginConnections].find(p => p.pluginId === targetPluginId);
1067
+ if (!pluginWs) {
1068
+ if (shouldLog('warn')) {
1069
+ console.log(` - WARNING: Plugin ${targetPluginId} not found!`);
1070
+ }
1071
+ ws.close(4004, `Plugin ${targetPluginId} not found`);
1072
+ return;
1073
+ }
1074
+ } else {
1075
+ pluginWs = pluginConnections.values().next().value;
1076
+ }
971
1077
  if (pluginWs) {
972
1078
  connectionPairs.set(id, pluginWs);
973
1079
  ws.pairedPlugin = pluginWs;
1080
+ ws.targetPluginId = pluginWs.pluginId;
974
1081
  clientIdToPlugin.set(id, pluginWs);
975
1082
 
976
1083
  if (shouldLog('info')) {
977
- console.log(` - Paired with plugin: ${pluginWs.id} (shared mode)`);
1084
+ console.log(` - Paired with plugin: ${pluginWs.id} (pluginId=${pluginWs.pluginId})`);
978
1085
  }
979
1086
 
980
1087
  logConnectionEvent('CLIENT_PAIRED', { clientId: id, pluginId: pluginWs.id });
981
1088
 
982
- // 通知 Plugin 新客户端已连接
983
1089
  pluginWs.send(JSON.stringify({
984
1090
  type: 'client-connected',
985
1091
  clientId: id
986
1092
  }));
987
1093
 
988
- // 发送当前所有客户端列表
989
1094
  broadcastClientList();
990
1095
  }
991
1096
  }
@@ -1063,8 +1168,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
1063
1168
  }
1064
1169
  if (parsed && parsed.id !== undefined) {
1065
1170
  if (parsed.method === 'Target.closeTarget') {
1171
+ const pluginWs = ws.pairedPlugin;
1172
+ const ns = pluginWs ? getNamespace(pluginWs) : null;
1066
1173
  const targetId = parsed.params?.targetId;
1067
- const ownerClient = targetId ? targetIdToClientId.get(targetId) : null;
1174
+ const ownerClient = (ns && targetId) ? ns.targetIdToClientId.get(targetId) : null;
1068
1175
  if (ownerClient && ownerClient !== id) {
1069
1176
  console.log(`[BLOCKED] ${parsed.method} targetId=${targetId?.substring(0,8)} owner=${ownerClient?.substring(0,8)} requester=${id?.substring(0,8)} — not owner`);
1070
1177
  const errMsg = JSON.stringify({
@@ -1080,8 +1187,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
1080
1187
  currentMapping.closeTargetId = targetId;
1081
1188
  }
1082
1189
  } else if (parsed.method === 'Target.attachToTarget') {
1190
+ const pluginWs = ws.pairedPlugin;
1191
+ const ns = pluginWs ? getNamespace(pluginWs) : null;
1083
1192
  const targetId = parsed.params?.targetId;
1084
- const ownerClient = targetId ? targetIdToClientId.get(targetId) : null;
1193
+ const ownerClient = (ns && targetId) ? ns.targetIdToClientId.get(targetId) : null;
1085
1194
  if (ownerClient && ownerClient !== id) {
1086
1195
  console.log(`[BLOCKED] ${parsed.method} targetId=${targetId?.substring(0,8)} owner=${ownerClient?.substring(0,8)} requester=${id?.substring(0,8)} — not owner`);
1087
1196
  const errMsg = JSON.stringify({
@@ -1173,7 +1282,17 @@ function handlePageConnection(ws, clientInfo, targetId) {
1173
1282
  ws.lastActivityTime = Date.now();
1174
1283
  clientById.set(id, ws);
1175
1284
 
1176
- const plugin = pluginConnections.values().next().value;
1285
+ let plugin = null;
1286
+ for (const p of pluginConnections) {
1287
+ const ns = getNamespace(p);
1288
+ if (ns.targetIdToClientId.has(targetId)) {
1289
+ plugin = p;
1290
+ break;
1291
+ }
1292
+ }
1293
+ if (!plugin) {
1294
+ plugin = pluginConnections.values().next().value;
1295
+ }
1177
1296
  if (plugin && plugin.readyState === WebSocket.OPEN) {
1178
1297
  ws.pairedPlugin = plugin;
1179
1298
  if (shouldLog('info')) {
@@ -1209,7 +1328,7 @@ function handlePageConnection(ws, clientInfo, targetId) {
1209
1328
  // 对于全局 Target 事件,需要广播给所有客户端
1210
1329
  const broadcastEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
1211
1330
  if (broadcastEvents.includes(msg.method)) {
1212
- rewriteBrowserContextId(cdpMsg);
1331
+ rewriteBrowserContextId(cdpMsg, ws.pairedPlugin);
1213
1332
  console.log(`[PLUGIN -> ALL CLIENTS] Broadcasting ${msg.method}`);
1214
1333
  broadcastToClients(JSON.stringify(cdpMsg), null);
1215
1334
  } else {
@@ -1313,23 +1432,24 @@ function handlePageConnection(ws, clientInfo, targetId) {
1313
1432
  * 插件总是报告 'default',但 Playwright 期望自己创建的 context ID
1314
1433
  * 通过 openerId 找到对应的 clientId,再找到该 client 的 browserContextId
1315
1434
  */
1316
- function rewriteBrowserContextId(cdpMsg) {
1435
+ function rewriteBrowserContextId(cdpMsg, pluginWs) {
1317
1436
  const targetInfo = cdpMsg.params?.targetInfo;
1318
1437
  if (!targetInfo || targetInfo.browserContextId !== 'default') {
1319
1438
  return cdpMsg;
1320
1439
  }
1321
1440
 
1441
+ const ns = pluginWs ? getNamespace(pluginWs) : null;
1322
1442
  let clientId = null;
1323
1443
 
1324
- if (targetInfo.openerId) {
1325
- clientId = targetIdToClientId.get(targetInfo.openerId);
1444
+ if (targetInfo.openerId && ns) {
1445
+ clientId = ns.targetIdToClientId.get(targetInfo.openerId);
1326
1446
  }
1327
- if (!clientId && targetInfo.targetId) {
1328
- clientId = targetIdToClientId.get(targetInfo.targetId);
1447
+ if (!clientId && targetInfo.targetId && ns) {
1448
+ clientId = ns.targetIdToClientId.get(targetInfo.targetId);
1329
1449
  }
1330
1450
 
1331
- if (clientId) {
1332
- const contextId = clientIdToBrowserContext.get(clientId);
1451
+ if (clientId && ns) {
1452
+ const contextId = ns.clientIdToBrowserContext.get(clientId);
1333
1453
  if (contextId) {
1334
1454
  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
1455
  targetInfo.browserContextId = contextId;
@@ -1605,6 +1725,13 @@ setInterval(() => {
1605
1725
  };
1606
1726
  });
1607
1727
 
1728
+ let totalSessions = 0;
1729
+ let totalPendingAttach = 0;
1730
+ pluginNamespaces.forEach(ns => {
1731
+ totalSessions += ns.sessionToClientId.size;
1732
+ totalPendingAttach += ns.pendingAttachRequests.size;
1733
+ });
1734
+
1608
1735
  logStatus({
1609
1736
  timestamp: now,
1610
1737
  plugins: pluginConnections.size,
@@ -1614,8 +1741,8 @@ setInterval(() => {
1614
1741
  pairs: connectionPairs.size,
1615
1742
  pluginDetails: pluginList,
1616
1743
  clientDetails: clientList,
1617
- sessions: sessionToClientId.size,
1618
- pendingAttach: pendingAttachRequests.size
1744
+ sessions: totalSessions,
1745
+ pendingAttach: totalPendingAttach
1619
1746
  });
1620
1747
  }, CONFIG.STATUS_PRINT_INTERVAL);
1621
1748