cdp-tunnel 1.0.4 → 1.0.6

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.
@@ -156,17 +156,24 @@ importScripts('features/automation-badge.js');
156
156
  var openerTabId = tab.openerTabId;
157
157
  var isOpenerControlled = openerTabId && State.isTabAttached(openerTabId);
158
158
 
159
- if (!isOpenerControlled) {
160
- Logger.info('[Tabs] Tab not controlled (no controlled opener), skipping. openerTabId:', openerTabId);
159
+ // 修改逻辑:只要有 opener,就尝试处理(不管 opener 是否被控制)
160
+ // 因为 Playwright 创建的页面 opener 可能没有被扩展识别
161
+ if (!openerTabId) {
162
+ Logger.info('[Tabs] Tab has no opener, skipping. tabId:', tabId);
161
163
  return;
162
164
  }
163
165
 
164
- Logger.info('[Tabs] Tab has controlled opener:', openerTabId, ', will attach');
166
+ Logger.info('[Tabs] Tab has opener:', openerTabId, ', controlled:', isOpenerControlled, ', will attach');
165
167
 
166
168
  LocalHandler.getTargetInfoById(String(tabId)).then(function(targetInfo) {
167
- if (!targetInfo) return;
169
+ Logger.info('[Tabs] getTargetInfoById result:', targetInfo ? targetInfo.targetId : 'null');
170
+ if (!targetInfo) {
171
+ Logger.error('[Tabs] getTargetInfoById returned null for tabId:', tabId);
172
+ return;
173
+ }
168
174
 
169
175
  var targetId = targetInfo.targetId;
176
+ Logger.info('[Tabs] targetId:', targetId);
170
177
 
171
178
  if (State.hasEmittedTarget(targetId)) {
172
179
  Logger.info('[Tabs] Target already emitted, skipping:', targetId);
@@ -174,23 +181,35 @@ importScripts('features/automation-badge.js');
174
181
  }
175
182
 
176
183
  State.addEmittedTarget(targetId);
184
+ Logger.info('[Tabs] Sending Target.targetCreated event');
177
185
 
178
186
  EventBuilder.send('Target.targetCreated', { targetInfo: targetInfo });
187
+ Logger.info('[Tabs] Target.targetCreated sent, now attaching to tab:', tabId);
179
188
 
180
189
  return DebuggerManager.attach(tabId).then(function(attached) {
181
- if (!attached) return;
190
+ Logger.info('[Tabs] DebuggerManager.attach result:', attached);
191
+ if (!attached) {
192
+ Logger.error('[Tabs] Failed to attach to tab:', tabId);
193
+ return;
194
+ }
182
195
 
183
196
  var sessionId = CDPUtils.generateSessionId();
184
197
  State.mapSession(sessionId, tabId, targetId);
185
198
 
186
199
  AutomationBadge.inject(tabId);
200
+ Logger.info('[Tabs] Sending Target.attachedToTarget event');
187
201
 
188
202
  EventBuilder.send('Target.attachedToTarget', {
189
203
  sessionId: sessionId,
190
204
  targetInfo: targetInfo,
191
205
  waitingForDebugger: false
192
206
  });
207
+ Logger.info('[Tabs] Target.attachedToTarget sent');
208
+ }).catch(function(err) {
209
+ Logger.error('[Tabs] DebuggerManager.attach error:', err);
193
210
  });
211
+ }).catch(function(err) {
212
+ Logger.error('[Tabs] getTargetInfoById error:', err);
194
213
  });
195
214
  });
196
215
 
