cdp-tunnel 1.0.12 → 1.0.14

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.
Files changed (69) hide show
  1. package/cli/index.js +66 -11
  2. package/extension-new/background.js +6 -1
  3. package/extension-new/cdp/handler/special.js +47 -27
  4. package/extension-new/core/state.js +64 -6
  5. package/extension-new/core/websocket.js +124 -19
  6. package/package.json +9 -2
  7. package/server/proxy-server.js +45 -34
  8. package/.github/workflows/publish.yml +0 -92
  9. package/.github/workflows/release-assets.yml +0 -50
  10. package/PUBLISH.md +0 -65
  11. package/console-test.js +0 -52
  12. package/docs/README_CN.md +0 -204
  13. package/docs/config-page-screenshot.png +0 -0
  14. package/final-console-test.js +0 -105
  15. package/simple-tab-group-test.js +0 -56
  16. package/test-cdp-connection.js +0 -85
  17. package/test-cdp-groups.js +0 -71
  18. package/test-check-newtab.js +0 -144
  19. package/test-chrome-native.js +0 -140
  20. package/test-client-connected.js +0 -99
  21. package/test-compare-formats.js +0 -88
  22. package/test-context-features.js +0 -113
  23. package/test-create-tab.js +0 -113
  24. package/test-debug-broadcast.js +0 -52
  25. package/test-debug-targets.js +0 -127
  26. package/test-expose-newtab.js +0 -164
  27. package/test-expose-shared.js +0 -189
  28. package/test-final-logs.js +0 -110
  29. package/test-fresh-chromium.js +0 -153
  30. package/test-init-script.js +0 -128
  31. package/test-keepalive.js +0 -89
  32. package/test-launch-chromium.js +0 -140
  33. package/test-launch-vs-connect.js +0 -149
  34. package/test-listen-events.js +0 -102
  35. package/test-monitor.js +0 -83
  36. package/test-multiple-cdp-groups.js +0 -78
  37. package/test-native.js +0 -96
  38. package/test-page-connection.js +0 -74
  39. package/test-playwright-connection.js +0 -45
  40. package/test-playwright-groups.js +0 -47
  41. package/test-playwright-pages.js +0 -47
  42. package/test-playwright-sequence.js +0 -81
  43. package/test-proper-context.js +0 -129
  44. package/test-real-final.js +0 -251
  45. package/test-real-scenario-v2.js +0 -166
  46. package/test-real-scenario-v3.js +0 -231
  47. package/test-real-scenario.js +0 -104
  48. package/test-server-logs.js +0 -98
  49. package/test-session-id.js +0 -91
  50. package/test-simple-cdp-groups.js +0 -44
  51. package/test-simple-context.js +0 -137
  52. package/test-tab-group-simple.js +0 -58
  53. package/test-tab-grouping.js +0 -48
  54. package/test-three-pages.js +0 -192
  55. package/test-wait-for-page.js +0 -95
  56. package/test-with-logs.js +0 -118
  57. package/test-ws-groups.js +0 -59
  58. package/tests/e2e-auto-test.js +0 -304
  59. package/tests/iframe-test-page.html +0 -89
  60. package/tests/playwright-demo.js +0 -45
  61. package/tests/playwright-interactive.js +0 -261
  62. package/tests/playwright-multi-demo.js +0 -60
  63. package/tests/playwright-multi.js +0 -85
  64. package/tests/playwright-single.js +0 -41
  65. package/tests/screenshot-config.js +0 -35
  66. package/tests/test-client.js +0 -89
  67. package/tests/test-douyin-iframe.js +0 -171
  68. package/tests/test-iframe-debug.js +0 -204
  69. package/tests/test-multi-client.js +0 -129
package/cli/index.js CHANGED
@@ -143,10 +143,72 @@ function openChromeExtensions() {
143
143
  }
144
144
  }
145
145
 
