cdp-tunnel 2.5.21 → 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 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(LOG_FILE)) {
60
- const stats = fs.statSync(LOG_FILE);
61
- if (stats.size > MAX_LOG_SIZE) {
62
- fs.writeFileSync(LOG_FILE, '');
63
- console.log('');
64
- log('yellow', '⚠ 日志文件超过 10MB,已清空');
65
- console.log('');
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
- if (fs.existsSync(CONFIG_FILE)) {
76
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
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
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
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
- if (!fs.existsSync(PID_FILE)) return false;
88
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
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(PID_FILE);
115
+ fs.unlinkSync(pidFile);
94
116
  return false;
95
117
  }
96
118
  }
97
119
 
98
- function getServerPid() {
99
- if (!fs.existsSync(PID_FILE)) return null;
100
- return parseInt(fs.readFileSync(PID_FILE, 'utf8'));
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
- if (!fs.existsSync(EXTENSION_STATE_FILE)) {
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(EXTENSION_STATE_FILE, 'utf8'));
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
- const child = spawn('node', [serverPath], {
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(PID_FILE, child.pid.toString());
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(LOG_FILE, logLine);
215
+ fs.appendFileSync(instanceLogFile, logLine);
187
216
 
188
217
  if (code === 0 && !signal) {
189
218
  log('gray', ' 服务器正常退出 (code=0),不重启');
190
- try { fs.unlinkSync(PID_FILE); } catch {}
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', ' 请检查日志: ' + LOG_FILE);
229
+ log('gray', ' 请检查日志: ' + instanceLogFile);
201
230
  console.log('');
202
- try { fs.unlinkSync(PID_FILE); } catch {}
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(PID_FILE); } catch {}
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(PID_FILE); } catch {}
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 config = getConfig();
286
- const port = options.port || config.port || 9221;
314
+ const globalConfig = getConfig();
315
+ const port = options.port || globalConfig.port || 9221;
316
+ const instanceConfig = getConfig(port);
287
317
 
288
- config.port = port;
289
- config.autoRestart = !!options.autoRestart;
290
- saveConfig(config);
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
- ensureConfigDir();
298
- cleanupLogFile();
327
+ ensureInstanceDir(port);
299
328
  startServer(port, options.watchdog, options.autoRestart);
300
329
  console.log('');
301
330
  log('green', '✅ 服务器已启动');
302
- log('cyan', ` CDP: http://localhost:${port}`);
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
- .action(() => {
381
- if (!isServerRunning()) {
382
- log('yellow', '⚠️ 服务器未运行');
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 pid = getServerPid();
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(PID_FILE);
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 config = getConfig();
403
- const wasRunning = isServerRunning();
404
- const savedPort = options.port || config.port;
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(savedPort, false, config.autoRestart);
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
- .action(() => {
485
- const config = getConfig();
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
- console.log(' 服务器: ' + (running ? '\x1b[32m运行中\x1b[0m' : '\x1b[31m已停止\x1b[0m'));
494
- console.log(' 端口: ' + config.port);
495
-
496
- if (running) {
497
- console.log(' PID: ' + getServerPid());
498
- console.log(' CDP: http://localhost:' + config.port);
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
- if (config.autoRestart) {
502
- console.log(' 自动重启: 已启用');
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
- if (extStatus.installed && extStatus.connected) {
507
- console.log(' 扩展: \x1b[32m已连接\x1b[0m');
508
- } else if (extStatus.installed) {
509
- console.log(' 扩展: \x1b[33m已安装但未连接\x1b[0m');
510
- console.log(' 提示: 请点击扩展图标连接服务器');
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(' 扩展: \x1b[31m未安装\x1b[0m');
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 config = getConfig();
643
- const port = options.port || config.port || 9221;
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 === 'reconnect') {
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) {