cdp-tunnel 1.0.3 → 1.0.5
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.
- package/extension-new/cdp/handler/local.js +49 -3
- package/package.json +1 -1
- package/server/proxy-server.js +113 -165
- package/test-three-pages.js +192 -0
|
@@ -137,7 +137,27 @@ var LocalHandler = (function() {
|
|
|
137
137
|
|
|
138
138
|
function getTargetInfos() {
|
|
139
139
|
return chrome.debugger.getTargets().then(function(targets) {
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/package.json
CHANGED
package/server/proxy-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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.
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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 (
|
|
596
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
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();
|