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/cli/index.js
CHANGED
|
@@ -13,6 +13,24 @@ const LOG_FILE = path.join(CONFIG_DIR, 'server.log');
|
|
|
13
13
|
const EXTENSION_STATE_FILE = path.join(CONFIG_DIR, 'extension-state.json');
|
|
14
14
|
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
|
|
15
15
|
|
|
16
|
+
const INSTANCES_DIR = path.join(CONFIG_DIR, 'instances');
|
|
17
|
+
|
|
18
|
+
function getInstanceDir(port) {
|
|
19
|
+
return path.join(INSTANCES_DIR, port.toString());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getInstanceFilePath(port, filename) {
|
|
23
|
+
return path.join(getInstanceDir(port), filename);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ensureInstanceDir(port) {
|
|
27
|
+
const dir = getInstanceDir(port);
|
|
28
|
+
if (!fs.existsSync(dir)) {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
const program = new Command();
|
|
17
35
|
|
|
18
36
|
program
|
|
@@ -54,60 +72,65 @@ function ensureConfigDir() {
|
|
|
54
72
|
}
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
function cleanupLogFile() {
|
|
75
|
+
function cleanupLogFile(logFilePath) {
|
|
58
76
|
try {
|
|
59
|
-
if (fs.existsSync(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
77
|
+
if (!fs.existsSync(logFilePath)) return;
|
|
78
|
+
const stats = fs.statSync(logFilePath);
|
|
79
|
+
if (stats.size > MAX_LOG_SIZE) {
|
|
80
|
+
fs.writeFileSync(logFilePath, '');
|
|
81
|
+
console.log('');
|
|
82
|
+
log('yellow', '⚠ 日志文件超过 10MB,已清空');
|
|
83
|
+
console.log('');
|
|
67
84
|
}
|
|
68
85
|
} catch (e) {
|
|
69
86
|
// 清理失败不影响启动
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
|
|
73
|
-
function getConfig() {
|
|
90
|
+
function getConfig(port) {
|
|
74
91
|
ensureConfigDir();
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
const file = port ? getInstanceFilePath(port, 'config.json') : CONFIG_FILE;
|
|
93
|
+
if (fs.existsSync(file)) {
|
|
94
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
77
95
|
}
|
|
78
|
-
return { port: 9221 };
|
|
96
|
+
return { port: port || 9221 };
|
|
79
97
|
}
|
|
80
98
|
|
|
81
|
-
function saveConfig(config) {
|
|
99
|
+
function saveConfig(config, port) {
|
|
82
100
|
ensureConfigDir();
|
|
83
|
-
|
|
101
|
+
const file = port ? getInstanceFilePath(port, 'config.json') : CONFIG_FILE;
|
|
102
|
+
const dir = port ? getInstanceDir(port) : CONFIG_DIR;
|
|
103
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2));
|
|
84
105
|
}
|
|
85
106
|
|
|
86
|
-
function isServerRunning() {
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
function isServerRunning(port) {
|
|
108
|
+
const pidFile = port ? getInstanceFilePath(port, 'server.pid') : PID_FILE;
|
|
109
|
+
if (!fs.existsSync(pidFile)) return false;
|
|
110
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'));
|
|
89
111
|
try {
|
|
90
112
|
process.kill(pid, 0);
|
|
91
113
|
return true;
|
|
92
114
|
} catch {
|
|
93
|
-
fs.unlinkSync(
|
|
115
|
+
fs.unlinkSync(pidFile);
|
|
94
116
|
return false;
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
|
|
98
|
-
function getServerPid() {
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
function getServerPid(port) {
|
|
121
|
+
const pidFile = port ? getInstanceFilePath(port, 'server.pid') : PID_FILE;
|
|
122
|
+
if (!fs.existsSync(pidFile)) return null;
|
|
123
|
+
return parseInt(fs.readFileSync(pidFile, 'utf8'));
|
|
101
124
|
}
|
|
102
125
|
|
|
103
|
-
function checkChromeExtension() {
|
|
104
|
-
|
|
126
|
+
function checkChromeExtension(port) {
|
|
127
|
+
const stateFile = port ? getInstanceFilePath(port, 'extension-state.json') : EXTENSION_STATE_FILE;
|
|
128
|
+
if (!fs.existsSync(stateFile)) {
|
|
105
129
|
return { installed: false };
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
try {
|
|
109
|
-
const state = JSON.parse(fs.readFileSync(
|
|
110
|
-
// 检查连接状态,允许5分钟的时间差容错
|
|
133
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
111
134
|
const timeDiff = Math.abs(Date.now() - state.lastSeen);
|
|
112
135
|
if (state.connected && timeDiff < 300000) {
|
|
113
136
|
return { installed: true, connected: true };
|
|
@@ -162,16 +185,22 @@ function openChromeExtensions() {
|
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
function startServer(port, watchdog, autoRestart) {
|
|
188
|
+
ensureInstanceDir(port);
|
|
189
|
+
const instancePidFile = getInstanceFilePath(port, 'server.pid');
|
|
190
|
+
const instanceLogFile = getInstanceFilePath(port, 'server.log');
|
|
165
191
|
const serverPath = path.join(__dirname, '..', 'server', 'proxy-server.js');
|
|
166
|
-
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
167
192
|
|
|
168
|
-
|
|
193
|
+
cleanupLogFile(instanceLogFile);
|
|
194
|
+
|
|
195
|
+
const logFd = fs.openSync(instanceLogFile, 'a');
|
|
196
|
+
|
|
197
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
169
198
|
detached: !watchdog,
|
|
170
199
|
stdio: ['ignore', logFd, logFd],
|
|
171
200
|
env: { ...process.env, PORT: port.toString(), AUTO_RESTART: autoRestart ? 'true' : 'false' }
|
|
172
201
|
});
|
|
173
202
|
|
|
174
|
-
fs.writeFileSync(
|
|
203
|
+
fs.writeFileSync(instancePidFile, child.pid.toString());
|
|
175
204
|
|
|
176
205
|
if (watchdog) {
|
|
177
206
|
let restartCount = 0;
|
|
@@ -183,11 +212,11 @@ function startServer(port, watchdog, autoRestart) {
|
|
|
183
212
|
const now = Date.now();
|
|
184
213
|
const reason = signal ? `信号 ${signal}` : `退出码 ${code}`;
|
|
185
214
|
const logLine = `[${new Date().toISOString()}] [WATCHDOG] 服务器退出: ${reason}\n`;
|
|
186
|
-
fs.appendFileSync(
|
|
215
|
+
fs.appendFileSync(instanceLogFile, logLine);
|
|
187
216
|
|
|
188
217
|
if (code === 0 && !signal) {
|
|
189
218
|
log('gray', ' 服务器正常退出 (code=0),不重启');
|
|
190
|
-
try { fs.unlinkSync(
|
|
219
|
+
try { fs.unlinkSync(instancePidFile); } catch {}
|
|
191
220
|
process.exit(0);
|
|
192
221
|
}
|
|
193
222
|
|
|
@@ -197,9 +226,9 @@ function startServer(port, watchdog, autoRestart) {
|
|
|
197
226
|
if (restartTimestamps.length > MAX_RESTARTS) {
|
|
198
227
|
console.log('');
|
|
199
228
|
log('red', '✗ 服务器在 60 秒内崩溃超过 ' + MAX_RESTARTS + ' 次,停止重启');
|
|
200
|
-
log('gray', ' 请检查日志: ' +
|
|
229
|
+
log('gray', ' 请检查日志: ' + instanceLogFile);
|
|
201
230
|
console.log('');
|
|
202
|
-
try { fs.unlinkSync(
|
|
231
|
+
try { fs.unlinkSync(instancePidFile); } catch {}
|
|
203
232
|
process.exit(1);
|
|
204
233
|
}
|
|
205
234
|
|
|
@@ -226,13 +255,13 @@ function startServer(port, watchdog, autoRestart) {
|
|
|
226
255
|
console.log('');
|
|
227
256
|
log('cyan', '正在停止服务器(含 watchdog)...');
|
|
228
257
|
try { child.kill('SIGTERM'); } catch {}
|
|
229
|
-
try { fs.unlinkSync(
|
|
258
|
+
try { fs.unlinkSync(instancePidFile); } catch {}
|
|
230
259
|
process.exit(0);
|
|
231
260
|
});
|
|
232
261
|
|
|
233
262
|
process.on('SIGTERM', () => {
|
|
234
263
|
try { child.kill('SIGTERM'); } catch {}
|
|
235
|
-
try { fs.unlinkSync(
|
|
264
|
+
try { fs.unlinkSync(instancePidFile); } catch {}
|
|
236
265
|
process.exit(0);
|
|
237
266
|
});
|
|
238
267
|
} else {
|
|
@@ -242,11 +271,11 @@ function startServer(port, watchdog, autoRestart) {
|
|
|
242
271
|
return child;
|
|
243
272
|
}
|
|
244
273
|
|
|
245
|
-
function waitForPluginConnection(maxWaitMs) {
|
|
274
|
+
function waitForPluginConnection(port, maxWaitMs) {
|
|
246
275
|
return new Promise((resolve) => {
|
|
247
276
|
const start = Date.now();
|
|
248
277
|
const interval = setInterval(() => {
|
|
249
|
-
const status = checkChromeExtension();
|
|
278
|
+
const status = checkChromeExtension(port);
|
|
250
279
|
if (status.connected) {
|
|
251
280
|
clearInterval(interval);
|
|
252
281
|
resolve(true);
|
|
@@ -282,29 +311,31 @@ program
|
|
|
282
311
|
.option('-w, --watchdog', '启用看门狗,服务器崩溃时自动重启')
|
|
283
312
|
.option('-a, --auto-restart', '浏览器断连时自动重启 Chrome(带插件)')
|
|
284
313
|
.action(async (options) => {
|
|
285
|
-
const
|
|
286
|
-
const port = options.port ||
|
|
314
|
+
const globalConfig = getConfig();
|
|
315
|
+
const port = options.port || globalConfig.port || 9221;
|
|
316
|
+
const instanceConfig = getConfig(port);
|
|
287
317
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
saveConfig(
|
|
318
|
+
instanceConfig.port = port;
|
|
319
|
+
instanceConfig.autoRestart = !!options.autoRestart;
|
|
320
|
+
saveConfig(instanceConfig, port);
|
|
291
321
|
|
|
292
|
-
if (isServerRunning()) {
|
|
322
|
+
if (isServerRunning(port)) {
|
|
293
323
|
console.log('');
|
|
294
|
-
log('yellow', `⚠ 服务器已在运行 (PID: ${getServerPid()})`);
|
|
324
|
+
log('yellow', `⚠ 服务器已在运行 (PID: ${getServerPid(port)}, 端口: ${port})`);
|
|
295
325
|
log('cyan', ` CDP: http://localhost:${port}`);
|
|
296
326
|
} else {
|
|
297
|
-
|
|
298
|
-
cleanupLogFile();
|
|
327
|
+
ensureInstanceDir(port);
|
|
299
328
|
startServer(port, options.watchdog, options.autoRestart);
|
|
300
329
|
console.log('');
|
|
301
330
|
log('green', '✅ 服务器已启动');
|
|
302
|
-
log('cyan', `
|
|
331
|
+
log('cyan', ` 端口: ${port}`);
|
|
332
|
+
log('cyan', ` CDP: http://localhost:${port}`);
|
|
333
|
+
log('cyan', ` Plugin: ws://localhost:${port}/plugin`);
|
|
303
334
|
}
|
|
304
335
|
|
|
305
336
|
await new Promise(r => setTimeout(r, 1000));
|
|
306
337
|
|
|
307
|
-
const extStatus = checkChromeExtension();
|
|
338
|
+
const extStatus = checkChromeExtension(port);
|
|
308
339
|
if (extStatus.connected) {
|
|
309
340
|
console.log('');
|
|
310
341
|
log('green', '✅ Ready! Chrome 扩展已连接');
|
|
@@ -323,7 +354,7 @@ program
|
|
|
323
354
|
const launched = launchChromeWithExtension();
|
|
324
355
|
if (launched) {
|
|
325
356
|
log('cyan', '⏳ 等待插件连接...');
|
|
326
|
-
const connected = await waitForPluginConnection(15000);
|
|
357
|
+
const connected = await waitForPluginConnection(port, 15000);
|
|
327
358
|
if (connected) {
|
|
328
359
|
console.log('');
|
|
329
360
|
log('green', '✅ Ready! Chrome 已启动,插件已连接');
|
|
@@ -337,7 +368,7 @@ program
|
|
|
337
368
|
log('yellow', '⚠ 无法自动启动 Chrome。请手动安装插件:');
|
|
338
369
|
printExtensionGuide();
|
|
339
370
|
openChromeExtensions();
|
|
340
|
-
const connected = await waitForPluginConnection(120000);
|
|
371
|
+
const connected = await waitForPluginConnection(port, 120000);
|
|
341
372
|
if (connected) {
|
|
342
373
|
console.log('');
|
|
343
374
|
log('green', '✅ Ready! 插件已连接');
|
|
@@ -361,7 +392,7 @@ program
|
|
|
361
392
|
printExtensionGuide();
|
|
362
393
|
|
|
363
394
|
log('cyan', '⏳ 等待插件安装并连接(最多 2 分钟)...');
|
|
364
|
-
const connected = await waitForPluginConnection(120000);
|
|
395
|
+
const connected = await waitForPluginConnection(port, 120000);
|
|
365
396
|
if (connected) {
|
|
366
397
|
console.log('');
|
|
367
398
|
log('green', '✅ Ready! 插件已连接');
|
|
@@ -377,17 +408,50 @@ program
|
|
|
377
408
|
program
|
|
378
409
|
.command('stop')
|
|
379
410
|
.description('停止 CDP Tunnel 服务器')
|
|
380
|
-
.
|
|
381
|
-
|
|
382
|
-
|
|
411
|
+
.option('-p, --port <port>', '指定端口', parseInt)
|
|
412
|
+
.option('--all', '停止所有实例')
|
|
413
|
+
.action((options) => {
|
|
414
|
+
if (options.all) {
|
|
415
|
+
if (!fs.existsSync(INSTANCES_DIR)) {
|
|
416
|
+
log('yellow', '⚠️ 没有运行中的实例');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const ports = fs.readdirSync(INSTANCES_DIR).filter(dir => {
|
|
420
|
+
return fs.existsSync(getInstanceFilePath(dir, 'server.pid'));
|
|
421
|
+
});
|
|
422
|
+
if (ports.length === 0) {
|
|
423
|
+
log('yellow', '⚠️ 没有运行中的实例');
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
ports.forEach(p => {
|
|
427
|
+
const port = parseInt(p);
|
|
428
|
+
if (isServerRunning(port)) {
|
|
429
|
+
const pid = getServerPid(port);
|
|
430
|
+
try {
|
|
431
|
+
process.kill(pid, 'SIGTERM');
|
|
432
|
+
fs.unlinkSync(getInstanceFilePath(port, 'server.pid'));
|
|
433
|
+
log('green', `✓ 端口 ${port} 已停止 (PID: ${pid})`);
|
|
434
|
+
} catch (e) {
|
|
435
|
+
log('red', `✗ 端口 ${port} 停止失败: ${e.message}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
383
439
|
return;
|
|
384
440
|
}
|
|
385
|
-
|
|
386
|
-
const
|
|
441
|
+
|
|
442
|
+
const globalConfig = getConfig();
|
|
443
|
+
const port = options.port || globalConfig.port || 9221;
|
|
444
|
+
|
|
445
|
+
if (!isServerRunning(port)) {
|
|
446
|
+
log('yellow', `⚠️ 服务器未运行 (端口: ${port})`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const pid = getServerPid(port);
|
|
387
451
|
try {
|
|
388
452
|
process.kill(pid, 'SIGTERM');
|
|
389
|
-
fs.unlinkSync(
|
|
390
|
-
log('green',
|
|
453
|
+
fs.unlinkSync(getInstanceFilePath(port, 'server.pid'));
|
|
454
|
+
log('green', `✓ 服务器已停止 (端口: ${port})`);
|
|
391
455
|
} catch (e) {
|
|
392
456
|
log('red', '✗ 停止服务器失败: ' + e.message);
|
|
393
457
|
}
|
|
@@ -399,9 +463,10 @@ program
|
|
|
399
463
|
.option('-p, --port <port>', '指定端口', parseInt)
|
|
400
464
|
.option('-w, --watchdog', '启用看门狗')
|
|
401
465
|
.action(async (options) => {
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
466
|
+
const globalConfig = getConfig();
|
|
467
|
+
const port = options.port || globalConfig.port || 9221;
|
|
468
|
+
const wasRunning = isServerRunning(port);
|
|
469
|
+
const savedConfig = getConfig(port);
|
|
405
470
|
const savedWatchdog = options.watchdog;
|
|
406
471
|
|
|
407
472
|
try {
|
|
@@ -427,14 +492,14 @@ program
|
|
|
427
492
|
if (localVersion === latestVersion) {
|
|
428
493
|
log('green', '✅ 已是最新版本 (' + localVersion + ')');
|
|
429
494
|
if (wasRunning) {
|
|
430
|
-
log('cyan', ' 服务器仍在运行中');
|
|
495
|
+
log('cyan', ' 服务器仍在运行中 (端口: ' + port + ')');
|
|
431
496
|
}
|
|
432
497
|
process.exit(0);
|
|
433
498
|
}
|
|
434
499
|
|
|
435
500
|
if (wasRunning) {
|
|
436
|
-
log('yellow', '⏸
|
|
437
|
-
const pid = getServerPid();
|
|
501
|
+
log('yellow', '⏸ 停止服务器 (端口: ' + port + ')...');
|
|
502
|
+
const pid = getServerPid(port);
|
|
438
503
|
if (pid) {
|
|
439
504
|
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
440
505
|
await new Promise(r => setTimeout(r, 1500));
|
|
@@ -463,10 +528,10 @@ program
|
|
|
463
528
|
}
|
|
464
529
|
|
|
465
530
|
if (wasRunning) {
|
|
466
|
-
log('cyan', '🔄
|
|
467
|
-
const child = startServer(
|
|
531
|
+
log('cyan', '🔄 重启服务器 (端口: ' + port + ')...');
|
|
532
|
+
const child = startServer(port, false, savedConfig.autoRestart);
|
|
468
533
|
child.unref();
|
|
469
|
-
log('green', '✅ 服务器已重启 (PID: ' + child.pid + ')');
|
|
534
|
+
log('green', '✅ 服务器已重启 (PID: ' + child.pid + ', 端口: ' + port + ')');
|
|
470
535
|
} else {
|
|
471
536
|
log('cyan', ' 运行 cdp-tunnel start 启动服务器');
|
|
472
537
|
}
|
|
@@ -478,41 +543,114 @@ program
|
|
|
478
543
|
}
|
|
479
544
|
});
|
|
480
545
|
|
|
546
|
+
function printInstanceStatus(port) {
|
|
547
|
+
const config = port ? getConfig(port) : getConfig();
|
|
548
|
+
const p = port || config.port || 9221;
|
|
549
|
+
const running = isServerRunning(p);
|
|
550
|
+
const pid = running ? getServerPid(p) : null;
|
|
551
|
+
const extStatus = checkChromeExtension(p);
|
|
552
|
+
|
|
553
|
+
console.log('');
|
|
554
|
+
console.log(` 实例 [端口 ${p}]`);
|
|
555
|
+
console.log(' ' + '─'.repeat(20));
|
|
556
|
+
console.log(' 服务器: ' + (running ? '\x1b[32m运行中\x1b[0m' : '\x1b[31m已停止\x1b[0m'));
|
|
557
|
+
if (running) {
|
|
558
|
+
console.log(' PID: ' + pid);
|
|
559
|
+
console.log(' CDP: http://localhost:' + p);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (extStatus.installed && extStatus.connected) {
|
|
563
|
+
console.log(' 扩展: \x1b[32m已连接\x1b[0m');
|
|
564
|
+
} else if (extStatus.installed) {
|
|
565
|
+
console.log(' 扩展: \x1b[33m已安装但未连接\x1b[0m');
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
481
569
|
program
|
|
482
570
|
.command('status')
|
|
483
571
|
.description('查看服务器状态')
|
|
484
|
-
.
|
|
485
|
-
|
|
486
|
-
const running = isServerRunning();
|
|
487
|
-
const extStatus = checkChromeExtension();
|
|
488
|
-
|
|
572
|
+
.option('-p, --port <port>', '指定端口', parseInt)
|
|
573
|
+
.action((options) => {
|
|
489
574
|
console.log('');
|
|
490
575
|
console.log('CDP Tunnel 状态');
|
|
491
576
|
console.log('─'.repeat(30));
|
|
577
|
+
|
|
578
|
+
if (options.port) {
|
|
579
|
+
printInstanceStatus(options.port);
|
|
580
|
+
} else {
|
|
581
|
+
let foundAny = false;
|
|
582
|
+
|
|
583
|
+
if (fs.existsSync(PID_FILE) && !fs.existsSync(INSTANCES_DIR)) {
|
|
584
|
+
printInstanceStatus(null);
|
|
585
|
+
foundAny = true;
|
|
586
|
+
} else if (fs.existsSync(INSTANCES_DIR)) {
|
|
587
|
+
const ports = fs.readdirSync(INSTANCES_DIR);
|
|
588
|
+
if (ports.length > 0) {
|
|
589
|
+
ports.forEach(p => {
|
|
590
|
+
printInstanceStatus(parseInt(p));
|
|
591
|
+
foundAny = true;
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!foundAny) {
|
|
597
|
+
const globalConfig = getConfig();
|
|
598
|
+
console.log('');
|
|
599
|
+
log('yellow', ' 没有运行中的实例');
|
|
600
|
+
log('gray', ` 默认端口: ${globalConfig.port || 9221}`);
|
|
601
|
+
log('cyan', ' 启动: cdp-tunnel start [-p 端口]');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
492
604
|
console.log('');
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
program
|
|
608
|
+
.command('remote')
|
|
609
|
+
.description('查看远程连接配置指引')
|
|
610
|
+
.option('-s, --server <url>', '远程服务器地址,如 wss://example.com/plugin')
|
|
611
|
+
.action((options) => {
|
|
612
|
+
console.log('');
|
|
613
|
+
log('cyan', '📡 CDP Tunnel 远程模式配置指引');
|
|
614
|
+
console.log('─'.repeat(40));
|
|
615
|
+
console.log('');
|
|
616
|
+
|
|
617
|
+
if (options.server) {
|
|
618
|
+
log('green', '✅ 远程服务器: ' + options.server);
|
|
619
|
+
} else {
|
|
620
|
+
log('yellow', '⚠ 未指定远程地址');
|
|
621
|
+
log('gray', ' 用法: cdp-tunnel remote -s wss://your-server.com/plugin');
|
|
499
622
|
}
|
|
500
623
|
|
|
501
|
-
|
|
502
|
-
|
|
624
|
+
console.log('');
|
|
625
|
+
log('bold', ' 配置步骤:');
|
|
626
|
+
console.log('');
|
|
627
|
+
console.log(' 1. 安装 CDP Bridge 扩展到 Chrome');
|
|
628
|
+
console.log(' → 运行: cdp-tunnel extension');
|
|
629
|
+
console.log('');
|
|
630
|
+
console.log(' 2. 点击浏览器工具栏的 CDP Bridge 图标');
|
|
631
|
+
console.log('');
|
|
632
|
+
if (options.server) {
|
|
633
|
+
console.log(' 3. 在 "Server Address" 输入框中填入:');
|
|
634
|
+
log('green', ` ${options.server}`);
|
|
635
|
+
} else {
|
|
636
|
+
console.log(' 3. 在 "Server Address" 输入框中填入远程 WS 地址');
|
|
503
637
|
}
|
|
504
|
-
|
|
505
638
|
console.log('');
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
639
|
+
console.log(' 4. 点击 "Save & Connect"');
|
|
640
|
+
console.log('');
|
|
641
|
+
console.log(' 5. 在远程服务器上运行 Playwright:');
|
|
642
|
+
if (options.server) {
|
|
643
|
+
const httpUrl = options.server
|
|
644
|
+
.replace('wss://', 'https://')
|
|
645
|
+
.replace('ws://', 'http://')
|
|
646
|
+
.replace(/\/plugin$/, '');
|
|
647
|
+
console.log(` chromium.connectOverCDP('${httpUrl}')`);
|
|
511
648
|
} else {
|
|
512
|
-
console.log('
|
|
513
|
-
console.log(' 提示: 运行 cdp-tunnel extension 安装扩展');
|
|
649
|
+
console.log(' chromium.connectOverCDP("http://your-server:port")');
|
|
514
650
|
}
|
|
515
651
|
console.log('');
|
|
652
|
+
log('gray', ' 注意: 此模式不需要启动本地 cdp-tunnel server');
|
|
653
|
+
console.log('');
|
|
516
654
|
});
|
|
517
655
|
|
|
518
656
|
function generateGuideHtml() {
|
|
@@ -639,15 +777,15 @@ program
|
|
|
639
777
|
.description('诊断 CDP Tunnel 连接问题')
|
|
640
778
|
.option('-p, --port <port>', '指定端口', parseInt)
|
|
641
779
|
.action(async (options) => {
|
|
642
|
-
const
|
|
643
|
-
const port = options.port ||
|
|
780
|
+
const globalConfig = getConfig();
|
|
781
|
+
const port = options.port || globalConfig.port || 9221;
|
|
644
782
|
const http = require('http');
|
|
645
783
|
|
|
646
784
|
console.log('');
|
|
647
785
|
log('bold', '🔍 CDP Tunnel 诊断');
|
|
648
786
|
console.log('');
|
|
649
787
|
|
|
650
|
-
const running = isServerRunning();
|
|
788
|
+
const running = isServerRunning(port);
|
|
651
789
|
log(running ? 'green' : 'red', ` 1. Proxy Server: ${running ? '运行中' : '❌ 未运行'}`);
|
|
652
790
|
if (!running) {
|
|
653
791
|
log('yellow', ' → 运行 cdp-tunnel start 启动服务器');
|
|
@@ -668,7 +806,7 @@ program
|
|
|
668
806
|
log('red', ` 2. HTTP 端点: ❌ ${err.message}`);
|
|
669
807
|
}
|
|
670
808
|
|
|
671
|
-
const extStatus = checkChromeExtension();
|
|
809
|
+
const extStatus = checkChromeExtension(port);
|
|
672
810
|
log(extStatus.connected ? 'green' : 'red', ` 3. Chrome 扩展: ${extStatus.connected ? '已连接' : '❌ 未连接'}`);
|
|
673
811
|
if (!extStatus.connected) {
|
|
674
812
|
log('yellow', ' → 点击浏览器工具栏上的 CDP Bridge 图标');
|
|
@@ -737,6 +875,31 @@ program
|
|
|
737
875
|
process.exit(0);
|
|
738
876
|
});
|
|
739
877
|
|
|
878
|
+
function migrateFromLegacy() {
|
|
879
|
+
if (!fs.existsSync(PID_FILE) && !fs.existsSync(EXTENSION_STATE_FILE)) return;
|
|
880
|
+
|
|
881
|
+
const oldConfig = getConfig();
|
|
882
|
+
const port = oldConfig.port || 9221;
|
|
883
|
+
|
|
884
|
+
if (fs.existsSync(getInstanceDir(port))) return;
|
|
885
|
+
|
|
886
|
+
ensureInstanceDir(port);
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
if (fs.existsSync(PID_FILE)) {
|
|
890
|
+
fs.copyFileSync(PID_FILE, getInstanceFilePath(port, 'server.pid'));
|
|
891
|
+
fs.unlinkSync(PID_FILE);
|
|
892
|
+
}
|
|
893
|
+
if (fs.existsSync(EXTENSION_STATE_FILE)) {
|
|
894
|
+
fs.copyFileSync(EXTENSION_STATE_FILE, getInstanceFilePath(port, 'extension-state.json'));
|
|
895
|
+
fs.unlinkSync(EXTENSION_STATE_FILE);
|
|
896
|
+
}
|
|
897
|
+
saveConfig(oldConfig, port);
|
|
898
|
+
|
|
899
|
+
log('gray', ` 已迁移旧配置到实例目录 (端口: ${port})`);
|
|
900
|
+
} catch {}
|
|
901
|
+
}
|
|
902
|
+
|
|
740
903
|
program.addHelpText('after', `
|
|
741
904
|
|
|
742
905
|
常用命令:
|
|
@@ -753,4 +916,7 @@ program.addHelpText('after', `
|
|
|
753
916
|
$ cdp-tunnel start # 一行命令搞定!
|
|
754
917
|
`);
|
|
755
918
|
|
|
919
|
+
ensureConfigDir();
|
|
920
|
+
migrateFromLegacy();
|
|
921
|
+
|
|
756
922
|
program.parse();
|
|
@@ -261,7 +261,25 @@ importScripts('features/automation-badge.js');
|
|
|
261
261
|
});
|
|
262
262
|
|
|
263
263
|
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
|
|
264
|
-
if (message.type === '
|
|
264
|
+
if (message.type === 'popup-query') {
|
|
265
|
+
var ws = State.getWs();
|
|
266
|
+
var isConnected = ws && ws.readyState === WebSocket.OPEN;
|
|
267
|
+
chrome.storage.local.get(['wsAddress', 'pluginId'], function(result) {
|
|
268
|
+
sendResponse({
|
|
269
|
+
connected: isConnected,
|
|
270
|
+
pluginId: result.pluginId || null,
|
|
271
|
+
cdpClients: State.getCDPClients() || [],
|
|
272
|
+
attachedPages: State.getAttachedTabIds().map(function(tid) { return { tabId: tid }; })
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
return true;
|
|
276
|
+
} else if (message.type === 'ws-reconnect') {
|
|
277
|
+
Logger.info('[Runtime] WS address changed, reconnecting');
|
|
278
|
+
var ws = State.getWs();
|
|
279
|
+
if (ws) ws.close();
|
|
280
|
+
WebSocketManager.connect();
|
|
281
|
+
sendResponse({ success: true });
|
|
282
|
+
} else if (message.type === 'reconnect') {
|
|
265
283
|
Logger.info('[Runtime] Received reconnect request from popup');
|
|
266
284
|
var ws = State.getWs();
|
|
267
285
|
if (ws) {
|
|
@@ -162,6 +162,8 @@ var SpecialHandler = (function() {
|
|
|
162
162
|
function addTabToAutomationGroup(tabId, clientId) {
|
|
163
163
|
Logger.info('[TabGroup] Starting addTabToAutomationGroup for tabId:', tabId, 'clientId:', clientId);
|
|
164
164
|
|
|
165
|
+
WebSocketManager.send({ type: 'tabgroup-debug', tabId: tabId, clientId: clientId, phase: 'start' });
|
|
166
|
+
|
|
165
167
|
setTimeout(function() {
|
|
166
168
|
muteTabIfNeeded(tabId);
|
|
167
169
|
}, 200);
|
|
@@ -185,12 +187,20 @@ var SpecialHandler = (function() {
|
|
|
185
187
|
|
|
186
188
|
function doGroup(tabId, clientId, baseName, retries) {
|
|
187
189
|
retries = retries || 0;
|
|
190
|
+
Logger.info('[TabGroup] doGroup: tabId=' + tabId + ' clientId=' + (clientId || 'none') + ' baseName=' + baseName + ' retry=' + retries);
|
|
188
191
|
chrome.tabGroups.query({}, function(allGroups) {
|
|
192
|
+
if (chrome.runtime.lastError) {
|
|
193
|
+
Logger.error('[TabGroup] tabGroups.query failed:', chrome.runtime.lastError.message);
|
|
194
|
+
EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'query', error: chrome.runtime.lastError.message });
|
|
195
|
+
}
|
|
196
|
+
Logger.info('[TabGroup] query result: ' + (allGroups ? allGroups.length : 'null') + ' groups');
|
|
189
197
|
var existing = CDPUtils.findGroupByName(allGroups, baseName);
|
|
190
198
|
if (existing) {
|
|
199
|
+
Logger.info('[TabGroup] Found existing group:', existing.id, 'title:', existing.title);
|
|
191
200
|
chrome.tabs.group({ tabIds: tabId, groupId: existing.id }, function(result) {
|
|
192
201
|
if (chrome.runtime.lastError) {
|
|
193
202
|
Logger.error('[TabGroup] Failed to add tab to group:', chrome.runtime.lastError.message, 'retries:', retries);
|
|
203
|
+
EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'addToExisting', error: chrome.runtime.lastError.message, tabId: tabId, groupId: existing.id });
|
|
194
204
|
if (retries < 3) {
|
|
195
205
|
setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1); }, 500);
|
|
196
206
|
}
|
|
@@ -201,14 +211,18 @@ var SpecialHandler = (function() {
|
|
|
201
211
|
}
|
|
202
212
|
});
|
|
203
213
|
} else {
|
|
214
|
+
Logger.info('[TabGroup] No existing group, creating new one for tab:', tabId);
|
|
204
215
|
chrome.tabs.group({ tabIds: tabId }, function(groupId) {
|
|
205
216
|
if (chrome.runtime.lastError) {
|
|
206
217
|
Logger.error('[TabGroup] Failed to create group:', chrome.runtime.lastError.message, 'retries:', retries);
|
|
218
|
+
EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'createGroup', error: chrome.runtime.lastError.message, tabId: tabId });
|
|
207
219
|
if (retries < 3) {
|
|
208
220
|
setTimeout(function() { doGroup(tabId, clientId, baseName, retries + 1); }, 500);
|
|
209
221
|
}
|
|
210
222
|
return;
|
|
211
223
|
}
|
|
224
|
+
Logger.info('[TabGroup] chrome.tabs.group returned groupId:', groupId);
|
|
225
|
+
EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'groupCreated', tabId: tabId, groupId: groupId });
|
|
212
226
|
if (groupId) {
|
|
213
227
|
chrome.tabGroups.update(groupId, {
|
|
214
228
|
title: baseName,
|
|
@@ -217,12 +231,15 @@ var SpecialHandler = (function() {
|
|
|
217
231
|
}, function() {
|
|
218
232
|
if (chrome.runtime.lastError) {
|
|
219
233
|
Logger.error('[TabGroup] Failed to update group:', chrome.runtime.lastError.message);
|
|
234
|
+
EventBuilder.send('CDPTunnel.debug', { source: 'doGroup', phase: 'updateGroup', error: chrome.runtime.lastError.message, groupId: groupId });
|
|
220
235
|
} else {
|
|
221
236
|
State.setGroupIdForClient(clientId, groupId);
|
|
222
237
|
updateTabGroupName(clientId);
|
|
223
238
|
Logger.info('[TabGroup] Created new group:', groupId, 'with tab:', tabId);
|
|
224
239
|
}
|
|
225
240
|
});
|
|
241
|
+
} else {
|
|
242
|
+
Logger.error('[TabGroup] chrome.tabs.group returned null groupId');
|
|
226
243
|
}
|
|
227
244
|
});
|
|
228
245
|
}
|