146
+ function startServer(port, watchdog) {
147
+ const serverPath = path.join(__dirname, '..', 'server', 'proxy-server.js');
148
+ const logFd = fs.openSync(LOG_FILE, 'a');
149
+
150
+ const child = spawn('node', [serverPath], {
151
+ detached: !watchdog,
152
+ stdio: ['ignore', logFd, logFd],
153
+ env: { ...process.env, PORT: port.toString() }
154
+ });
155
+
156
+ fs.writeFileSync(PID_FILE, child.pid.toString());
157
+
158
+ if (watchdog) {
159
+ let restartCount = 0;
160
+ const MAX_RESTARTS = 10;
161
+ const RESTART_WINDOW = 60000;
162
+ let restartTimestamps = [];
163
+
164
+ child.on('exit', (code, signal) => {
165
+ const now = Date.now();
166
+ restartTimestamps = restartTimestamps.filter(t => now - t < RESTART_WINDOW);
167
+ restartTimestamps.push(now);
168
+
169
+ if (restartTimestamps.length > MAX_RESTARTS) {
170
+ console.log('');
171
+ log('red', '✗ 服务器在 60 秒内崩溃超过 ' + MAX_RESTARTS + ' 次,停止重启');
172
+ log('gray', ' 请检查日志: ' + LOG_FILE);
173
+ console.log('');
174
+ try { fs.unlinkSync(PID_FILE); } catch {}
175
+ process.exit(1);
176
+ }
177
+
178
+ const reason = signal ? `信号 ${signal}` : `退出码 ${code}`;
179
+ console.log('');
180
+ log('yellow', '⚠ 服务器异常退出 (' + reason + '),3 秒后自动重启...');
181
+ console.log(' 重启次数: ' + restartTimestamps.length + '/' + MAX_RESTARTS + ' (60秒内)');
182
+ console.log('');
183
+
184
+ setTimeout(() => startServer(port, true), 3000);
185
+ });
186
+
187
+ process.on('SIGINT', () => {
188
+ console.log('');
189
+ log('cyan', '正在停止服务器(含 watchdog)...');
190
+ try { child.kill('SIGTERM'); } catch {}
191
+ try { fs.unlinkSync(PID_FILE); } catch {}
192
+ process.exit(0);
193
+ });
194
+
195
+ process.on('SIGTERM', () => {
196
+ try { child.kill('SIGTERM'); } catch {}
197
+ try { fs.unlinkSync(PID_FILE); } catch {}
198
+ process.exit(0);
199
+ });
200
+ } else {
201
+ child.unref();
202
+ }
203
+
204
+ return child;
205
+ }
206
+
146
207
  program
147
208
  .command('start')
148
209
  .description('启动 CDP Tunnel 服务器')
149
210
  .option('-p, --port <port>', '指定端口', parseInt)