@@ -137,7 +137,27 @@ var LocalHandler = (function() {
137
137
 
138
138
  function getTargetInfos() {
139
139
  return chrome.debugger.getTargets().then(function(targets) {
140
- return targets.map(mapToTargetInfo).filter(Boolean);
140
+ // 为每个有 tabId 的 target 查询 openerTabId
141
+ const promises = targets.map(function(target) {
142
+ if (target.tabId) {
143
+ return new Promise(function(resolve) {
144
+ chrome.tabs.get(target.tabId, function(tab) {
145
+ if (tab && tab.openerTabId) {
146
+ const openerMatch = targets.find(function(t) {
147
+ return String(t.tabId) === String(tab.openerTabId);
148
+ });
149
+ if (openerMatch) {
150
+ target.openerId = openerMatch.id;
151
+ }
152
+ }
153
+ resolve(mapToTargetInfo(target));
154
+ });
155
+ });
156
+ } else {
157
+ return Promise.resolve(mapToTargetInfo(target));
158
+ }
159
+ });
160
+ return Promise.all(promises);
141
161
  });
142
162
  }
143
163
 
@@ -146,7 +166,28 @@ var LocalHandler = (function() {
146
166
  var match = targets.find(function(t) {
147
167
  return t.id === targetId || String(t.tabId) === String(targetId);
148
168
  });
149
- return match ? mapToTargetInfo(match) : null;
169
+ if (!match) return null;
170
+
171
+ // 获取 tab 信息以获取 openerTabId
172
+ var tabId = match.tabId;
173
+ if (tabId) {
174
+ return new Promise(function(resolve) {
175
+ chrome.tabs.get(tabId, function(tab) {
176
+ if (tab && tab.openerTabId) {
177
+ // 查找 opener 的 targetId
178
+ var openerMatch = targets.find(function(t) {
179
+ return String(t.tabId) === String(tab.openerTabId);
180
+ });
181
+ if (openerMatch) {
182
+ match.openerId = openerMatch.id;
183
+ }
184
+ }
185
+ resolve(mapToTargetInfo(match));
186
+ });
187
+ });
188
+ }
189
+
190
+ return mapToTargetInfo(match);
150
191
  });
151
192
  }
152
193
 
@@ -194,7 +235,7 @@ var LocalHandler = (function() {
194
235
 
195
236
  function mapToTargetInfo(target) {
196
237
  if (!target) return null;
197
- return {
238
+ var info = {
198
239
  targetId: target.id || String(target.tabId),
199
240
  type: target.type || 'page',
200
241
  title: target.title || '',
@@ -203,6 +244,11 @@ var LocalHandler = (function() {
203
244
  canAccessOpener: false,
204
245
  browserContextId: 'default'
205
246
  };
247
+ // 添加 openerId(如果有)
248
+ if (target.openerId) {
249
+ info.openerId = target.openerId;
250
+ }
251
+ return info;
206
252
  }
207
253
 
208
254
  return {
@@ -39,6 +39,7 @@ var EventBuilder = (function() {
39
39
  function send(method, params, sessionId) {
40
40
  var event = EventBuilder.build(method, params, sessionId);
41
41
  WebSocketManager.send(event);
42
+ console.log('[EventBuilder.send]', method);
42
43
  return event;
43
44
  }
44
45
 
@@ -52,8 +52,9 @@ var WebSocketManager = (function() {
52
52
 
53
53
  function send(message) {
54
54
  var ws = State.getWs();
55
+ var wsState = ws ? ws.readyState : 'no ws';
55
56
  if (!ws || ws.readyState !== WebSocket.OPEN) {
56
- Logger.warn('[WS] Cannot send, WebSocket not open');
57
+ Logger.warn('[WS] Cannot send, WebSocket not open. State:', wsState);
57
58
  return false;
58
59
  }
59
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",
@@ -50,6 +50,7 @@ const globalRequestIdMap = new Map();
50
50
  const targetIdToClientId = new Map();
51
51
  const pendingAttachedEvents = new Map();
52
52
  const browserContextToClientId = new Map();
53
+ const clientIdToBrowserContext = new Map();
53
54
  let globalRequestIdCounter = 0;
54
55
 
55
56
  let cachedTargets = [];
@@ -309,6 +310,8 @@ function handlePluginConnection(ws, clientInfo) {
309
310
 
310
311
  // 消息转发: Plugin -> Client
311
312
  ws.on('message', (data) => {
313
+ console.log(`[PLUGIN MESSAGE] size=${data.length}`);
314
+
312
315
  const messageSize = data.length;
313
316
  let messagePreview;
314
317
  let parsed;
@@ -331,69 +334,77 @@ function handlePluginConnection(ws, clientInfo) {
331
334
  return;
332
335
  }
333
336
 
334
- // 记录所有 PLUGIN -> CLIENT 消息到日志文件
335
- logCDP('PLUGIN -> CLIENT', data.toString().substring(0, CONFIG.LOG_MESSAGE_PREVIEW_LENGTH), parsed?.sessionId, ws.pluginType);
336
-
337
337
  // 调试:打印所有收到的消息
338
338
  console.log(`[PLUGIN MSG] id=${parsed?.id} method=${parsed?.method || 'none'} type=${parsed?.type || 'none'} sessionId=${parsed?.sessionId?.substring(0,8) || 'none'}`);
339
339
 
340
+ // 记录所有 PLUGIN -> CLIENT 消息到日志文件
341
+ logCDP('PLUGIN -> CLIENT', data.toString().substring(0, CONFIG.LOG_MESSAGE_PREVIEW_LENGTH), parsed?.sessionId, ws.pluginType);
342
+
340
343
  // 处理 type: 'event' 消息(来自 background.js 的 screencast 等事件)
341
344
  if (parsed && parsed.type === 'event' && parsed.method) {
342
- logCDP('DEBUG', `Converting type:event message: ${parsed.method}`, parsed?.sessionId);
345
+ // 对于 Target 事件,始终广播给所有客户端
346
+ const targetEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
347
+ if (targetEvents.includes(parsed.method)) {
348
+ const cdpMsg = {
349
+ method: parsed.method,
350
+ params: parsed.params,
351
+ type: 'event'
352
+ };
353
+ if (parsed.sessionId) {
354
+ cdpMsg.params.sessionId = parsed.sessionId;
355
+ cdpMsg.sessionId = parsed.sessionId;
356
+ }
357
+
358
+ if (parsed.method === 'Target.targetCreated') {
359
+ const targetId = parsed.params?.targetInfo?.targetId;
360
+ const openerId = parsed.params?.targetInfo?.openerId;
361
+ if (openerId && targetId) {
362
+ const openerClientId = targetIdToClientId.get(openerId);
363
+ if (openerClientId) {
364
+ targetIdToClientId.set(targetId, openerClientId);
365
+ console.log(`[TARGET CREATED with opener] targetId=${targetId.substring(0,8)} openerId=${openerId.substring(0,8)} -> clientId=${openerClientId}`);
366
+ }
367
+ }
368
+ }
369
+
370
+ rewriteBrowserContextId(cdpMsg);
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);
375
+ }
343
376
 
344
- // 处理 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
377
+ // 对于 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
345
378
  if (parsed.method === 'Target.attachedToTarget') {
346
379
  const targetId = parsed.params?.targetInfo?.targetId;
347
380
  const sessionId = parsed.params?.sessionId;
348
381
 
349
- console.log(`[ATTACHED EVENT (type:event)] targetId=${targetId} sessionId=${sessionId?.substring(0,8)}`);
350
-
351
- // 查找 targetId 对应的 clientId
352
- const clientId = targetIdToClientId.get(targetId);
353
- if (clientId && sessionId) {
354
- sessionToClientId.set(sessionId, clientId);
355
- console.log(`[SESSION MAPPED from event] sessionId=${sessionId.substring(0,8)} -> clientId=${clientId} (targetId=${targetId})`);
356
- targetIdToClientId.delete(targetId);
357
-
358
- // 转换为 CDP 格式并发送给对应的客户端
359
- const cdpMsg = {
360
- method: parsed.method,
361
- params: parsed.params
362
- };
363
-
364
- const clientWs = clientById.get(clientId);
365
- if (clientWs && clientWs.readyState === WebSocket.OPEN) {
366
- clientWs.send(JSON.stringify(cdpMsg));
367
- console.log(`[ATTACHED EVENT] Sent to client: ${clientId}`);
368
- }
369
- return;
370
- } else if (targetId && sessionId) {
371
- // targetId 还没有映射,缓存事件等待 Target.createTarget 响应
372
- pendingAttachedEvents.set(targetId, { sessionId, parsed, data });
373
- console.log(`[ATTACHED EVENT] Cached for targetId=${targetId}, waiting for createTarget response`);
374
- return;
382
+ if (targetId && sessionId) {
383
+ sessionToClientId.set(sessionId, targetId);
384
+ console.log(`[SESSION MAPPED] sessionId=${sessionId.substring(0,8)} -> targetId=${targetId.substring(0,8)}`);
375
385
  }
376
386
  }
377
387
 
378
- const cdpMsg = {
379
- method: parsed.method,
380
- params: parsed.params
381
- };
382
- if (parsed.sessionId) {
383
- cdpMsg.sessionId = parsed.sessionId;
384
- }
385
- const cdpData = JSON.stringify(cdpMsg);
386
-
387
- // 发送给配对的 client
388
- if (ws.pairedClientId) {
389
- const clientWs = clientById.get(ws.pairedClientId);
390
- if (safeSend(clientWs, cdpData, 'client')) {
391
- logCDP('DEBUG', `Sent converted event to client: ${parsed.method}`, parsed?.sessionId);
392
- return;
388
+ // 如果不是 Target 事件,按照原来的逻辑发送
389
+ if (!targetEvents.includes(parsed.method)) {
390
+ const cdpMsg = {
391
+ method: parsed.method,
392
+ params: parsed.params
393
+ };
394
+ if (parsed.sessionId) {
395
+ cdpMsg.sessionId = parsed.sessionId;
393
396
  }
397
+ const cdpData = JSON.stringify(cdpMsg);
398
+
399
+ if (ws.pairedClientId) {
400
+ const clientWs = clientById.get(ws.pairedClientId);
401
+ if (safeSend(clientWs, cdpData, 'client')) {
402
+ logCDP('DEBUG', `Sent converted event to client: ${parsed.method}`, parsed?.sessionId);
403
+ return;
404
+ }
405
+ }
406
+ broadcastToClients(cdpData, ws);
394
407
  }
395
- // 广播给所有 client
396
- broadcastToClients(cdpData, ws);
397
408
  return;
398
409
  }
399
410
 
@@ -418,6 +429,7 @@ function handlePluginConnection(ws, clientInfo) {
418
429
  if (mapping.isCreateBrowserContext && parsed.result?.browserContextId) {
419
430
  const browserContextId = parsed.result.browserContextId;
420
431
  browserContextToClientId.set(browserContextId, mapping.clientId);
432
+ clientIdToBrowserContext.set(mapping.clientId, browserContextId);
421
433
  console.log(`[BROWSER CONTEXT MAPPED] browserContextId=${browserContextId} -> clientId=${mapping.clientId}`);
422
434
  }
423
435
 
@@ -484,120 +496,16 @@ function handlePluginConnection(ws, clientInfo) {
484
496
  return;
485
497
  }
486
498
 
487
- // 3. 事件广播:无 id 和 sessionId 的消息(如 Target.targetCreated)
488
- // 只广播特定类型的事件,避免干扰其他客户端
489
- if (parsed && parsed.method) {
490
- // 处理 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
491
- if (parsed.method === 'Target.attachedToTarget') {
492
- const targetId = parsed.params?.targetInfo?.targetId;
493
- const sessionId = parsed.params?.sessionId;
494
- const openerId = parsed.params?.targetInfo?.openerId;
495
-
496
- // 查找 targetId 对应的 clientId
497
- let clientId = targetIdToClientId.get(targetId);
498
-
499
- // 如果没有直接映射,检查 openerId(window.open 打开的新 tab)
500
- if (!clientId && openerId) {
501
- // 查找 openerId 对应的 clientId
502
- // openerId 可能是某个已知的 targetId
503
- clientId = targetIdToClientId.get(openerId);
504
- if (clientId) {
505
- console.log(`[OPENER TRACKING] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${clientId}`);
506
- // 记录新 targetId 的映射
507
- targetIdToClientId.set(targetId, clientId);
508
- }
509
- }
510
-
511
- if (clientId && sessionId) {
512
- sessionToClientId.set(sessionId, clientId);
513
- console.log(`[SESSION MAPPED from event] sessionId=${sessionId.substring(0,8)} -> clientId=${clientId} (targetId=${targetId?.substring(0,8)})`);
514
- targetIdToClientId.delete(targetId);
515
-
516
- // 只发送给对应的客户端
517
- const clientWs = clientById.get(clientId);
518
- if (clientWs && clientWs.readyState === WebSocket.OPEN) {
519
- clientWs.send(data);
520
- }
521
- return;
522
- }
523
- }
524
-
525
- // 处理 Target.targetInfoChanged 事件
526
- if (parsed.method === 'Target.targetInfoChanged') {
527
- const targetId = parsed.params?.targetInfo?.targetId;
528
- const openerId = parsed.params?.targetInfo?.openerId;
529
- console.log(`[TARGET INFO CHANGED] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0, 8) || 'none'}`);
530
- }
531
-
532
- if (parsed.method === 'Target.targetCreated') {
533
- const targetId = parsed.params?.targetInfo?.targetId;
534
- const openerId = parsed.params?.targetInfo?.openerId;
535
- const browserContextId = parsed.params?.targetInfo?.browserContextId;
536
- const targetType = parsed.params?.targetInfo?.type;
537
-
538
- console.log(`[TARGET CREATED] targetId=${targetId?.substring(0,8)} type=${targetType} openerId=${openerId?.substring(0, 8) || 'none'} browserContextId=${browserContextId?.substring(0, 8) || 'none'}`);
539
-
540
- // 如果有 openerId,尝试找到对应的 clientId
541
- if (openerId && targetId) {
542
- const openerClientId = targetIdToClientId.get(openerId);
543
- if (openerClientId) {
544
- targetIdToClientId.set(targetId, openerClientId);
545
- console.log(`[TARGET CREATED with opener] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${openerClientId}`);
546
- }
547
- }
548
-
549
- // 如果有 browserContextId,尝试找到对应的 clientId
550
- // browserContextId 是通过 Target.createBrowserContext 创建的
551
- if (browserContextId && targetId) {
552
- const contextClientId = browserContextToClientId.get(browserContextId);
553
- if (contextClientId && !targetIdToClientId.has(targetId)) {
554
- targetIdToClientId.set(targetId, contextClientId);
555
- console.log(`[TARGET CREATED in context] targetId=${targetId?.substring(0,8)} browserContextId=${browserContextId?.substring(0,8)} -> clientId=${contextClientId}`);
556
- }
557
- }
558
-
559
- // Service Worker 处理:Service Worker 通常属于创建它的页面所在的客户端
560
- // 通过 browserContextId 来判断归属
561
- if (targetType === 'service_worker' && browserContextId && targetId) {
562
- const contextClientId = browserContextToClientId.get(browserContextId);
563
- if (contextClientId) {
564
- targetIdToClientId.set(targetId, contextClientId);
565
- console.log(`[SERVICE WORKER] targetId=${targetId?.substring(0,8)} -> clientId=${contextClientId}`);
566
- }
567
- }
568
-
569
- // iframe (OOPIF) 处理:跨域 iframe 可能有独立的 target
570
- // 通过 openerId 或 browserContextId 来判断归属
571
- if (targetType === 'iframe' && targetId) {
572
- // 优先使用 openerId
573
- if (openerId) {
574
- const openerClientId = targetIdToClientId.get(openerId);
575
- if (openerClientId) {
576
- targetIdToClientId.set(targetId, openerClientId);
577
- console.log(`[IFRAME with opener] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${openerClientId}`);
578
- }
579
- } else if (browserContextId) {
580
- // 否则使用 browserContextId
581
- const contextClientId = browserContextToClientId.get(browserContextId);
582
- if (contextClientId) {
583
- targetIdToClientId.set(targetId, contextClientId);
584
- console.log(`[IFRAME in context] targetId=${targetId?.substring(0,8)} browserContextId=${browserContextId?.substring(0,8)} -> clientId=${contextClientId}`);
585
- }
586
- }
587
- }
588
- }
589
-
590
- const broadcastMethods = [
591
- 'Target.targetCreated',
592
- 'Target.targetDestroyed',
593
- 'Target.targetInfoChanged'
499
+ // 3. 其他事件:无 id 和 sessionId 的消息
500
+ // 注意:Target 事件已在上方 type: 'event' 分支处理
501
+ // 这里只处理非 Target 事件
502
+ if (parsed && parsed.method && !parsed.sessionId) {
503
+ const nonTargetBroadcastMethods = [
504
+ 'Inspector.detached',
505
+ 'Log.entryAdded'
594
506
  ];
595
- if (broadcastMethods.includes(parsed.method)) {
596
- for (const clientWs of clientConnections) {
597
- if (clientWs.readyState === WebSocket.OPEN) {
598
- clientWs.send(data);
599
- }
600
- }
507
+ if (nonTargetBroadcastMethods.includes(parsed.method)) {
508
+ broadcastToClients(data, null);
601
509
  }
602
510
  }
603
511
  });
@@ -1016,11 +924,16 @@ function handlePageConnection(ws, clientInfo, targetId) {
1016
924
  cdpMsg.sessionId = msg.sessionId;
1017
925
  }
1018
926
 
1019
- if (msg.method === 'Page.screencastFrame' && shouldLog('debug')) {
1020
- console.log(`[PLUGIN -> PAGE] ${id}: Page.screencastFrame`);
927
+ // 对于全局 Target 事件,需要广播给所有客户端
928
+ const broadcastEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
929
+ if (broadcastEvents.includes(msg.method)) {
930
+ rewriteBrowserContextId(cdpMsg);
931
+ console.log(`[PLUGIN -> ALL CLIENTS] Broadcasting ${msg.method}`);
932
+ broadcastToClients(JSON.stringify(cdpMsg), null);
933
+ } else {
934
+ ws.lastActivityTime = Date.now();
935
+ ws.send(JSON.stringify(cdpMsg));
1021
936
  }
1022
- ws.lastActivityTime = Date.now();
1023
- ws.send(JSON.stringify(cdpMsg));
1024
937
  return;
1025
938
  }
1026
939
 
@@ -1109,16 +1022,51 @@ function handlePageConnection(ws, clientInfo, targetId) {
1109
1022
  });
1110
1023
  }
1111
1024
 
1025
+ /**
1026
+ * 重写 Target 事件中的 browserContextId
1027
+ * 插件总是报告 'default',但 Playwright 期望自己创建的 context ID
1028
+ * 通过 openerId 找到对应的 clientId,再找到该 client 的 browserContextId
1029
+ */
1030
+ function rewriteBrowserContextId(cdpMsg) {
1031
+ const targetInfo = cdpMsg.params?.targetInfo;
1032
+ if (!targetInfo || targetInfo.browserContextId !== 'default') {
1033
+ return cdpMsg;
1034
+ }
1035
+
1036
+ let clientId = null;
1037
+
1038
+ if (targetInfo.openerId) {
1039
+ clientId = targetIdToClientId.get(targetInfo.openerId);
1040
+ }
1041
+ if (!clientId && targetInfo.targetId) {
1042
+ clientId = targetIdToClientId.get(targetInfo.targetId);
1043
+ }
1044
+
1045
+ if (clientId) {
1046
+ const contextId = clientIdToBrowserContext.get(clientId);
1047
+ if (contextId) {
1048
+ console.log(`[CONTEXT REWRITE] targetId=${targetInfo.targetId?.substring(0,8)} browserContextId: 'default' -> '${contextId}' (via openerId=${targetInfo.openerId?.substring(0,8) || 'none'}, clientId=${clientId})`);
1049
+ targetInfo.browserContextId = contextId;
1050
+ }
1051
+ }
1052
+
1053
+ return cdpMsg;
1054
+ }
1055
+
1112
1056
  /**
1113
1057
  * 广播消息给所有客户端
1114
1058
  */
1115
1059
  function broadcastToClients(data, excludeWs = null) {
1116
1060
  let sent = 0;
1061
+ logCDP('BROADCAST', `Starting broadcast to ${clientConnections.size} clients, data preview: ${data.substring(0, 200)}`);
1117
1062
  clientConnections.forEach((client) => {
1063
+ logCDP('BROADCAST', `Checking client ${client.id}, state=${client.readyState}, excluded=${client === excludeWs}`);
1118
1064
  if (client !== excludeWs && safeSend(client, data, 'client')) {
1119
1065
  sent++;
1066
+ logCDP('BROADCAST', `Sent to client ${client.id}`);
1120
1067
  }
1121
1068
  });
1069
+ logCDP('BROADCAST', `Finished: sent to ${sent} clients`);
1122
1070
  return sent;
1123
1071
  }
1124
1072
 
@@ -0,0 +1,192 @@
1
+ const { chromium } = require('playwright');
2
+ const { spawn } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ let serverPort = 9050;
7
+
8
+ async function testThreePages(port, label) {
9
+ serverPort++;
10
+ const currentPort = serverPort;
11
+
12
+ console.log(`\n${'='.repeat(60)}`);
13
+ console.log(`Testing ${label} (port ${port})`);
14
+ console.log('='.repeat(60));
15
+
16
+ try {
17
+ const sharedState = { counter: 0 };
18
+
19
+ const htmlContent1 = `
20
+ <!DOCTYPE html>
21
+ <html>
22
+ <head><title>Page 1</title></head>
23
+ <body>
24
+ <h1>Page 1</h1>
25
+ <a href="page2.html" id="link1">Go to Page 2 (same tab)</a>
26
+ <button id="btn" onclick="window.getCounter().then(r => document.getElementById('result').innerText = r)">Get Counter</button>
27
+ <div id="result">-</div>
28
+ </body>
29
+ </html>
30
+ `;
31
+
32
+ const htmlContent2 = `
33
+ <!DOCTYPE html>
34
+ <html>
35
+ <head><title>Page 2</title></head>
36
+ <body>
37
+ <h1>Page 2</h1>
38
+ <a href="page3.html" target="_blank" id="link2">Open Page 3 (new tab)</a>
39
+ <button id="btn" onclick="window.getCounter().then(r => document.getElementById('result').innerText = r)">Get Counter</button>
40
+ <div id="result">-</div>
41
+ </body>
42
+ </html>
43
+ `;
44
+
45
+ const htmlContent3 = `
46
+ <!DOCTYPE html>
47
+ <html>
48
+ <head><title>Page 3</title></head>
49
+ <body>
50
+ <h1>Page 3 - New Tab</h1>
51
+ <button id="btn" onclick="window.getCounter().then(r => document.getElementById('result').innerText = r)">Get Counter</button>
52
+ <div id="result">-</div>
53
+ </body>
54
+ </html>
55
+ `;
56
+
57
+ const serverDir = '/tmp/test-three-pages';
58
+ if (!fs.existsSync(serverDir)) {
59
+ fs.mkdirSync(serverDir, { recursive: true });
60
+ }
61
+ fs.writeFileSync(path.join(serverDir, 'page1.html'), htmlContent1);
62
+ fs.writeFileSync(path.join(serverDir, 'page2.html'), htmlContent2);
63
+ fs.writeFileSync(path.join(serverDir, 'page3.html'), htmlContent3);
64
+
65
+ const http = require('http');
66
+ const server = http.createServer((req, res) => {
67
+ let filePath = path.join(serverDir, req.url === '/' ? 'page1.html' : req.url);
68
+ if (fs.existsSync(filePath)) {
69
+ const content = fs.readFileSync(filePath);
70
+ res.writeHead(200, { 'Content-Type': 'text/html' });
71
+ res.end(content);
72
+ } else {
73
+ res.writeHead(404);
74
+ res.end('Not found');
75
+ }
76
+ });
77
+
78
+ await new Promise(resolve => server.listen(currentPort, resolve));
79
+ console.log(`[${label}] Test server on port ${currentPort}`);
80
+
81
+ const browser = await chromium.connectOverCDP(`http://localhost:${port}`);
82
+ console.log(`[${label}] Connected!`);
83
+
84
+ const context = await browser.newContext();
85
+ console.log(`[${label}] Context created`);
86
+
87
+ await context.exposeFunction('getCounter', () => {
88
+ sharedState.counter += 1;
89
+ console.log(`[${label}] getCounter called -> counter: ${sharedState.counter}`);
90
+ return sharedState.counter;
91
+ });
92
+ console.log(`[${label}] exposeFunction registered`);
93
+
94
+ const page1 = await context.newPage();
95
+ await page1.goto(`http://localhost:${currentPort}/page1.html`, { waitUntil: 'domcontentloaded' });
96
+ console.log(`[${label}] Page 1 loaded: ${page1.url()}`);
97
+
98
+ const r1 = await page1.evaluate(() => window.getCounter());
99
+ console.log(`[${label}] Page 1 counter: ${r1} (expect 1)`);
100
+
101
+ console.log(`[${label}] --- Step 1: Click link in Page 1 -> navigate to Page 2 (same tab) ---`);
102
+ await Promise.all([
103
+ page1.waitForURL('**/page2.html'),
104
+ page1.click('#link1')
105
+ ]);
106
+ console.log(`[${label}] Page 1 navigated to Page 2: ${page1.url()}`);
107
+
108
+ const r2 = await page1.evaluate(() => window.getCounter());
109
+ console.log(`[${label}] Page 2 counter: ${r2} (expect 2)`);
110
+
111
+ console.log(`[${label}] --- Step 2: Click link in Page 2 -> open Page 3 (new tab) ---`);
112
+ const [page3] = await Promise.all([
113
+ context.waitForEvent('page', { timeout: 10000 }),
114
+ page1.click('#link2')
115
+ ]);
116
+ await page3.waitForLoadState('domcontentloaded');
117
+ console.log(`[${label}] Page 3 opened in new tab: ${page3.url()}`);
118
+
119
+ const r3 = await page3.evaluate(() => window.getCounter());
120
+ console.log(`[${label}] Page 3 counter: ${r3} (expect 3)`);
121
+
122
+ console.log(`[${label}] --- Step 3: Verify all pages ---`);
123
+ const allPages = context.pages();
124
+ console.log(`[${label}] Total pages in context: ${allPages.length} (expect 2)`);
125
+
126
+ const r2again = await page1.evaluate(() => window.getCounter());
127
+ console.log(`[${label}] Page 2 counter again: ${r2again} (expect 4)`);
128
+
129
+ const r3again = await page3.evaluate(() => window.getCounter());
130
+ console.log(`[${label}] Page 3 counter again: ${r3again} (expect 5)`);
131
+
132
+ const success = r1 === 1 && r2 === 2 && r3 === 3 && r2again === 4 && r3again === 5;
133
+
134
+ server.close();
135
+
136
+ if (success) {
137
+ console.log(`[${label}] ✓ PASS! Counter: ${r1} -> ${r2} -> ${r3} -> ${r2again} -> ${r3again}`);
138
+ } else {
139
+ console.log(`[${label}] ✗ FAIL! Expected: 1,2,3,4,5 Got: ${r1},${r2},${r3},${r2again},${r3again}`);
140
+ }
141
+
142
+ await browser.close();
143
+ return success;
144
+
145
+ } catch (error) {
146
+ console.error(`[${label}] ✗ Error:`, error.message);
147
+ return false;
148
+ }
149
+ }
150
+
151
+ async function main() {
152
+ console.log('Testing: Page1 -> navigate to Page2 (same tab) -> open Page3 (new tab)');
153
+ console.log('Counter: 1 -> 2 -> 3 -> 4 -> 5 (shared state via exposeFunction)\n');
154
+
155
+ console.log('Step 1: Starting Chromium on port 9230...');
156
+ const chromiumProcess = spawn('/Applications/Chromium.app/Contents/MacOS/Chromium', [
157
+ '--remote-debugging-port=9230',
158
+ '--user-data-dir=/tmp/chromium-test-three',
159
+ '--no-first-run',
160
+ '--no-default-browser-check'
161
+ ], { detached: true, stdio: 'ignore' });
162
+
163
+ await new Promise(resolve => setTimeout(resolve, 3000));
164
+
165
+ console.log('\nStep 2: Testing Native CDP...');
166
+ const nativeResult = await testThreePages(9230, 'Native CDP');
167
+
168
+ await new Promise(resolve => setTimeout(resolve, 1000));
169
+
170
+ console.log('\nStep 3: Testing CDP Tunnel...');
171
+ const tunnelResult = await testThreePages(9221, 'CDP Tunnel');
172
+
173
+ try { process.kill(-chromiumProcess.pid); } catch (e) {}
174
+
175
+ console.log('\n' + '='.repeat(60));
176
+ console.log(`Native CDP: ${nativeResult ? '✓ PASS' : '✗ FAIL'}`);
177
+ console.log(`CDP Tunnel: ${tunnelResult ? '✓ PASS' : '✗ FAIL'}`);
178
+
179
+ if (nativeResult && tunnelResult) {
180
+ console.log('\n✓ Both work identically!');
181
+ } else if (!nativeResult && tunnelResult) {
182
+ console.log('\n✓ CDP Tunnel works! Native CDP has limitations.');
183
+ } else if (nativeResult && !tunnelResult) {
184
+ console.log('\n✗ CDP Tunnel has issues!');
185
+ } else {
186
+ console.log('\n✗ Both have issues!');
187
+ }
188
+
189
+ process.exit(0);
190
+ }
191
+
192
+ main();