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