211
+ .option('-w, --watchdog', '启用看门狗,服务器崩溃时自动重启')
150
212
  .action((options) => {
151
213
  const config = getConfig();
152
214
  const port = options.port || config.port;
@@ -168,17 +230,7 @@ program
168
230
  ensureConfigDir();
169
231
  cleanupLogFile();
170
232
 
171
- const serverPath = path.join(__dirname, '..', 'server', 'proxy-server.js');
172
-
173
- const child = spawn('node', [serverPath], {
174
- detached: true,
175
- stdio: ['ignore', fs.openSync(LOG_FILE, 'a'), fs.openSync(LOG_FILE, 'a')],
176
- env: { ...process.env, PORT: port.toString() }
177
- });
178
-
179
- child.unref();
180
-
181
- fs.writeFileSync(PID_FILE, child.pid.toString());
233
+ startServer(port, options.watchdog);
182
234
 
183
235
  if (port !== config.port) {
184
236
  config.port = port;
@@ -191,6 +243,9 @@ program
191
243
  console.log(' 端口: ' + port);
192
244
  console.log(' 插件: ws://localhost:' + port + '/plugin');
193
245
  console.log(' CDP: http://localhost:' + port);
246
+ if (options.watchdog) {
247
+ console.log(' 看门狗: 已启用(崩溃自动重启)');
248
+ }
194
249
  console.log('');
195
250
  log('gray', ' 日志: ' + LOG_FILE);
196
251
  console.log('');
@@ -122,6 +122,7 @@ String.prototype.hashCode = function() {
122
122
  Logger.info('[Tabs] Tab removed:', tabId);
123
123
 
124
124
  State.removeAttachedTab(tabId);
125
+ var removedClientId = State.getClientIdByTabId(tabId);
125
126
  Screencast.stopPolling(tabId);
126
127
  AutomationBadge.remove(tabId);
127
128
 
@@ -134,6 +135,9 @@ String.prototype.hashCode = function() {
134
135
  targetId: targetId
135
136
  });
136
137
  State.unmapSession(sessionId);
138
+ if (removedClientId) {
139
+ SpecialHandler.updateTabGroupName(removedClientId);
140
+ }
137
141
  }
138
142
 
139
143
  if (State.getCurrentTabId() === tabId) {
@@ -260,7 +264,8 @@ String.prototype.hashCode = function() {
260
264
 
261
265
  chrome.tabGroups.update(groupId, {
262
266
  title: groupName,
263
- color: groupColor
267
+ color: groupColor,
268
+ collapsed: true
264
269
  }, function(group) {
265
270
  if (chrome.runtime.lastError) {
266
271
  Logger.error('[TabGroup] Failed to update group:', chrome.runtime.lastError.message);
@@ -117,74 +117,88 @@ var SpecialHandler = (function() {
117
117
  Logger.info('[TabGroup] Starting addTabToAutomationGroup for tabId:', tabId, 'clientId:', clientId);
118
118
 
119
119
  var groupName;
120
+ var groupClientId = clientId;
120
121
 
121
- // 如果有指定的clientId,使用该clientId作为组名
122
122
  if (clientId) {
123
123
  groupName = 'CDP-' + clientId.substring(0, 8);
124
- Logger.info('[TabGroup] Using specific clientId for group name:', groupName);
125
124
  } else {
126
- // 如果没有clientId,回退到使用第一个CDP客户端的ID
127
125
  var cdpClients = State.getCDPClients() || [];
128
126
  if (cdpClients.length > 0 && cdpClients[0] && cdpClients[0].id) {
129
127
  groupName = 'CDP-' + cdpClients[0].id.substring(0, 8);
130
- Logger.info('[TabGroup] Using first CDP client ID for group name:', groupName);
128
+ groupClientId = cdpClients[0].id;
131
129
  } else {
132
- // 如果没有CDP客户端,使用固定的组名
133
130
  groupName = 'CDP-Automation';
134
- Logger.info('[TabGroup] No CDP client, using default group name:', groupName);
135
131
  }
136
132
  }
137
133
 
138
- // 添加延迟等待,确保标签页完全加载后再分组
139
134
  setTimeout(function() {
140
135
  Logger.info('[TabGroup] Executing group operation after delay...');
141
136
 
142
- // 查找或创建标签组
143
- chrome.tabGroups.query({ title: groupName }, function(groups) {
144
- Logger.info('[TabGroup] Found groups:', groups);
145
-
137
+ chrome.tabGroups.query({}, function(allGroups) {
138
+ var groups = allGroups.filter(function(g) {
139
+ return g.title && g.title.indexOf(groupName) === 0;
140
+ });
146
141
  if (groups.length > 0) {
147
- // 找到现有的组,将标签页添加到组
148
- Logger.info('[TabGroup] Adding tab to existing group:', groups[0].id);
149
- chrome.tabs.group({ tabIds: tabId, groupId: groups[0].id }, function(groupId) {
142
+ chrome.tabs.group({ tabIds: tabId, groupId: groups[0].id }, function(result) {
150
143
  if (chrome.runtime.lastError) {
151
144
  Logger.error('[TabGroup] Failed to add tab to group:', chrome.runtime.lastError.message);
152
145
  } else {
153
- Logger.info('[TabGroup] Tab added to group:', groupId, 'Group name:', groupName);
146
+ State.setGroupIdForClient(groupClientId, groups[0].id);
147
+ updateTabGroupName(groupClientId);
154
148
  }
155
149
  });
156
150
  } else {
157
- // 创建新组并添加标签页
158
- Logger.info('[TabGroup] Creating new group...');
159
151
  chrome.tabs.group({ tabIds: tabId }, function(groupId) {
160
152
  if (chrome.runtime.lastError) {
161
153
  Logger.error('[TabGroup] Failed to create group:', chrome.runtime.lastError.message);
162
154
  return;
163
155
  }
164
- // 更新组的标题和颜色
165
156
  if (groupId) {
166
- Logger.info('[TabGroup] Group created with ID:', groupId);
167
- // 为不同的组使用不同的颜色
168
157
  var colors = ['blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan', 'orange'];
169
158
  var colorIndex = Math.abs(groupName.hashCode ? groupName.hashCode() : 0) % colors.length;
170
- var groupColor = colors[colorIndex];
171
159
 
172
- Logger.info('[TabGroup] Updating group with title:', groupName, 'color:', groupColor);
173
160
  chrome.tabGroups.update(groupId, {
174
161
  title: groupName,
175
- color: groupColor
176
- }, function(group) {
162
+ color: colors[colorIndex],
163
+ collapsed: true
164
+ }, function() {
177
165
  if (chrome.runtime.lastError) {
178
166
  Logger.error('[TabGroup] Failed to update group:', chrome.runtime.lastError.message);
179
167
  } else {
180
- Logger.info('[TabGroup] Group created and updated:', group);
168
+ State.setGroupIdForClient(groupClientId, groupId);
169
+ updateTabGroupName(groupClientId);
181
170
  }
182
171
  });
183
172
  }
184
173
  });
185
174
  }
186
175
  });
187
- }, 2000); // 等待2秒
176
+ }, 2000);
177
+ }
178
+
179
+ function updateTabGroupName(clientId) {
180
+ if (!clientId) return;
181
+
182
+ var groupId = State.getGroupIdForClient(clientId);
183
+ if (!groupId) return;
184
+
185
+ chrome.tabs.query({ groupId: groupId }, function(tabs) {
186
+ if (chrome.runtime.lastError || !tabs) return;
187
+
188
+ var count = tabs.length;
189
+ var baseName = 'CDP-' + clientId.substring(0, 8);
190
+ var newName = baseName + ' (' + count + ')';
191
+
192
+ chrome.tabGroups.update(groupId, {
193
+ title: newName
194
+ }, function() {
195
+ if (chrome.runtime.lastError) {
196
+ Logger.error('[TabGroup] Failed to update group name:', chrome.runtime.lastError.message);
197
+ } else {
198
+ Logger.info('[TabGroup] Updated group name:', newName);
199
+ }
200
+ });
201
+ });
188
202
  }
189
203
 
190
204
  // 为字符串添加hashCode方法(用于生成颜色索引)
@@ -219,8 +233,13 @@ var SpecialHandler = (function() {
219
233
  if (targetId) {
220
234
  var tabId = State.getTabIdByTargetId(targetId);
221
235
  if (tabId) {
236
+ State.removeAttachedTab(tabId);
237
+ var closeClientId = State.getClientIdByTabId(tabId);
222
238
  return new Promise(function(resolve) {
223
239
  chrome.tabs.remove(tabId, function() {
240
+ if (closeClientId) {
241
+ updateTabGroupName(closeClientId);
242
+ }
224
243
  resolve({ success: true });
225
244
  });
226
245
  }).catch(function() {
@@ -540,6 +559,7 @@ function checkTabVisibility(tabId) {
540
559
  pageCreateIsolatedWorld: pageCreateIsolatedWorld,
541
560
  pageAddScriptToEvaluateOnNewDocument: pageAddScriptToEvaluateOnNewDocument,
542
561
  runtimeRunIfWaitingForDebugger: runtimeRunIfWaitingForDebugger,
543
- domSetFileInputFiles: domSetFileInputFiles
562
+ domSetFileInputFiles: domSetFileInputFiles,
563
+ updateTabGroupName: updateTabGroupName
544
564
  };
545
565
  })();
@@ -23,7 +23,8 @@ var State = (function() {
23
23
  pendingCreatedTabUrls: new Set(),
24
24
  clientIdToTabId: new Map(),
25
25
  clientIdToSessionId: new Map(),
26
- tabIdToClientId: new Map()
26
+ tabIdToClientId: new Map(),
27
+ clientIdToGroupId: new Map()
27
28
  };
28
29
 
29
30
  function mapSession(sessionId, tabId, targetId) {
@@ -261,17 +262,59 @@ var State = (function() {
261
262
  };
262
263
  _state.discoverTargetsEnabled = false;
263
264
  _state.hasConnectedClient = false;
265
+ _state.tabIdToClientId.clear();
266
+ _state.clientIdToGroupId.clear();
264
267
  }
265
268
 
266
269
  function cleanupAllTabs() {
267
270
  return new Promise(function(resolve) {
268
271
  var tabIds = Array.from(_state.attachedTabIds);
269
- var promises = tabIds.map(function(tabId) {
272
+
273
+ var detachPromises = tabIds.map(function(tabId) {
270
274
  return chrome.debugger.detach({ tabId: tabId }).catch(function() {});
271
275
  });
272
- Promise.all(promises).then(function() {
273
- clearAllState();
274
- persist(null, false).then(resolve);
276
+
277
+ Promise.all(detachPromises).then(function() {
278
+ chrome.tabGroups.query({}, function(groups) {
279
+ if (!groups || groups.length === 0) {
280
+ clearAllState();
281
+ persist(null, false).then(resolve);
282
+ return;
283
+ }
284
+
285
+ var cdpGroups = groups.filter(function(g) {
286
+ return g.title && g.title.indexOf('CDP-') === 0;
287
+ });
288
+
289
+ if (cdpGroups.length === 0) {
290
+ clearAllState();
291
+ persist(null, false).then(resolve);
292
+ return;
293
+ }
294
+
295
+ var closePromises = cdpGroups.map(function(group) {
296
+ return new Promise(function(res) {
297
+ chrome.tabs.query({ groupId: group.id }, function(tabs) {
298
+ if (!tabs || tabs.length === 0) {
299
+ res();
300
+ return;
301
+ }
302
+ var ids = tabs.map(function(t) { return t.id; });
303
+ chrome.tabs.remove(ids, function() {
304
+ if (chrome.runtime.lastError) {
305
+ console.error('[State] Failed to close tabs:', chrome.runtime.lastError.message);
306
+ }
307
+ res();
308
+ });
309
+ });
310
+ });
311
+ });
312
+
313
+ Promise.all(closePromises).then(function() {
314
+ clearAllState();
315
+ persist(null, false).then(resolve);
316
+ });
317
+ });
275
318
  });
276
319
  });
277
320
  }
@@ -291,6 +334,18 @@ var State = (function() {
291
334
  return _state.tabIdToClientId.get(tabId);
292
335
  }
293
336
 
337
+ function setGroupIdForClient(clientId, groupId) {
338
+ _state.clientIdToGroupId.set(clientId, groupId);
339
+ }
340
+
341
+ function getGroupIdForClient(clientId) {
342
+ return _state.clientIdToGroupId.get(clientId);
343
+ }
344
+
345
+ function removeGroupForClient(clientId) {
346
+ _state.clientIdToGroupId.delete(clientId);
347
+ }
348
+
294
349
  function getTabIdByClientId(clientId) {
295
350
  return _state.clientIdToTabId.get(clientId);
296
351
  }
@@ -390,6 +445,9 @@ var State = (function() {
390
445
  clearAllState: clearAllState,
391
446
  cleanupAllTabs: cleanupAllTabs,
392
447
  setTabIdToClientId: setTabIdToClientId,
393
- getClientIdByTabId: getClientIdByTabId
448
+ getClientIdByTabId: getClientIdByTabId,
449
+ setGroupIdForClient: setGroupIdForClient,
450
+ getGroupIdForClient: getGroupIdForClient,
451
+ removeGroupForClient: removeGroupForClient
394
452
  };
395
453
  })();
@@ -181,7 +181,7 @@ var WebSocketManager = (function() {
181
181
  break;
182
182
 
183
183
  case 'browser-close':
184
- handleBrowserClose(message.sessions);
184
+ handleBrowserClose(message.sessions, message.clientId);
185
185
  break;
186
186
 
187
187
  case 'client-connected':
@@ -193,11 +193,18 @@ var WebSocketManager = (function() {
193
193
 
194
194
  case 'client-disconnected':
195
195
  Logger.info('[WS] Client disconnected:', message.clientId);
196
- State.removeCDPClient(message.clientId);
197
- if (State.getCDPClients().length === 0) {
198
- State.setHasConnectedClient(false);
199
- }
200
- broadcastStateUpdate();
196
+ var discClientId = message.clientId;
197
+ closeTabGroupByClientId(discClientId).then(function() {
198
+ return new Promise(function(resolve) {
199
+ closeTabsByClientId(discClientId, resolve);
200
+ });
201
+ }).then(function() {
202
+ State.removeCDPClient(discClientId);
203
+ if (State.getCDPClients().length === 0) {
204
+ State.setHasConnectedClient(false);
205
+ }
206
+ broadcastStateUpdate();
207
+ });
201
208
  break;
202
209
 
203
210
  case 'client-list':
@@ -243,6 +250,103 @@ var WebSocketManager = (function() {
243
250
  }
244
251
  }
245
252
 
253
+ function closeTabGroupByClientId(clientId) {
254
+ if (!clientId) return Promise.resolve();
255
+
256
+ Logger.info('[WS] Closing tab group for client:', clientId);
257
+
258
+ return new Promise(function(resolve) {
259
+ var groupId = State.getGroupIdForClient(clientId);
260
+
261
+ if (groupId) {
262
+ closeGroupById(groupId, clientId, resolve);
263
+ } else {
264
+ var groupName = 'CDP-' + clientId.substring(0, 8);
265
+ chrome.tabGroups.query({ title: groupName }, function(groups) {
266
+ if (!groups || groups.length === 0) {
267
+ chrome.tabGroups.query({}, function(allGroups) {
268
+ if (allGroups) {
269
+ var match = allGroups.find(function(g) {
270
+ return g.title && g.title.indexOf(groupName) === 0;
271
+ });
272
+ if (match) {
273
+ closeGroupById(match.id, clientId, resolve);
274
+ } else {
275
+ Logger.info('[WS] No tab group found, closing tabs by clientId:', clientId);
276
+ closeTabsByClientId(clientId, resolve);
277
+ }
278
+ } else {
279
+ resolve();
280
+ }
281
+ });
282
+ } else {
283
+ closeGroupById(groups[0].id, clientId, resolve);
284
+ }
285
+ });
286
+ }
287
+ });
288
+ }
289
+
290
+ function closeGroupById(groupId, clientId, resolve) {
291
+ chrome.tabs.query({ groupId: groupId }, function(tabs) {
292
+ if (!tabs || tabs.length === 0) {
293
+ Logger.info('[WS] No tabs in group:', groupId);
294
+ State.removeGroupForClient(clientId);
295
+ resolve();
296
+ return;
297
+ }
298
+
299
+ var tabIds = tabs.map(function(tab) { return tab.id; });
300
+ Logger.info('[WS] Closing ' + tabIds.length + ' tabs in group:', groupId);
301
+
302
+ chrome.tabs.remove(tabIds, function() {
303
+ if (chrome.runtime.lastError) {
304
+ Logger.error('[WS] Failed to close tabs:', chrome.runtime.lastError.message);
305
+ } else {
306
+ Logger.info('[WS] Successfully closed ' + tabIds.length + ' tabs');
307
+ }
308
+
309
+ tabIds.forEach(function(tabId) {
310
+ chrome.debugger.detach({ tabId: tabId }).catch(function() {});
311
+ });
312
+
313
+ State.removeGroupForClient(clientId);
314
+ resolve();
315
+ });
316
+ });
317
+ }
318
+
319
+ function closeTabsByClientId(clientId, resolve) {
320
+ var attachedTabs = State.getAttachedTabIds();
321
+ var tabsToClose = [];
322
+
323
+ attachedTabs.forEach(function(tabId) {
324
+ if (State.getClientIdByTabId(tabId) === clientId) {
325
+ tabsToClose.push(tabId);
326
+ }
327
+ });
328
+
329
+ if (tabsToClose.length === 0) {
330
+ Logger.info('[WS] No attached tabs found for clientId:', clientId);
331
+ resolve();
332
+ return;
333
+ }
334
+
335
+ Logger.info('[WS] Closing ' + tabsToClose.length + ' attached tabs for clientId:', clientId);
336
+
337
+ tabsToClose.forEach(function(tabId) {
338
+ chrome.tabs.remove(tabId, function() {
339
+ if (chrome.runtime.lastError) {
340
+ Logger.info('[WS] Tab already closed:', tabId);
341
+ }
342
+ });
343
+ chrome.debugger.detach({ tabId: tabId }).catch(function() {});
344
+ State.removeAttachedTab(tabId);
345
+ });
346
+
347
+ resolve();
348
+ }
349
+
246
350
  function handleServerRestart() {
247
351
  Logger.info('[WS] Server restarted, cleaning up all state...');
248
352
 
@@ -260,20 +364,21 @@ var WebSocketManager = (function() {
260
364
  });
261
365
  }
262
366
 
263
- function handleBrowserClose(sessions) {
264
- Logger.info('[WS] Browser.close received, cleaning up...');
265
-
266
- var attachedTabIds = State.getAttachedTabIds();
267
- var promises = attachedTabIds.map(function(tabId) {
268
- return chrome.debugger.detach({ tabId: tabId }).catch(function(e) {
269
- Logger.info('[WS] Detach failed for tab', tabId, ':', e.message);
367
+ function handleBrowserClose(sessions, clientId) {
368
+ Logger.info('[WS] Browser.close received, cleaning up... clientId:', clientId);
369
+
370
+ closeTabGroupByClientId(clientId).then(function() {
371
+ var attachedTabIds = State.getAttachedTabIds();
372
+ var promises = attachedTabIds.map(function(tabId) {
373
+ return chrome.debugger.detach({ tabId: tabId }).catch(function(e) {
374
+ Logger.info('[WS] Detach failed for tab', tabId, ':', e.message);
375
+ });
376
+ });
377
+ Promise.all(promises).then(function() {
378
+ State.clearAllState();
379
+ State.persist(null, false);
380
+ Logger.info('[WS] Browser.close cleanup complete');
270
381
  });
271
- });
272
-
273
- Promise.all(promises).then(function() {
274
- State.clearAllState();
275
- State.persist(null, false);
276
- Logger.info('[WS] Browser.close cleanup complete');
277
382
  });
278
383
  }
279
384
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Chrome Extension CDP Proxy - 通过 Chrome 扩展将 chrome.debugger API 暴露为 WebSocket 端点",
5
5
  "main": "server/proxy-server.js",
6
6
  "bin": "./cli/index.js",
@@ -25,14 +25,21 @@
25
25
  ],
26
26
  "author": "dyyz1993",
27
27
  "license": "Apache-2.0",
28
+ "files": [
29
+ "server/",
30
+ "cli/",
31
+ "extension-new/",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
28
35
  "dependencies": {
29
36
  "commander": "^12.0.0",
30
- "playwright": "^1.58.2",
31
37
  "ws": "^8.16.0"
32
38
  },
33
39
  "devDependencies": {
34
40
  "lz-string": "^1.5.0",
35
41
  "pako": "^2.1.0",
42
+ "playwright": "^1.58.2",
36
43
  "playwright-core": "^1.58.2",
37
44
  "puppeteer-core": "^22.0.0",
38
45
  "sharp": "^0.34.0"
@@ -369,9 +369,20 @@ function handlePluginConnection(ws, clientInfo) {
369
369
 
370
370
  rewriteBrowserContextId(cdpMsg);
371
371
  const cdpData = JSON.stringify(cdpMsg);
372
- logCDP('BROADCAST', `Broadcasting ${parsed.method}, full data: ${cdpData}`, parsed?.sessionId);
373
- broadcastToClients(cdpData, null);
374
- logCDP('BROADCAST', `Done broadcasting ${parsed.method}`, parsed?.sessionId);
372
+
373
+ const targetId = parsed.params?.targetInfo?.targetId;
374
+ const eventClientId = targetId ? targetIdToClientId.get(targetId) : null;
375
+
376
+ if (eventClientId) {
377
+ const clientWs = clientById.get(eventClientId);
378
+ if (clientWs && clientWs.readyState === WebSocket.OPEN) {
379
+ clientWs.send(cdpData);
380
+ console.log(`[TARGET EVENT ROUTED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> clientId=${eventClientId}`);
381
+ }
382
+ } else {
383
+ console.log(`[TARGET EVENT BROADCAST] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (no owner, broadcasting)`);
384
+ broadcastToClients(cdpData, null);
385
+ }
375
386
  }
376
387
 
377
388
  // 对于 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
@@ -462,6 +473,23 @@ function handlePluginConnection(ws, clientInfo) {
462
473
  console.log(`[ATTACHED EVENT] Sent cached event to client: ${mapping.clientId}`);
463
474
  }
464
475
  }
476
+ // 过滤 Target.getTargets 响应,只返回该客户端拥有的 target
477
+ if (mapping.isGetTargets && parsed.result && parsed.result.targetInfos) {
478
+ const clientId = mapping.clientId;
479
+ parsed.result.targetInfos = parsed.result.targetInfos.filter(t => {
480
+ if (t.type !== 'page') return true;
481
+ const ownerClient = targetIdToClientId.get(t.targetId);
482
+ return !ownerClient || ownerClient === clientId;
483
+ });
484
+ console.log(`[GET TARGETS FILTERED] client=${clientId} returned ${parsed.result.targetInfos.filter(t => t.type === 'page').length} page targets`);
485
+ }
486
+ // 清理 Target.closeTarget 成功后的映射
487
+ if (parsed.result && parsed.result.success !== undefined && mapping.method === 'Target.closeTarget') {
488
+ if (mapping.closeTargetId) {
489
+ targetIdToClientId.delete(mapping.closeTargetId);
490
+ console.log(`[CLOSE TARGET CLEANUP] removed targetId=${mapping.closeTargetId?.substring(0,8)} from mapping`);
491
+ }
492
+ }
465
493
 
466
494
  // 然后发送响应给客户端
467
495
  const originalId = mapping.originalId;
@@ -731,6 +759,13 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
731
759
  }
732
760
  console.log(`[PENDING CREATE TARGET] Request id=${parsed.id} from client=${id}`);
733
761
  }
762
+ if (parsed && parsed.method === 'Target.closeTarget' && parsed.id !== undefined) {
763
+ const currentMapping = globalRequestIdMap.get(parsed.id);
764
+ if (currentMapping) {
765
+ currentMapping.method = 'Target.closeTarget';
766
+ currentMapping.closeTargetId = parsed.params?.targetId;
767
+ }
768
+ }
734
769
 
735
770
  // 记录 Target.createBrowserContext 请求,用于后续建立 browserContextId -> clientId 映射
736
771
  if (parsed && parsed.method === 'Target.createBrowserContext' && parsed.id !== undefined) {
@@ -740,41 +775,17 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
740
775
  }
741
776
  console.log(`[PENDING CREATE CONTEXT] Request id=${parsed.id} from client=${id}`);
742
777
  }
778
+ if (parsed && parsed.method === 'Target.getTargets' && parsed.id !== undefined) {
779
+ const currentMapping = globalRequestIdMap.get(parsed.id);
780
+ if (currentMapping) {
781
+ currentMapping.isGetTargets = true;
782
+ }
783
+ }
743
784
 
744
- // 拦截 Browser.close - 清理会话状态
745
785
  if (parsed && parsed.method === 'Browser.close') {
746
786
  if (shouldLog('info')) {
747
- console.log(`\n[BROWSER CLOSE] Client ${id} requested Browser.close`);
787
+ console.log(`\n[BROWSER CLOSE] Client ${id} requested Browser.close, forwarding to plugin`);
748
788
  }
749
-
750
- // 清理该客户端的所有 session 映射
751
- const sessionsToClean = [];
752
- for (const [sessionId, clientId] of sessionToClientId.entries()) {
753
- if (clientId === id) {
754
- sessionsToClean.push(sessionId);
755
- sessionToClientId.delete(sessionId);
756
- }
757
- }
758
- if (shouldLog('info')) {
759
- console.log(` - Cleaned ${sessionsToClean.length} sessions`);
760
- }
761
-
762
- // 通知扩展清理状态
763
- if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
764
- ws.pairedPlugin.send(JSON.stringify({
765
- type: 'browser-close',
766
- clientId: id,
767
- sessions: sessionsToClean
768
- }));
769
- }
770
-
771
- // 返回 mock 响应(包含 sessionId)
772
- const response = { id: parsed.id, result: {} };
773
- if (parsed.sessionId) {
774
- response.sessionId = parsed.sessionId;
775
- }
776
- ws.send(JSON.stringify(response));
777
- return;
778
789
  }
779
790
 
780
791
  if (parsed && parsed.method) {