cdp-tunnel 2.8.4 → 2.9.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.
@@ -466,14 +466,14 @@ importScripts('features/automation-badge.js');
466
466
  status = 'error';
467
467
  }
468
468
  }
469
- return { id: conn.id, tag: conn.tag, url: conn.url, status: status, attachedCount: attachedCount };
469
+ return { id: conn.id, tag: conn.tag, url: conn.url, mode: conn.mode || 'create', status: status, attachedCount: attachedCount };
470
470
  });
471
471
  sendResponse({ connections: list });
472
472
  });
473
473
  return true;
474
474
  } else if (message.type === 'add-connection') {
475
- Logger.info('[Runtime] Adding connection:', message.tag, message.url);
476
- Config.addConnection({ tag: message.tag, url: message.url }, function(conn) {
475
+ Logger.info('[Runtime] Adding connection:', message.tag, message.url, 'mode:', message.mode);
476
+ Config.addConnection({ tag: message.tag, url: message.url, mode: message.mode || 'create' }, function(conn) {
477
477
  if (conn && conn.enabled) {
478
478
  ConnectionManager.addConnection(conn);
479
479
  var entry = ConnectionManager.getConnection(conn.id);
@@ -42,9 +42,35 @@ var LocalHandler = (function() {
42
42
  var state = _getState(context);
43
43
  var params = context.params;
44
44
  var wsManager = context._wsManager;
45
+ var mode = context ? context.mode : null;
45
46
  state.setDiscoverTargets(!!(params && params.discover));
46
47
 
47
48
  if (params && params.discover) {
49
+ if (mode === 'takeover') {
50
+ return chrome.debugger.getTargets().then(function(targets) {
51
+ return new Promise(function(resolve) {
52
+ var pageTargets = targets.filter(function(t) { return t.type === 'page' && t.tabId; });
53
+ var checked = 0;
54
+ if (pageTargets.length === 0) { resolve({}); return; }
55
+ pageTargets.forEach(function(target) {
56
+ var tabId = target.tabId;
57
+ var targetId = target.id;
58
+ chrome.tabs.get(tabId, function(tab) {
59
+ checked++;
60
+ if (chrome.runtime.lastError || !tab) { if (checked === pageTargets.length) resolve({}); return; }
61
+ var isGrouped = tab.groupId != null && tab.groupId !== -1;
62
+ var isCDPCreated = state.isCDPCreatedTab(tabId);
63
+ if (!isGrouped && !isCDPCreated && !state.hasEmittedTarget(targetId)) {
64
+ state.addEmittedTarget(targetId);
65
+ var targetInfo = mapToTargetInfo(target);
66
+ EventBuilder.send('Target.targetCreated', { targetInfo: targetInfo }, null, wsManager);
67
+ }
68
+ if (checked === pageTargets.length) resolve({});
69
+ });
70
+ });
71
+ });
72
+ });
73
+ }
48
74
  return getTargetInfos().then(function(targets) {
49
75
  targets.forEach(function(targetInfo) {
50
76
  state.addEmittedTarget(targetInfo.targetId);
@@ -56,12 +82,51 @@ var LocalHandler = (function() {
56
82
  return Promise.resolve({});
57
83
  }
58
84
 
59
- function targetGetTargets() {
85
+ function targetGetTargets(context) {
86
+ var mode = context ? context.mode : null;
87
+ var state = context ? context._state : null;
88
+
89
+ if (mode === 'takeover') {
90
+ return chrome.debugger.getTargets().then(function(targets) {
91
+ return filterTakeoverTargetsFromRaw(targets, state);
92
+ });
93
+ }
60
94
  return getTargetInfos().then(function(targetInfos) {
61
95
  return { targetInfos: targetInfos };
62
96
  });
63
97
  }
64
98
 
99
+ function filterTakeoverTargetsFromRaw(targets, state) {
100
+ return new Promise(function(resolve) {
101
+ var pageTargets = targets.filter(function(t) { return t.type === 'page' && t.tabId; });
102
+ var nonPageTargets = targets.filter(function(t) { return t.type !== 'page'; }).map(mapToTargetInfo);
103
+
104
+ if (pageTargets.length === 0) {
105
+ resolve({ targetInfos: nonPageTargets });
106
+ return;
107
+ }
108
+
109
+ var checked = 0;
110
+ var filtered = [];
111
+ pageTargets.forEach(function(target) {
112
+ var tabId = target.tabId;
113
+ chrome.tabs.get(tabId, function(tab) {
114
+ checked++;
115
+ if (!chrome.runtime.lastError && tab) {
116
+ var isGrouped = tab.groupId != null && tab.groupId !== -1;
117
+ var isCDPCreated = state ? state.isCDPCreatedTab(tabId) : false;
118
+ if (!isGrouped && !isCDPCreated) {
119
+ filtered.push(mapToTargetInfo(target));
120
+ }
121
+ }
122
+ if (checked === pageTargets.length) {
123
+ resolve({ targetInfos: nonPageTargets.concat(filtered) });
124
+ }
125
+ });
126
+ });
127
+ });
128
+ }
129
+
65
130
  function targetGetTargetInfo(params) {
66
131
  return getFallbackTargetId().then(function(fallbackId) {
67
132
  var targetId = (params && params.targetId) || fallbackId;
@@ -82,6 +82,10 @@ var SpecialHandler = (function() {
82
82
 
83
83
  if (state.isCDPCreatedTab(tabId)) {
84
84
  addTabToAutomationGroup(tabId, clientId, null, context);
85
+ } else if (context.mode === 'takeover') {
86
+ state.addPreExistingTab(tabId);
87
+ addTabToAutomationGroup(tabId, clientId, null, context);
88
+ Logger.info('[CDP TAKEOVER] Target.attachToTarget: added to TAKE group. tabId:', tabId);
85
89
  } else {
86
90
  state.addPreExistingTab(tabId);
87
91
  Logger.info('[CDP] Target.attachToTarget: user tab not CDP-created, treating as pre-existing. tabId:', tabId);
@@ -236,7 +240,7 @@ var SpecialHandler = (function() {
236
240
  return;
237
241
  }
238
242
  }
239
- var baseName = CDPUtils.getGroupBaseName(groupClientId, _getConnectionTag(context));
243
+ var baseName = CDPUtils.getGroupBaseName(groupClientId, _getConnectionTag(context), context ? context.mode : null);
240
244
 
241
245
  Logger.info('[TabGroup] Grouping tab immediately for:', baseName);
242
246
  doGroup(tabId, groupClientId, baseName, 0, callback, context);
@@ -258,7 +262,7 @@ var SpecialHandler = (function() {
258
262
  Logger.info('[TabGroup] Using cached groupId:', cachedGroupId, 'for client:', clientId);
259
263
  chrome.tabs.group({ tabIds: tabId, groupId: cachedGroupId }, function(result) {
260
264
  if (!chrome.runtime.lastError) {
261
- updateTabGroupName(clientId, state, wsManager);
265
+ updateTabGroupName(clientId, state, wsManager, context ? context.mode : null);
262
266
  Logger.info('[TabGroup] Tab', tabId, 'added to cached group:', cachedGroupId);
263
267
  if (callback) callback(true);
264
268
  return;
@@ -303,7 +307,7 @@ var SpecialHandler = (function() {
303
307
  }
304
308
  } else {
305
309
  if (state) state.setGroupIdForClient(clientId, existing.id);
306
- updateTabGroupName(clientId, state, wsManager);
310
+ updateTabGroupName(clientId, state, wsManager, context ? context.mode : null);
307
311
  Logger.info('[TabGroup] Tab', tabId, 'added to existing group:', existing.id);
308
312
  if (callback) callback(true);
309
313
  }
@@ -335,7 +339,7 @@ var SpecialHandler = (function() {
335
339
  EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'updateGroup', error: chrome.runtime.lastError.message, groupId: groupId }, null, wsManager);
336
340
  } else {
337
341
  if (state) state.setGroupIdForClient(clientId, groupId);
338
- updateTabGroupName(clientId, state, wsManager);
342
+ updateTabGroupName(clientId, state, wsManager, context ? context.mode : null);
339
343
  Logger.info('[TabGroup] Group updated:', groupId, baseName);
340
344
  }
341
345
  if (callback) callback(true);
@@ -354,7 +358,7 @@ var SpecialHandler = (function() {
354
358
  });
355
359
  }
356
360
 
357
- function updateTabGroupName(clientId, state, wsManager) {
361
+ function updateTabGroupName(clientId, state, wsManager, mode) {
358
362
  if (!clientId) return;
359
363
 
360
364
  var groupId = state ? state.getGroupIdForClient(clientId) : null;
@@ -365,7 +369,7 @@ var SpecialHandler = (function() {
365
369
  chrome.tabs.query({ groupId: groupId }, function(tabs) {
366
370
  if (chrome.runtime.lastError || !tabs) return;
367
371
 
368
- var baseName = CDPUtils.getGroupBaseName(clientId, connectionTag);
372
+ var baseName = CDPUtils.getGroupBaseName(clientId, connectionTag, mode);
369
373
  var newName = baseName + ' (' + tabs.length + ')';
370
374
 
371
375
  chrome.tabGroups.update(groupId, {
@@ -407,7 +411,7 @@ var SpecialHandler = (function() {
407
411
  chrome.tabs.remove(tabId, function() {
408
412
  state.removeAttachedTab(tabId);
409
413
  if (closeClientId) {
410
- updateTabGroupName(closeClientId, state, _getWSManager(context));
414
+ updateTabGroupName(closeClientId, state, _getWSManager(context), context ? context.mode : null);
411
415
  }
412
416
  resolve({ success: true });
413
417
  });
@@ -516,12 +520,60 @@ function checkTabVisibility(tabId) {
516
520
  var state = _getState(context);
517
521
  var wsManager = _getWSManager(context);
518
522
  var clientId = context ? context.clientId : null;
523
+ var mode = context ? context.mode : null;
519
524
  var config = state.getAutoAttachConfig();
520
525
 
521
526
  return chrome.debugger.getTargets().then(function(targets) {
522
527
  var promises = [];
523
528
 
524
- Logger.info('[CDP] emitAutoAttachForExistingTargets: checking', targets.length, 'targets, clientId:', clientId);
529
+ Logger.info('[CDP] emitAutoAttachForExistingTargets: checking', targets.length, 'targets, clientId:', clientId, 'mode:', mode);
530
+
531
+ if (mode === 'takeover') {
532
+ var takeoverPromises = [];
533
+ targets.forEach(function(target) {
534
+ if (target.type !== 'page' || !target.tabId) return;
535
+ var targetId = target.id;
536
+ var tabId = target.tabId;
537
+ if (state.hasEmittedTarget(targetId)) return;
538
+
539
+ takeoverPromises.push(new Promise(function(resolve) {
540
+ chrome.tabs.get(tabId, function(tab) {
541
+ if (chrome.runtime.lastError || !tab) { resolve(); return; }
542
+ var isGrouped = tab.groupId != null && tab.groupId !== -1;
543
+ var isCDPCreated = state.isCDPCreatedTab(tabId);
544
+ if (isGrouped || isCDPCreated) { resolve(); return; }
545
+
546
+ state.addEmittedTarget(targetId);
547
+ state.addPreExistingTab(tabId);
548
+ if (clientId) state.setTabIdToClientId(tabId, clientId);
549
+ var targetInfo = LocalHandler.mapToTargetInfo(target);
550
+ Logger.info('[CDP TAKEOVER] Emitting ungrouped target:', targetId, 'tabId:', tabId);
551
+
552
+ var attachLogic = function(attached) {
553
+ var sessionId = CDPUtils.generateSessionId();
554
+ state.mapSession(sessionId, tabId, targetId);
555
+ EventBuilder.send('Target.attachedToTarget', {
556
+ sessionId: sessionId,
557
+ targetInfo: Object.assign({}, targetInfo, { attached: true }),
558
+ waitingForDebugger: false
559
+ }, null, wsManager);
560
+ };
561
+
562
+ if (target.attached) {
563
+ attachLogic(true);
564
+ resolve();
565
+ } else {
566
+ DebuggerManager.attach(tabId, state).then(function(attached) {
567
+ if (!attached) { resolve(); return; }
568
+ attachLogic(attached);
569
+ resolve();
570
+ }).catch(resolve);
571
+ }
572
+ });
573
+ }));
574
+ });
575
+ return Promise.all(takeoverPromises);
576
+ }
525
577
 
526
578
  targets.forEach(function(target) {
527
579
  if (target.type !== 'page' && target.type !== 'background_page') return;
@@ -78,7 +78,7 @@ function routeCDPCommand(message, connState, wsManager) {
78
78
  var logType = route ? route.type : 'FORWARD';
79
79
  Logger.info('[CDP] RECV id=' + id + ' method=' + method + ' type=' + logType + ' sessionId=' + (sessionId || 'null') + ' clientId=' + (clientId || 'null'));
80
80
 
81
- var ctx = { id: id, method: method, params: params, sessionId: sessionId, clientId: clientId, _state: state, _wsManager: wsManager };
81
+ var ctx = { id: id, method: method, params: params, sessionId: sessionId, clientId: clientId, mode: message.mode, _state: state, _wsManager: wsManager };
82
82
 
83
83
  return new Promise(function(resolve) {
84
84
  if (route) {
@@ -814,6 +814,10 @@
814
814
  <div class="add-conn-form">
815
815
  <input type="text" class="input-tag" id="newConnTag" placeholder="名称" value="local">
816
816
  <input type="text" class="input-url" id="newConnUrl" placeholder="ws://localhost:9221/plugin" value="ws://localhost:9221/plugin">
817
+ <select id="inputMode" style="padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;outline:none;transition:border-color 0.2s;flex-shrink:0;">
818
+ <option value="create">创建模式</option>
819
+ <option value="takeover">接管模式</option>
820
+ </select>
817
821
  <button class="btn btn-primary btn-sm" id="addConnBtn">添加连接</button>
818
822
  </div>
819
823
  </div>
@@ -27,6 +27,7 @@
27
27
  newConnTag: document.getElementById('newConnTag'),
28
28
  newConnUrl: document.getElementById('newConnUrl'),
29
29
  addConnBtn: document.getElementById('addConnBtn'),
30
+ inputMode: document.getElementById('inputMode'),
30
31
  autoMuteToggle: document.getElementById('autoMuteToggle'),
31
32
  pluginIdDisplay: document.getElementById('pluginIdDisplay')
32
33
  };
@@ -99,7 +100,7 @@
99
100
  '<input type="checkbox" class="conn-toggle" data-id="' + conn.id + '"' + (conn.enabled ? ' checked' : '') + ' title="启用/禁用">' +
100
101
  '<span class="status-dot ' + statusClass + '" title="' + statusClass + '"></span>' +
101
102
  '<div class="conn-config-info">' +
102
- '<div class="conn-config-tag">' + escapeHtml(conn.tag) + '</div>' +
103
+ '<div class="conn-config-tag">' + (conn.mode === 'takeover' ? '🔗 ' : '🆕 ') + escapeHtml(conn.tag) + '</div>' +
103
104
  '<div class="conn-config-url" title="' + escapeAttr(conn.url) + '">' + escapeHtml(conn.url) + '</div>' +
104
105
  '</div>' +
105
106
  '<button class="btn-delete" data-id="' + conn.id + '" title="删除">删除</button>' +
@@ -333,6 +334,7 @@
333
334
  elements.addConnBtn.addEventListener('click', function() {
334
335
  var tag = elements.newConnTag.value.trim();
335
336
  var url = elements.newConnUrl.value.trim();
337
+ var mode = elements.inputMode.value || 'create';
336
338
 
337
339
  if (!tag) {
338
340
  showToast('请输入连接名称', 'error');
@@ -344,16 +346,18 @@
344
346
  }
345
347
 
346
348
  if (typeof chrome !== 'undefined' && chrome.runtime) {
347
- chrome.runtime.sendMessage({ type: 'add-connection', tag: tag, url: url }, function() {
349
+ chrome.runtime.sendMessage({ type: 'add-connection', tag: tag, url: url, mode: mode }, function() {
348
350
  elements.newConnTag.value = '';
349
351
  elements.newConnUrl.value = '';
352
+ elements.inputMode.value = 'create';
350
353
  loadAndRenderConnections();
351
354
  showToast('连接已添加');
352
355
  });
353
356
  } else if (typeof Config !== 'undefined') {
354
- Config.addConnection({ tag: tag, url: url }, function() {
357
+ Config.addConnection({ tag: tag, url: url, mode: mode }, function() {
355
358
  elements.newConnTag.value = '';
356
359
  elements.newConnUrl.value = '';
360
+ elements.inputMode.value = 'create';
357
361
  loadAndRenderConnections();
358
362
  showToast('连接已添加');
359
363
  });
@@ -23,7 +23,7 @@ var ConnectionManager = (function() {
23
23
  return;
24
24
  }
25
25
 
26
- var state = new ConnectionState(config.id);
26
+ var state = new ConnectionState(config.id, config.mode);
27
27
  var wsManager = new WebSocketConnection(config.id, state, config);
28
28
 
29
29
  _connections.set(config.id, {
@@ -1,5 +1,6 @@
1
- function ConnectionState(connectionId) {
1
+ function ConnectionState(connectionId, mode) {
2
2
  this.connectionId = connectionId;
3
+ this.mode = mode || 'create';
3
4
  this.ws = null;
4
5
  this.reconnectTimer = null;
5
6
  this._hasConnectedClient = false;
@@ -227,7 +227,54 @@ var WebSocketConnection = (function() {
227
227
  Logger.info('[WS:' + self.connectionId + '] Client connected, resuming event forwarding');
228
228
  self.state.setHasConnectedClient(true);
229
229
  self.state.addCDPClient(message.clientId, message.clientId);
230
- self._createGroupForClient(message.clientId);
230
+ self._createGroupForClient(message.clientId, message.__mode);
231
+ self._broadcastStateUpdate();
232
+ break;
233
+
234
+ case 'takeover-disconnect':
235
+ Logger.info('[WS:' + self.connectionId + '] Takeover disconnect:', message.clientId);
236
+ var takeClientId = message.clientId;
237
+ self._groupCreationPending.delete(takeClientId);
238
+ var takeAttachedTabs = self.state.getAttachedTabIds();
239
+ var takeToDetach = takeAttachedTabs.filter(function(tid) {
240
+ return self.state.getClientIdByTabId(tid) === takeClientId;
241
+ });
242
+ var takeTargetIds = [];
243
+ self.state.sessionIdToTargetId.forEach(function(tTargetId, sessId) {
244
+ var tTabId = self.state.sessionIdToTabId.get(sessId);
245
+ if (tTabId && self.state.getClientIdByTabId(tTabId) === takeClientId) {
246
+ takeTargetIds.push(tTargetId);
247
+ }
248
+ });
249
+ takeToDetach.forEach(function(tid) {
250
+ chrome.debugger.detach({ tabId: tid }).catch(function() {});
251
+ self.state.removeAttachedTab(tid);
252
+ });
253
+ takeTargetIds.forEach(function(tTargetId) {
254
+ self.state.emittedTargets.delete(tTargetId);
255
+ });
256
+ self.state.clearPreExistingTabsForClient(takeClientId);
257
+ var takeSessions = self.state.sessionIdToTabId.entries();
258
+ var takeEntry = takeSessions.next();
259
+ while (!takeEntry.done) {
260
+ var sessId = takeEntry.value[0];
261
+ var tTabId = takeEntry.value[1];
262
+ if (self.state.getClientIdByTabId(tTabId) === takeClientId) {
263
+ self.state.unmapSession(sessId);
264
+ }
265
+ takeEntry = takeSessions.next();
266
+ }
267
+ takeToDetach.forEach(function(tid) {
268
+ self.state.removeTabIdToClientId(tid);
269
+ var sessions = self.state.findSessionsByTabId(tid);
270
+ sessions.forEach(function(sid) { self.state.unmapSession(sid); });
271
+ });
272
+ self.state.removeGroupForClient(takeClientId);
273
+ self.state.removeCDPClient(takeClientId);
274
+ if (self.state.getCDPClients().length === 0) {
275
+ self.state.setHasConnectedClient(false);
276
+ }
277
+ self._cleanupStaleState(takeClientId);
231
278
  self._broadcastStateUpdate();
232
279
  break;
233
280
 
@@ -282,6 +329,7 @@ var WebSocketConnection = (function() {
282
329
  tabId: tabId,
283
330
  sessionId: sessionId,
284
331
  clientId: message.__clientId,
332
+ mode: message.__mode,
285
333
  connectionId: self.connectionId
286
334
  }, self.state, self);
287
335
  }
@@ -296,6 +344,7 @@ var WebSocketConnection = (function() {
296
344
  tabId: tabId,
297
345
  sessionId: sessionId,
298
346
  clientId: message.__clientId,
347
+ mode: message.__mode,
299
348
  connectionId: self.connectionId
300
349
  }, self.state, self);
301
350
  }
@@ -324,9 +373,10 @@ var WebSocketConnection = (function() {
324
373
  resolve();
325
374
  });
326
375
  } else {
327
- var baseName = CDPUtils.getGroupBaseName(clientId, self.config ? self.config.tag : null);
376
+ var cdpBaseName = CDPUtils.getGroupBaseName(clientId, self.config ? self.config.tag : null, 'create');
377
+ var takeBaseName = CDPUtils.getGroupBaseName(clientId, self.config ? self.config.tag : null, 'takeover');
328
378
  chrome.tabGroups.query({}, function(allGroups) {
329
- var match = CDPUtils.findGroupByName(allGroups, baseName);
379
+ var match = CDPUtils.findGroupByName(allGroups, cdpBaseName) || CDPUtils.findGroupByName(allGroups, takeBaseName);
330
380
  if (match) {
331
381
  self._closeGroupById(match.id, clientId, function() {
332
382
  clearTimeout(timeoutId);
@@ -488,7 +538,7 @@ var WebSocketConnection = (function() {
488
538
  });
489
539
  };
490
540
 
491
- WebSocketConnection.prototype._createGroupForClient = function(clientId) {
541
+ WebSocketConnection.prototype._createGroupForClient = function(clientId, mode) {
492
542
  var self = this;
493
543
  if (!clientId || !chrome.tabGroups) return;
494
544
 
@@ -505,7 +555,7 @@ var WebSocketConnection = (function() {
505
555
 
506
556
  self._groupCreationPending.add(clientId);
507
557
 
508
- var baseName = CDPUtils.getGroupBaseName(clientId, self.config ? self.config.tag : null);
558
+ var baseName = CDPUtils.getGroupBaseName(clientId, self.config ? self.config.tag : null, mode);
509
559
  chrome.tabs.query({ currentWindow: true }, function(tabs) {
510
560
  if (!tabs || tabs.length === 0) {
511
561
  Logger.warn('[WS:' + self.connectionId + '] No tabs found for group creation');
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "CDP Bridge",
4
- "version": "2.8.4",
4
+ "version": "2.9.1",
5
5
  "description": "Chrome DevTools Protocol Bridge for Playwright/Puppeteer automation",
6
6
  "permissions": [
7
7
  "debugger",
@@ -32,9 +32,11 @@
32
32
  var dot = document.createElement('span');
33
33
  dot.className = 'conn-dot ' + conn.status;
34
34
 
35
+ var modeIcon = conn.mode === 'takeover' ? '🔗 ' : '🆕 ';
36
+
35
37
  var tag = document.createElement('span');
36
38
  tag.className = 'conn-tag';
37
- tag.textContent = conn.tag;
39
+ tag.textContent = modeIcon + conn.tag;
38
40
 
39
41
  header.appendChild(dot);
40
42
  header.appendChild(tag);
@@ -1,5 +1,5 @@
1
1
  var Config = {
2
- WS_URL: 'ws://localhost:9221/plugin',
2
+ WS_URL: 'ws://localhost:19065/plugin',
3
3
  RECONNECT_DELAY: 3000,
4
4
  DEBUGGER_VERSION: '1.3',
5
5
  HEARTBEAT_INTERVAL: 25000,
@@ -58,6 +58,7 @@ var Config = {
58
58
  id: 'conn_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6),
59
59
  tag: opts.tag || 'unnamed',
60
60
  url: opts.url || '',
61
+ mode: opts.mode || 'create',
61
62
  enabled: opts.enabled !== undefined ? opts.enabled : true
62
63
  };
63
64
  connections.push(conn);
@@ -91,6 +92,7 @@ var Config = {
91
92
  if (c.id === id) {
92
93
  if (updates.tag !== undefined) c.tag = updates.tag;
93
94
  if (updates.url !== undefined) c.url = updates.url;
95
+ if (updates.mode !== undefined) c.mode = updates.mode;
94
96
  }
95
97
  });
96
98
  Config.setConnections(connections, callback);
@@ -42,7 +42,7 @@ var CDPUtils = (function() {
42
42
  return 0;
43
43
  }
44
44
 
45
- function buildGroupName(clientId, connectionTag) {
45
+ function buildGroupName(clientId, connectionTag, mode) {
46
46
  if (!clientId) return 'CDP';
47
47
  var hash = 0;
48
48
  for (var i = 0; i < clientId.length; i++) {
@@ -52,11 +52,12 @@ var CDPUtils = (function() {
52
52
  }
53
53
  var suffix = Math.abs(hash).toString(16).substring(0, 8).padStart(8, '0');
54
54
  var tag = (connectionTag && connectionTag !== 'default') ? connectionTag + '-' : '';
55
- return 'CDP-' + tag + suffix;
55
+ var prefix = (mode === 'takeover') ? 'TAKE-' : 'CDP-';
56
+ return prefix + tag + suffix;
56
57
  }
57
58
 
58
- function getGroupBaseName(clientId, connectionTag) {
59
- return buildGroupName(clientId, connectionTag);
59
+ function getGroupBaseName(clientId, connectionTag, mode) {
60
+ return buildGroupName(clientId, connectionTag, mode);
60
61
  }
61
62
 
62
63
  function findGroupByName(allGroups, baseName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "2.8.4",
3
+ "version": "2.9.1",
4
4
  "description": "Bridge Chrome's debugger API to WebSocket — control your existing browser with Playwright/Puppeteer via CDP",
5
5
  "main": "server/proxy-server.js",
6
6
  "bin": "./cli/index.js",
@@ -10,7 +10,8 @@ const CONFIG = {
10
10
  LOG_LEVEL: process.env.LOG_LEVEL || 'info',
11
11
  AUTO_RESTART: process.env.AUTO_RESTART === 'true',
12
12
  CHROME_RESTART_COOLDOWN: 30000,
13
- PLUGIN_MAX_MISSED_PINGS: 3
13
+ PLUGIN_MAX_MISSED_PINGS: 3,
14
+ TAKEOVER_PORT: process.env.TAKEOVER_PORT ? parseInt(process.env.TAKEOVER_PORT) : (parseInt(process.env.PORT || '9221') + 1)
14
15
  };
15
16
 
16
17
  const LOG_LEVELS = {
@@ -16,6 +16,7 @@ const path = require('path');
16
16
  const os = require('os');
17
17
  const { execSync, spawn: spawnProcess } = require('child_process');
18
18
  const { CONFIG, BROWSER_ID, shouldLog } = require('./modules/config');
19
+ const TAKEOVER_PORT = CONFIG.TAKEOVER_PORT;
19
20
  const { logCDP, logEvent, clearLog, logStatus, logConnectionEvent, flushAllLogs, logDisconnect } = require('./modules/logger');
20
21
 
21
22
  try {
@@ -195,6 +196,8 @@ console.log(` Server started on port ${PORT}`);
195
196
  console.log(` - Plugin path: ws://localhost:${PORT}/plugin`);
196
197
  console.log(` - Client path: ws://localhost:${PORT}/client`);
197
198
  console.log(` - CDP endpoint: http://localhost:${PORT}`);
199
+ console.log(` - Takeover port: ${TAKEOVER_PORT} (mode=takeover)`);
200
+ console.log(` - Takeover CDP: http://localhost:${TAKEOVER_PORT}`);
198
201
  console.log('='.repeat(60));
199
202
 
200
203
  /**
@@ -436,7 +439,8 @@ wss.on('connection', (ws, req) => {
436
439
  } else if (pathParts[0] === 'devtools' && pathParts[1] === 'browser' && pathParts[2]) {
437
440
  targetPluginId = pathParts[2];
438
441
  }
439
- handleClientConnection(ws, clientInfo, customClientId, targetPluginId);
442
+ const mode = req._takeoverMode ? 'takeover' : 'create';
443
+ handleClientConnection(ws, clientInfo, customClientId, targetPluginId, mode);
440
444
  } else if (path.startsWith('/devtools/page/')) {
441
445
  const targetId = path.replace('/devtools/page/', '');
442
446
  handlePageConnection(ws, clientInfo, targetId);
@@ -449,6 +453,7 @@ wss.on('connection', (ws, req) => {
449
453
  function cleanupClient(ws, id, reason) {
450
454
  const pluginWs = ws.pairedPlugin || clientIdToPlugin.get(id);
451
455
  const ns = pluginWs ? getNamespace(pluginWs) : null;
456
+ const isTakeover = ws.mode === 'takeover';
452
457
 
453
458
  if (ns) {
454
459
  const sessionsToClean = [];
@@ -467,6 +472,7 @@ function cleanupClient(ws, id, reason) {
467
472
  logConnectionEvent('CLIENT_DISCONNECTED', {
468
473
  id,
469
474
  reason,
475
+ mode: ws.mode || 'create',
470
476
  totalPlugins: pluginConnections.size,
471
477
  totalClients: clientConnections.size
472
478
  });
@@ -474,6 +480,7 @@ function cleanupClient(ws, id, reason) {
474
480
  logDisconnect('CLIENT_CLEANUP', {
475
481
  clientId: id,
476
482
  reason,
483
+ mode: ws.mode || 'create',
477
484
  cdpMethodsUsed: ws.cdpTrace ? [...new Set(ws.cdpTrace)] : [],
478
485
  uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
479
486
  remainingClients: clientConnections.size,
@@ -487,32 +494,41 @@ function cleanupClient(ws, id, reason) {
487
494
  }
488
495
 
489
496
  if (ws.pairedPlugin) {
490
- const sendOk = safeSend(ws.pairedPlugin, JSON.stringify({
491
- type: 'client-disconnected',
492
- clientId: id,
493
- sessions: []
494
- }), 'plugin');
495
- if (!sendOk) {
496
- console.log(`[WARN] cleanupClient: failed to send client-disconnected for ${id} to plugin`);
497
- }
498
-
499
- const pluginNs = getNamespace(ws.pairedPlugin);
500
- if (pluginNs) {
501
- const targetsToClose = [];
502
- for (const [tId, cId] of pluginNs.targetIdToClientId.entries()) {
503
- if (cId === id) {
504
- targetsToClose.push(tId);
505
- }
497
+ if (isTakeover) {
498
+ safeSend(ws.pairedPlugin, JSON.stringify({
499
+ type: 'takeover-disconnect',
500
+ clientId: id,
501
+ sessions: []
502
+ }), 'plugin');
503
+ console.log(`[TAKEOVER DISCONNECT] client=${id} detaching only, not closing tabs`);
504
+ } else {
505
+ const sendOk = safeSend(ws.pairedPlugin, JSON.stringify({
506
+ type: 'client-disconnected',
507
+ clientId: id,
508
+ sessions: []
509
+ }), 'plugin');
510
+ if (!sendOk) {
511
+ console.log(`[WARN] cleanupClient: failed to send client-disconnected for ${id} to plugin`);
506
512
  }
507
- targetsToClose.forEach(function(tId) {
508
- const closeReq = JSON.stringify({
509
- id: -1,
510
- method: 'Target.closeTarget',
511
- params: { targetId: tId },
512
- __clientId: id
513
+
514
+ const pluginNs = getNamespace(ws.pairedPlugin);
515
+ if (pluginNs) {
516
+ const targetsToClose = [];
517
+ for (const [tId, cId] of pluginNs.targetIdToClientId.entries()) {
518
+ if (cId === id) {
519
+ targetsToClose.push(tId);
520
+ }
521
+ }
522
+ targetsToClose.forEach(function(tId) {
523
+ const closeReq = JSON.stringify({
524
+ id: -1,
525
+ method: 'Target.closeTarget',
526
+ params: { targetId: tId },
527
+ __clientId: id
528
+ });
529
+ safeSend(ws.pairedPlugin, closeReq, 'plugin');
513
530
  });
514
- safeSend(ws.pairedPlugin, closeReq, 'plugin');
515
- });
531
+ }
516
532
  }
517
533
  }
518
534
 
@@ -819,7 +835,23 @@ function handlePluginConnection(ws, clientInfo, request) {
819
835
  pendingMap.set(targetId, { parsed: JSON.parse(JSON.stringify(parsed)), cdpData });
820
836
  console.log(`[TARGET EVENT PENDING] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (cached, waiting for createTarget response)`);
821
837
  } else {
822
- console.log(`[TARGET EVENT DROPPED] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (no owner, dropped for isolation)`);
838
+ let takeoverRouted = false;
839
+ ns.discoveringClientIds.forEach((timestamp, discClientId) => {
840
+ if (takeoverRouted) return;
841
+ const discWs = clientById.get(discClientId);
842
+ if (discWs && discWs.mode === 'takeover' && discWs.readyState === WebSocket.OPEN) {
843
+ discWs.send(cdpData);
844
+ takeoverRouted = true;
845
+ console.log(`[TAKEOVER EVENT ROUTED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> clientId=${discClientId}`);
846
+ if (parsed.params?.sessionId) {
847
+ ns.sessionToClientId.set(parsed.params.sessionId, discClientId);
848
+ }
849
+ ns.targetIdToClientId.set(targetId, discClientId);
850
+ }
851
+ });
852
+ if (!takeoverRouted) {
853
+ console.log(`[TARGET EVENT DROPPED] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (no owner, dropped for isolation)`);
854
+ }
823
855
  }
824
856
  } else {
825
857
  console.log(`[TARGET EVENT DROPPED] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (no owner, dropped for isolation)`);
@@ -893,6 +925,10 @@ function handlePluginConnection(ws, clientInfo, request) {
893
925
  if (parsed.result?.sessionId && mapping.method === 'Target.attachToTarget') {
894
926
  ns.sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
895
927
  console.log(`[SESSION MAPPED from attach response] sessionId=${parsed.result.sessionId?.substring(0,8)} -> clientId=${mapping.clientId?.substring(0,8)}`);
928
+ if (mapping.attachTargetId && !ns.targetIdToClientId.has(mapping.attachTargetId)) {
929
+ ns.targetIdToClientId.set(mapping.attachTargetId, mapping.clientId);
930
+ console.log(`[TARGET MAPPED from attach] targetId=${mapping.attachTargetId?.substring(0,8)} -> clientId=${mapping.clientId}`);
931
+ }
896
932
  }
897
933
 
898
934
  if (mapping.isCreateTarget && parsed.result?.targetId) {
@@ -937,12 +973,16 @@ function handlePluginConnection(ws, clientInfo, request) {
937
973
  }
938
974
  if (mapping.isGetTargets && parsed.result && parsed.result.targetInfos) {
939
975
  const clientId = mapping.clientId;
940
- parsed.result.targetInfos = parsed.result.targetInfos.filter(t => {
941
- if (t.type !== 'page') return true;
942
- const ownerClient = ns.targetIdToClientId.get(t.targetId);
943
- return ownerClient === clientId;
944
- });
945
- console.log(`[GET TARGETS FILTERED] client=${clientId} returned ${parsed.result.targetInfos.filter(t => t.type === 'page').length} page targets`);
976
+ if (mapping.isTakeover) {
977
+ console.log(`[GET TARGETS TAKEOVER] client=${clientId} returning unfiltered targets (${parsed.result.targetInfos.length})`);
978
+ } else {
979
+ parsed.result.targetInfos = parsed.result.targetInfos.filter(t => {
980
+ if (t.type !== 'page') return true;
981
+ const ownerClient = ns.targetIdToClientId.get(t.targetId);
982
+ return ownerClient === clientId;
983
+ });
984
+ console.log(`[GET TARGETS FILTERED] client=${clientId} returned ${parsed.result.targetInfos.filter(t => t.type === 'page').length} page targets`);
985
+ }
946
986
  }
947
987
  if (parsed.result && parsed.result.success !== undefined && mapping.method === 'Target.closeTarget') {
948
988
  if (mapping.closeTargetId) {
@@ -1123,11 +1163,12 @@ function forwardToPlugin(clientWs, data, clientId) {
1123
1163
  /**
1124
1164
  * 处理 CDP 客户端连接 (Playwright/Puppeteer)
1125
1165
  */
1126
- function handleClientConnection(ws, clientInfo, customClientId = null, targetPluginId = null) {
1166
+ function handleClientConnection(ws, clientInfo, customClientId = null, targetPluginId = null, mode = 'create') {
1127
1167
  clientConnections.add(ws);
1128
1168
  const id = customClientId || generateId('client');
1169
+ ws.mode = mode;
1129
1170
  if (shouldLog('info')) {
1130
- console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}${targetPluginId ? ` targetPlugin=${targetPluginId}` : ''}`);
1171
+ console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}${targetPluginId ? ` targetPlugin=${targetPluginId}` : ''} mode=${mode}`);
1131
1172
  console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
1132
1173
  console.log(` - Total client connections: ${clientConnections.size}`);
1133
1174
  }
@@ -1296,6 +1337,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1296
1337
  safeSend(ws, errMsg, 'client');
1297
1338
  return;
1298
1339
  }
1340
+ const currentMapping = globalRequestIdMap.get(parsed.id);
1341
+ if (currentMapping && targetId) {
1342
+ currentMapping.attachTargetId = targetId;
1343
+ }
1299
1344
  }
1300
1345
  }
1301
1346
 
@@ -1311,6 +1356,9 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1311
1356
  const currentMapping = globalRequestIdMap.get(parsed.id);
1312
1357
  if (currentMapping) {
1313
1358
  currentMapping.isGetTargets = true;
1359
+ if (ws.mode === 'takeover') {
1360
+ currentMapping.isTakeover = true;
1361
+ }
1314
1362
  }
1315
1363
  }
1316
1364
 
@@ -1324,17 +1372,31 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1324
1372
 
1325
1373
  if (parsed && parsed.method === 'Target.setAutoAttach' && parsed.params?.autoAttach && !ws._autoDefaultPageSent) {
1326
1374
  ws._autoDefaultPageSent = true;
1327
- autoCreateDefaultPageAndForward(ws, parsed, modifiedData, id, originalId);
1375
+ if (ws.mode === 'takeover') {
1376
+ const takeoverMsg = { ...parsed, __clientId: id, __mode: 'takeover' };
1377
+ if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
1378
+ ws.pairedPlugin.send(JSON.stringify(takeoverMsg));
1379
+ }
1380
+ } else {
1381
+ autoCreateDefaultPageAndForward(ws, parsed, modifiedData, id, originalId);
1382
+ }
1328
1383
  return;
1329
1384
  }
1330
1385
 
1331
1386
  if (parsed && parsed.method === 'Browser.close') {
1332
- console.log(`[BROWSER CLOSE] Client ${id} requested Browser.close — closing client group only`);
1387
+ console.log(`[BROWSER CLOSE] Client ${id} mode=${ws.mode} requested Browser.close`);
1333
1388
  if (ws.pairedPlugin) {
1334
- safeSend(ws.pairedPlugin, JSON.stringify({
1335
- type: 'browser-close',
1336
- clientId: id
1337
- }), 'plugin');
1389
+ if (ws.mode === 'takeover') {
1390
+ safeSend(ws.pairedPlugin, JSON.stringify({
1391
+ type: 'takeover-disconnect',
1392
+ clientId: id
1393
+ }), 'plugin');
1394
+ } else {
1395
+ safeSend(ws.pairedPlugin, JSON.stringify({
1396
+ type: 'browser-close',
1397
+ clientId: id
1398
+ }), 'plugin');
1399
+ }
1338
1400
  }
1339
1401
  safeSend(ws, JSON.stringify({ id: originalId, result: {} }), 'client');
1340
1402
  return;
@@ -1353,8 +1415,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1353
1415
  // 发送给配对的 plugin (或广播)
1354
1416
  if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
1355
1417
  console.log(`[SEND TO PLUGIN] id=${parsed?.id} method=${parsed?.method} sessionId=${parsed?.sessionId?.substring(0,8) || 'none'} clientId=${id}`);
1356
- // 在消息中附加 clientId 信息
1357
1418
  const pluginMsg = { ...parsed, __clientId: id };
1419
+ if (ws.mode === 'takeover') {
1420
+ pluginMsg.__mode = 'takeover';
1421
+ }
1358
1422
  ws.pairedPlugin.send(JSON.stringify(pluginMsg));
1359
1423
  } else {
1360
1424
  broadcastToPlugins(modifiedData, ws);
@@ -1920,3 +1984,35 @@ server.on('error', (err) => {
1920
1984
  });
1921
1985
 
1922
1986
  server.listen(PORT, '0.0.0.0');
1987
+
1988
+ const takeoverServer = http.createServer((req, res) => handleHttpRequest(req, res));
1989
+ takeoverServer.on('upgrade', (req, socket, head) => {
1990
+ req._takeoverMode = true;
1991
+ const url = new URL(req.url, `http://localhost:${TAKEOVER_PORT}`);
1992
+ const path = url.pathname;
1993
+ const isPlugin = path === '/plugin';
1994
+ const isClient = path === '/client' ||
1995
+ path.startsWith('/client/') ||
1996
+ path.startsWith('/client-') ||
1997
+ path.startsWith('/devtools/browser/') ||
1998
+ path.startsWith('/devtools/page/');
1999
+
2000
+ if (!isPlugin && !isClient) {
2001
+ socket.destroy();
2002
+ return;
2003
+ }
2004
+
2005
+ wss.handleUpgrade(req, socket, head, (ws) => {
2006
+ wss.emit('connection', ws, req);
2007
+ });
2008
+ });
2009
+ takeoverServer.on('error', (err) => {
2010
+ if (err.code === 'EADDRINUSE') {
2011
+ console.error(`[WARN] Takeover port ${TAKEOVER_PORT} is already in use. Takeover mode disabled.`);
2012
+ } else {
2013
+ console.error('[WARN] Takeover server error:', err.message);
2014
+ }
2015
+ });
2016
+ takeoverServer.listen(TAKEOVER_PORT, '0.0.0.0', () => {
2017
+ console.log(`[TAKEOVER] Listening on port ${TAKEOVER_PORT}`);
2018
+ });