cdp-tunnel 3.6.0 → 3.6.2

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/README.md CHANGED
@@ -5,220 +5,309 @@
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- <strong>Chrome DevTools Protocol Bridge</strong>
8
+ <strong>Bridge Chrome's debugger API to WebSocket — control your browser with Playwright/Puppeteer via CDP</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- A Chrome extension that exposes your browser as a CDP endpoint,<br>
13
- supporting multiple Playwright/Puppeteer clients to connect simultaneously.
12
+ Control your **existing** browser (with your logins, cookies, extensions) via standard CDP protocol.<br>
13
+ No headless Chrome, no Selenium, no browser restart. Just connect and automate.
14
14
  </p>
15
15
 
16
16
  <p align="center">
17
- <a href="docs/README_CN.md">中文文档</a> |
18
- <a href="https://github.com/dyyz1993/cdp-tunnel">GitHub</a>
17
+ <a href="docs/README_CN.md">中文文档</a> ·
18
+ <a href="https://www.npmjs.com/package/cdp-tunnel">npm</a> ·
19
+ <a href="#quick-start">Quick Start</a>
19
20
  </p>
20
21
 
21
22
  <p align="center">
23
+ <img src="https://img.shields.io/npm/v/cdp-tunnel" alt="npm version">
22
24
  <img src="https://img.shields.io/github/stars/dyyz1993/cdp-tunnel?style=social" alt="GitHub stars">
23
25
  <img src="https://img.shields.io/github/forks/dyyz1993/cdp-tunnel?style=social" alt="GitHub forks">
24
- <img src="https://img.shields.io/github/watchers/dyyz1993/cdp-tunnel?style=social" alt="GitHub watchers">
25
26
  </p>
26
27
 
27
28
  ---
28
29
 
30
+ ## What It Does
31
+
32
+ CDP Tunnel lets standard CDP clients (Playwright/Puppeteer/CDP SDK) control your **real Chrome browser** — the one with your logins, cookies, and extensions. It works like `chrome --remote-debugging-port`, but **without restarting Chrome**.
33
+
34
+ **Key capabilities:**
35
+ - ✅ Control your existing browser (keep logins/cookies/extensions)
36
+ - ✅ Standard CDP protocol — Playwright/Puppeteer work out of the box
37
+ - ✅ Multiple isolated environments (port pool — each port = one independent "browser")
38
+ - ✅ API Key authentication for remote/cloud deployment
39
+ - ✅ Built-in admin console (browser list, key management, CDP operations)
40
+ - ✅ Tab Group isolation — automation tabs never mix with user tabs
41
+ - ✅ Takeover mode — attach to user's existing tabs
42
+
29
43
  ## Architecture
30
44
 
31
45
  ```
32
- ┌─────────────────────────────────────────────────────────────────┐
33
- Proxy Server
34
- (localhost:9221)
35
- │ │
36
- │ /plugin ←─── Chrome Extension (WebSocket) │
37
- HTTP ←─── Playwright/Puppeteer Clients
38
- └─────────────────────────────────────────────────────────────────┘
39
- ↑ ↑ ↑
40
-
41
- Client 1 Client 2 Client 3
42
- (clientId_1) (clientId_2) (clientId_3)
46
+ Playwright/Puppeteer Chrome Extension
47
+
48
+ ws://localhost:9221/client ws://localhost:9221/plugin
49
+ ▼ ▼
50
+ ┌─────────────────────────────────────────────┐
51
+ CDP Tunnel Proxy
52
+ │ (Node.js WebSocket) │
53
+ │ │
54
+ 9221 ── create mode (port pool #0)
55
+ │ 9220 ── takeover mode (attach user tabs) │
56
+ │ 9231+ ── port pool (each = isolated env)
57
+ │ /admin ── web management console │
58
+ └─────────────────────────────────────────────┘
59
+ │ │
60
+ ▼ ▼
61
+ chrome.debugger API Tab Group isolation
43
62
  ```
44
63
 
45
- ## Features
64
+ ## Quick Start
46
65
 
47
- - **Multi-client Support** - Multiple Playwright/Puppeteer clients can connect simultaneously
48
- - **Message Isolation** - Pages created by each client are owned by that client
49
- - **Configuration Page** - Visualize connection status, client list, and controlled pages
50
- - **Auto Reconnect** - Extension automatically reconnects to the server
66
+ ### 1. Install
51
67
 
52
- ## Screenshot
68
+ ```bash
69
+ npm install -g cdp-tunnel
70
+ cdp-tunnel setup # Start server + auto-load extension
71
+ ```
53
72
 
54
- ![Config Page](docs/config-page-screenshot.png)
73
+ Or run from source:
55
74
 
56
- ## Quick Start
75
+ ```bash
76
+ git clone https://github.com/dyyz1993/cdp-tunnel.git
77
+ cd cdp-tunnel
78
+ npm install
79
+ node server/proxy-server.js
80
+ ```
57
81
 
58
- ### Option 1: Install Extension Only (No npm needed)
82
+ ### 2. Load Extension
59
83
 
60
- Download and install the Chrome extension directly:
84
+ 1. Open `chrome://extensions/`
85
+ 2. Enable **Developer mode**
86
+ 3. Click **Load unpacked** → select `extension-new/` directory
87
+ 4. Extension connects to `ws://localhost:9221/plugin` automatically
61
88
 
62
- 1. Download [`cdp-tunnel-extension.zip`](https://github.com/dyyz1993/cdp-tunnel/releases/latest/download/cdp-tunnel-extension.zip) from the latest release
63
- 2. Unzip it
64
- 3. Open `chrome://extensions/` in Chrome
65
- 4. Enable **Developer mode** (top right)
66
- 5. Click **Load unpacked** → select the unzipped folder
67
- 6. Start the proxy server separately (see Option 2 or 3 below)
89
+ ### 3. Connect Your Automation
68
90
 
69
- ### Option 2: Install via npm (Recommended)
91
+ ```javascript
92
+ // Playwright — control your real browser!
93
+ const { chromium } = require('playwright');
94
+ const browser = await chromium.connectOverCDP('http://localhost:9221');
95
+ const page = await browser.contexts()[0].newPage();
96
+ await page.goto('https://www.google.com');
97
+ console.log(await page.title());
98
+ ```
70
99
 
71
- ```bash
72
- # One command to set up everything
73
- npx cdp-tunnel setup
100
+ ```python
101
+ # Python Playwright
102
+ from playwright.sync_api import sync_playwright
74
103
 
75
- # Or install globally
76
- npm install -g cdp-tunnel
77
- cdp-tunnel setup # Start server + auto-load extension into Chrome
78
- cdp-tunnel status # Check status
79
- cdp-tunnel diagnose # Diagnose connection issues
104
+ with sync_playwright() as p:
105
+ browser = p.chromium.connect_over_cdp("http://localhost:9221")
106
+ page = browser.contexts[0].new_page()
107
+ page.goto("https://www.google.com")
108
+ print(page.title())
80
109
  ```
81
110
 
82
- ### Option 3: Manual Installation
111
+ ## Key Features
112
+
113
+ ### Port Pool — Multiple Isolated Environments
83
114
 
84
- #### 1. Start the Proxy Server
115
+ Each port = one independent "browser" with its own Tab Group. Different ports can't see each other's tabs.
85
116
 
86
117
  ```bash
87
- npx cdp-tunnel start
88
- # or manually:
89
- npx cdp-tunnel start -p 9221
118
+ # Default ports
119
+ 9221 ── Main create port (port pool #0)
120
+ 9220 ── Takeover port (attach user's existing tabs)
121
+ 9231-9239 ── Port pool (9 isolated environments)
122
+ ```
123
+
124
+ ```javascript
125
+ // Each port is independent
126
+ const browserA = await chromium.connectOverCDP('http://localhost:9231');
127
+ const browserB = await chromium.connectOverCDP('http://localhost:9232');
128
+ // browserA and browserB have completely separate tabs
90
129
  ```
91
130
 
92
- #### 2. Install Chrome Extension
131
+ ### Takeover Mode Control Existing Tabs
93
132
 
94
- 1. Open `chrome://extensions/`
95
- 2. Enable "Developer mode"
96
- 3. Click "Load unpacked"
97
- 4. Select the `extension-new` directory
133
+ Connect to port `9220` to take over the user's already-open tabs (no new Tab Group created).
134
+
135
+ ```javascript
136
+ const browser = await chromium.connectOverCDP('http://localhost:9220');
137
+ // Now you can see and control all user's open tabs
138
+ ```
139
+
140
+ ### API Key Authentication (Remote Deployment)
98
141
 
99
- #### 3. Connect the Extension
142
+ Enable authentication for cloud/remote deployment:
100
143
 
101
- Click the extension icon, then click **打开完整配置** to open the configuration page. Add a connection: fill in Name (e.g., "local"), WebSocket URL (default `ws://localhost:9221/plugin`), and Mode (create/takeover). The CDP address will be displayed (e.g., `http://localhost:9221` for create mode, `http://localhost:9222` for takeover mode). Copy it and use it in Playwright: `chromium.connectOverCDP('http://localhost:9221')`.
144
+ ```bash
145
+ REQUIRE_AUTH=true node server/proxy-server.js
102
146
 
103
- ### 3. Client Connection
147
+ # Create an API key
148
+ node server/saas/key-manager.js create my-browser
149
+ # Output: ws://localhost:9221/plugin?key=cdp_xxxxx
150
+ ```
104
151
 
105
152
  ```javascript
106
- // Playwright
107
- const { chromium } = require('playwright');
153
+ // Client connects with key
154
+ const browser = await chromium.connectOverCDP(
155
+ 'ws://your-server.com:9221/client?key=cdp_xxxxx'
156
+ );
157
+ ```
108
158
 
109
- const browser = await chromium.connectOverCDP('http://localhost:9221');
110
- const context = browser.contexts()[0];
111
- const page = await context.newPage();
112
- await page.goto('https://example.com');
159
+ **One key = one browser = one isolated Tab Group.** Different keys are completely isolated.
160
+
161
+ ### Admin Console
113
162
 
114
- // Puppeteer
115
- const puppeteer = require('puppeteer');
163
+ Built-in web UI at `/admin`:
116
164
 
117
- const browser = await puppeteer.connect({
118
- browserWSEndpoint: 'ws://localhost:9221'
119
- });
120
- const page = await browser.newPage();
121
- await page.goto('https://example.com');
165
+ ```
166
+ http://localhost:9221/admin
122
167
  ```
123
168
 
124
- ### 4. Using with agent-browser
169
+ Features:
170
+ - 📱 Online browser list (real-time)
171
+ - 🔑 API Key management (create/revoke + copy address)
172
+ - 🔧 Tab management (list/close/switch)
173
+ - ⚡ CDP operations (evaluate JS, screenshot, new tab)
174
+ - 🎬 One-click demo
125
175
 
126
- [agent-browser](https://github.com/nick1udwig/agent-browser) is a browser automation CLI that can connect to CDP Tunnel:
176
+ ### Version Check
127
177
 
128
178
  ```bash
129
- # Install agent-browser
130
- npm install -g agent-browser
131
-
132
- # Connect to CDP Tunnel
133
- agent-browser --cdp http://localhost:9221 open https://example.com
179
+ STRICT_VERSION=true node server/proxy-server.js
180
+ # Extension version must match proxy version, otherwise connection rejected
181
+ ```
134
182
 
135
- # Take a snapshot
136
- agent-browser --cdp http://localhost:9221 snapshot -i
183
+ ## Remote / Cloud Deployment
137
184
 
138
- # Interact with elements
139
- agent-browser --cdp http://localhost:9221 click @e1
140
- agent-browser --cdp http://localhost:9221 fill @e2 "hello"
185
+ CDP Tunnel can be deployed to a VPS for remote browser control:
141
186
 
142
- # Take screenshot
143
- agent-browser --cdp http://localhost:9221 screenshot output.png
187
+ ```
188
+ Remote Client ──wss──▶ Cloud Proxy ──wss──▶ User's Chrome + Extension
144
189
  ```
145
190
 
146
- This allows you to control the browser through CDP Tunnel using simple CLI commands.
191
+ See [`docs/DEPLOY-CLOUDFLARE.md`](docs/DEPLOY-CLOUDFLARE.md) for Cloudflare Tunnel setup.
147
192
 
148
- ## Multi-client Usage
193
+ Key environment variables:
149
194
 
150
- All clients connect to the same endpoint `http://localhost:9221`. The server automatically assigns a unique `clientId` to each connection.
195
+ | Variable | Default | Description |
196
+ |---|---|---|
197
+ | `PORT` | 9221 | Main create port |
198
+ | `TAKEOVER_PORT` | 9220 | Takeover port |
199
+ | `POOL_START` | 9231 | Port pool start |
200
+ | `POOL_SIZE` | 9 | Number of port pool ports |
201
+ | `REQUIRE_AUTH` | false | Require API key |
202
+ | `STRICT_VERSION` | false | Reject version mismatch |
203
+ | `ADMIN_TOKEN` | (none) | Admin console auth token |
151
204
 
152
- ```javascript
153
- // Multiple clients can connect simultaneously
154
- const browser1 = await chromium.connectOverCDP('http://localhost:9221');
155
- const browser2 = await chromium.connectOverCDP('http://localhost:9221');
156
- const browser3 = await chromium.connectOverCDP('http://localhost:9221');
205
+ ## CLI Commands
157
206
 
158
- // Pages created by each client are independent
159
- const page1 = await browser1.contexts()[0].newPage();
160
- const page2 = await browser2.contexts()[0].newPage();
161
- const page3 = await browser3.contexts()[0].newPage();
207
+ ```bash
208
+ cdp-tunnel setup # Start server + load extension
209
+ cdp-tunnel start # Start server only
210
+ cdp-tunnel stop # Stop server
211
+ cdp-tunnel status # Check status
212
+ cdp-tunnel diagnose # Diagnose connection
213
+ cdp-tunnel extension # Open extension installation guide
162
214
  ```
163
215
 
164
- ## Configuration Page
216
+ ## API Key Management
165
217
 
166
- Click the extension icon to open the configuration page, where you can view:
167
-
168
- - **CDP Client List** - Shows connected Playwright/Puppeteer clients
169
- - **Controlled Pages List** - Shows controlled pages with click-to-navigate support
170
- - **Activity Log** - Connection status change records
218
+ ```bash
219
+ # Create a key (returns connection address)
220
+ node server/saas/key-manager.js create "张三的浏览器"
171
221
 
172
- ## Project Structure
222
+ # List all keys
223
+ node server/saas/key-manager.js list
173
224
 
174
- ```
175
- cdp-tunnel/
176
- ├── server/
177
- │ └── proxy-server.js # Proxy server
178
- ├── extension-new/
179
- │ ├── background.js # Extension Service Worker
180
- │ ├── config-page-preview.html # Configuration page
181
- │ ├── config-page.js # Configuration page script
182
- │ ├── core/
183
- │ │ ├── state.js # State management
184
- │ │ └── websocket.js # WebSocket connection management
185
- │ └── features/
186
- │ ├── cdp-router.js # CDP message routing
187
- │ └── screencast.js # Screenshot functionality
188
- └── tests/
189
- ├── playwright-single.js # Single client test
190
- ├── playwright-multi.js # Multi-client test
191
- └── playwright-interactive.js # Interactive test
225
+ # Revoke a key
226
+ node server/saas/key-manager.js revoke <keyId>
192
227
  ```
193
228
 
194
229
  ## Testing
195
230
 
196
231
  ```bash
197
- # Single client test
198
- node tests/playwright-single.js
232
+ # Smoke tests (runs before every commit via husky pre-commit)
233
+ node tests/e2e/run-all.js
234
+
235
+ # A/B Gate (compare with direct Chrome CDP)
236
+ node tests/e2e/test-ab-gate.js
199
237
 
200
- # Multi-client test
201
- node tests/playwright-multi.js
238
+ # Key isolation test
239
+ node tests/e2e/test-key-isolation.js
202
240
 
203
- # Interactive test
204
- node tests/playwright-interactive.js
241
+ # Admin console test
242
+ node tests/e2e/test-admin-console.js
243
+
244
+ # Production deployment verification
245
+ PROD_WSS=wss://your-server:port PROD_KEY=cdp_xxx node tests/e2e/test-prod-deploy.js
205
246
  ```
206
247
 
207
- ## Notes
248
+ ## How It Works
208
249
 
209
- 1. **Port Availability** - Ensure port 9221 is not in use
210
- 2. **Extension Permissions** - The extension requires `debugger`, `tabs`, and other permissions
211
- 3. **Browser Limitation** - Only one extension can control a browser via debugger at a time
250
+ CDP Tunnel uses Chrome's `chrome.debugger` API (available to extensions) to bridge CDP commands:
212
251
 
213
- ## License
252
+ ```
253
+ Playwright command (e.g., Page.navigate)
254
+ → CDP Tunnel Proxy (WebSocket)
255
+ → Chrome Extension (chrome.debugger.attach)
256
+ → Chrome renders the page
257
+ → Events flow back: Chrome → Extension → Proxy → Playwright
258
+ ```
259
+
260
+ The extension acts as the bridge — it receives CDP commands via WebSocket and executes them via `chrome.debugger`. This means:
261
+ - Your browser keeps all logins, cookies, extensions
262
+ - No need to restart Chrome or use `--remote-debugging-port`
263
+ - Works with regular Chrome (not just Chromium)
264
+
265
+ **Synthetic input handling:** Chrome throttles synthetic events (keyboard/mouse) on non-active tabs. CDP Tunnel automatically calls `Page.bringToFront` before synthetic input commands, making it transparent to Playwright/Puppeteer.
214
266
 
215
- This project is licensed under the Apache License 2.0 with Attribution Requirement.
267
+ ## Project Structure
268
+
269
+ ```
270
+ cdp-tunnel/
271
+ ├── server/
272
+ │ ├── proxy-server.js # Main proxy server
273
+ │ ├── modules/
274
+ │ │ ├── config.js # Port configuration
275
+ │ │ └── port-pool.js # Port pool manager (v3.0+)
276
+ │ ├── saas/
277
+ │ │ ├── auth.js # API Key + JWT authentication
278
+ │ │ ├── key-manager.js # Key management CLI
279
+ │ │ └── db.js # SQLite database
280
+ │ └── admin-console.html # Admin web UI
281
+ ├── extension-new/ # Chrome Extension (Manifest V3)
282
+ │ ├── background.js # Service Worker
283
+ │ ├── core/
284
+ │ │ ├── websocket.js # WebSocket connection
285
+ │ │ ├── connection-state.js # State management
286
+ │ │ └── connection-manager.js# Multi-connection
287
+ │ ├── cdp/
288
+ │ │ ├── handler/
289
+ │ │ │ ├── special.js # Tab grouping logic
290
+ │ │ │ └── forward.js # CDP command forwarding
291
+ │ │ └── response.js # CDP response builder
292
+ │ └── utils/
293
+ │ ├── config.js # Extension config
294
+ │ └── helpers.js # Group naming/color
295
+ ├── cli/ # CLI tool
296
+ ├── tests/e2e/ # E2E tests
297
+ └── docs/ # Documentation
298
+ ```
299
+
300
+ ## Changelog
301
+
302
+ See [CHANGELOG.md](CHANGELOG.md) for version history.
303
+
304
+ ## License
216
305
 
217
- See [LICENSE](LICENSE) for details.
306
+ Apache License 2.0 with Attribution Requirement. See [LICENSE](LICENSE).
218
307
 
219
308
  ---
220
309
 
221
- If you use this project in your work, please include attribution:
310
+ If you use this project, please include attribution:
222
311
  - Project: CDP Tunnel
223
312
  - Author: dyyz1993
224
313
  - Source: https://github.com/dyyz1993/cdp-tunnel
@@ -76,7 +76,6 @@ importScripts('features/automation-badge.js');
76
76
  var primary = ConnectionManager.getPrimaryConnection();
77
77
  if (primary && result.currentTabId != null) {
78
78
  primary.state.currentTabId = result.currentTabId;
79
- primary.state.isAttached = result.isAttached;
80
79
  }
81
80
 
82
81
  ConnectionManager.connectAll();
@@ -196,9 +195,14 @@ importScripts('features/automation-badge.js');
196
195
  }
197
196
 
198
197
  chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
199
- if (changeInfo.status === 'complete') {
198
+ if (changeInfo.status === 'complete' || changeInfo.title) {
200
199
  var entry = ConnectionManager.getConnectionByTabId(tabId);
201
200
  if (entry && entry.state.isTabAttached(tabId)) {
201
+ LocalHandler.getTargetInfoById(String(tabId)).then(function(targetInfo) {
202
+ if (targetInfo) {
203
+ EventBuilder.send('Target.targetInfoChanged', { targetInfo: targetInfo }, null, entry.wsManager);
204
+ }
205
+ });
202
206
  }
203
207
  }
204
208
 
@@ -29,7 +29,7 @@ var ForwardHandler = (function() {
29
29
 
30
30
  Logger.debug('[Forward]', method, '-> tabId:', tabId);
31
31
 
32
- // 合成输入事件需要页面 visible:先 ensureVisible 再发命令
32
+ // 合成输入事件 + 带 clip 的元素截图需要页面 visible:先 ensureVisible 再发命令
33
33
  if (SYNTHETIC_INPUT_METHODS.indexOf(method) >= 0) {
34
34
  return ensureVisible(tabId).then(function() {
35
35
  return chrome.debugger.sendCommand({ tabId: tabId }, method, params);
@@ -38,6 +38,15 @@ var ForwardHandler = (function() {
38
38
  });
39
39
  }
40
40
 
41
+ // 元素级截图(带 clip)在 hidden 状态下可能返回空数据,需要 ensureVisible 确保渲染完成
42
+ if (method === 'Page.captureScreenshot' && params && params.clip) {
43
+ return ensureVisible(tabId).then(function() {
44
+ return chrome.debugger.sendCommand({ tabId: tabId }, method, params);
45
+ }).then(function(result) {
46
+ return result || {};
47
+ });
48
+ }
49
+
41
50
  return chrome.debugger.sendCommand({ tabId: tabId }, method, params).then(function(result) {
42
51
  return result || {};
43
52
  });
@@ -5,6 +5,12 @@ var LocalHandler = (function() {
5
5
 
6
6
  function _getConnectionTag(ctx) {
7
7
  var wm = ctx._wsManager;
8
+ var state = ctx._state;
9
+ var clientId = ctx.clientId;
10
+ if (state && clientId && state.getTagForClient) {
11
+ var tag = state.getTagForClient(clientId);
12
+ if (tag) return tag;
13
+ }
8
14
  return (wm && wm.config && wm.config.tag) || null;
9
15
  }
10
16
 
@@ -32,11 +38,29 @@ var LocalHandler = (function() {
32
38
  };
33
39
  }
34
40
 
35
- function getWindowBounds() {
36
- return {
37
- bounds: { left: 0, top: 0, width: 1920, height: 1080, windowState: 'normal' }
38
- };
39
- }
41
+ function getWindowBounds() {
42
+ return {
43
+ bounds: { left: 0, top: 0, width: 1920, height: 1080, windowState: 'normal' }
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Browser.setWindowBounds — 设置窗口位置/大小,支持 focused。
49
+ * 当 bounds.focused = true 时,用 Chrome 扩展 API 将窗口弹到前台。
50
+ * 这是跨平台最可靠的方式(替代 CDP 原生 setWindowBounds 在 macOS 上的限制)。
51
+ */
52
+ function setWindowBounds(context) {
53
+ var params = context ? context.params : null;
54
+ var bounds = params ? params.bounds : null;
55
+ if (bounds && bounds.focused) {
56
+ chrome.windows.getCurrent(function(w) {
57
+ if (w && w.id) {
58
+ chrome.windows.update(w.id, { focused: true });
59
+ }
60
+ });
61
+ }
62
+ return {};
63
+ }
40
64
 
41
65
  function targetSetDiscoverTargets(context) {
42
66
  var state = _getState(context);
@@ -517,7 +541,8 @@ var LocalHandler = (function() {
517
541
  browserGetVersion: browserGetVersion,
518
542
  browserClose: browserClose,
519
543
  getWindowForTarget: getWindowForTarget,
520
- getWindowBounds: getWindowBounds,
544
+ getWindowBounds: getWindowBounds,
545
+ setWindowBounds: setWindowBounds,
521
546
  targetSetDiscoverTargets: targetSetDiscoverTargets,
522
547
  targetGetTargets: targetGetTargets,
523
548
  targetGetTargetInfo: targetGetTargetInfo,
@@ -785,14 +785,6 @@ function checkTabVisibility(tabId) {
785
785
  });
786
786
  }
787
787
 
788
- function pageCreateIsolatedWorld(context) {
789
- return ForwardHandler.execute(context);
790
- }
791
-
792
- function pageAddScriptToEvaluateOnNewDocument(context) {
793
- return ForwardHandler.execute(context);
794
- }
795
-
796
788
  function domSetFileInputFiles(context) {
797
789
  var params = context.params;
798
790
  var sessionId = context.sessionId;
@@ -899,8 +891,6 @@ function checkTabVisibility(tabId) {
899
891
  pageStartScreencast: pageStartScreencast,
900
892
  pageStopScreencast: pageStopScreencast,
901
893
  pageScreencastFrameAck: pageScreencastFrameAck,
902
- pageCreateIsolatedWorld: pageCreateIsolatedWorld,
903
- pageAddScriptToEvaluateOnNewDocument: pageAddScriptToEvaluateOnNewDocument,
904
894
  runtimeRunIfWaitingForDebugger: runtimeRunIfWaitingForDebugger,
905
895
  domSetFileInputFiles: domSetFileInputFiles,
906
896
  updateTabGroupName: updateTabGroupName,
@@ -5,7 +5,7 @@ var CDP_HANDLERS = {
5
5
  'Browser.crash': { type: 'LOCAL', handler: LocalHandler.emptyResult },
6
6
  'Browser.crashGpuProcess': { type: 'LOCAL', handler: LocalHandler.emptyResult },
7
7
  'Browser.getWindowForTarget': { type: 'LOCAL', handler: LocalHandler.getWindowForTarget },
8
- 'Browser.setWindowBounds': { type: 'LOCAL', handler: LocalHandler.emptyResult },
8
+ 'Browser.setWindowBounds': { type: 'LOCAL', handler: LocalHandler.setWindowBounds },
9
9
  'Browser.getWindowBounds': { type: 'LOCAL', handler: LocalHandler.getWindowBounds },
10
10
  'Browser.getBrowserCommandLine': { type: 'LOCAL', handler: LocalHandler.emptyArray },
11
11
  'Browser.getHistograms': { type: 'LOCAL', handler: LocalHandler.emptyArray },
@@ -50,8 +50,6 @@ var CDP_HANDLERS = {
50
50
  'Page.startScreencast': { type: 'SPECIAL', handler: SpecialHandler.pageStartScreencast },
51
51
  'Page.stopScreencast': { type: 'SPECIAL', handler: SpecialHandler.pageStopScreencast },
52
52
  'Page.screencastFrameAck': { type: 'SPECIAL', handler: SpecialHandler.pageScreencastFrameAck },
53
- 'Page.createIsolatedWorld': { type: 'FORWARD', handler: SpecialHandler.pageCreateIsolatedWorld },
54
- 'Page.addScriptToEvaluateOnNewDocument': { type: 'FORWARD', handler: SpecialHandler.pageAddScriptToEvaluateOnNewDocument },
55
53
 
56
54
  'Runtime.runIfWaitingForDebugger': { type: 'SPECIAL', handler: SpecialHandler.runtimeRunIfWaitingForDebugger },
57
55
 
@@ -1,14 +1,12 @@
1
1
  function ConnectionState(connectionId, mode) {
2
2
  this.connectionId = connectionId;
3
3
  this.mode = mode || 'create';
4
- this.connectionTag = null;
5
4
  this.clientIdToTag = new Map();
6
5
  this.ws = null;
7
6
  this.reconnectTimer = null;
8
7
  this._hasConnectedClient = false;
9
8
  this.cdpClients = [];
10
9
  this.currentTabId = null;
11
- this.isAttached = false;
12
10
 
13
11
  this.sessionIdToTabId = new Map();
14
12
  this.sessionIdToTargetId = new Map();
@@ -372,18 +370,7 @@ ConnectionState.prototype.clearAllState = function() {
372
370
 
373
371
  ConnectionState.prototype.persist = function(tabId, attached) {
374
372
  this.currentTabId = tabId;
375
- this.isAttached = attached;
376
373
  return new Promise(function(resolve) {
377
374
  chrome.storage.local.set({ currentTabId: tabId, isAttached: attached }, resolve);
378
375
  }.bind(this));
379
376
  };
380
-
381
- ConnectionState.prototype.loadPersisted = function() {
382
- return new Promise(function(resolve) {
383
- chrome.storage.local.get(['currentTabId', 'isAttached'], function(result) {
384
- this.currentTabId = result.currentTabId || null;
385
- this.isAttached = result.isAttached || false;
386
- resolve(result);
387
- }.bind(this));
388
- }.bind(this));
389
- };
@@ -187,10 +187,6 @@ var DebuggerManager = (function() {
187
187
  });
188
188
  }
189
189
 
190
- function isAttached(tabId) {
191
- return getActualAttachState(tabId);
192
- }
193
-
194
190
  function getActualAttachState(tabId) {
195
191
  return chrome.debugger.getTargets().then(function(targets) {
196
192
  var target = targets.find(function(t) {
@@ -364,7 +360,6 @@ var DebuggerManager = (function() {
364
360
  return {
365
361
  attach: attach,
366
362
  detach: detach,
367
- isAttached: isAttached,
368
363
  getActualAttachState: getActualAttachState,
369
364
  handleDebuggerEvent: handleDebuggerEvent,
370
365
  handleDetach: handleDetach
@@ -62,14 +62,6 @@ var State = (function() {
62
62
  });
63
63
  }
64
64
 
65
- function persist(tabId, attached) {
66
- _state.currentTabId = tabId;
67
- _state.isAttached = attached;
68
- return new Promise(function(resolve) {
69
- chrome.storage.local.set({ currentTabId: tabId, isAttached: attached }, resolve);
70
- });
71
- }
72
-
73
65
  function isTabAttached(tabId) {
74
66
  var entry = ConnectionManager.getConnectionByTabId(tabId);
75
67
  return entry ? entry.state.isTabAttached(tabId) : false;
@@ -343,21 +343,6 @@ var WebSocketConnection = (function() {
343
343
  self._handleServerRestart();
344
344
  break;
345
345
 
346
- case 'cdp':
347
- if (method) {
348
- routeCDPCommand({
349
- id: id,
350
- method: method,
351
- params: params,
352
- tabId: tabId,
353
- sessionId: sessionId,
354
- clientId: message.__clientId,
355
- mode: message.__mode,
356
- connectionId: self.connectionId
357
- }, self.state, self);
358
- }
359
- break;
360
-
361
346
  default:
362
347
  if (method) {
363
348
  routeCDPCommand({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "CDP Bridge",
4
- "version": "3.6.0",
4
+ "version": "3.6.2",
5
5
  "description": "Chrome DevTools Protocol Bridge for Playwright/Puppeteer automation",
6
6
  "permissions": [
7
7
  "debugger",
@@ -1,5 +1,5 @@
1
1
  var Config = {
2
- WS_URL: 'ws://localhost:14576/plugin',
2
+ WS_URL: 'ws://localhost:51100/plugin',
3
3
  RECONNECT_DELAY: 3000,
4
4
  DEBUGGER_VERSION: '1.3',
5
5
  HEARTBEAT_INTERVAL: 25000,
package/package.json CHANGED
@@ -1,20 +1,13 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "3.6.0",
3
+ "version": "3.6.2",
4
4
  "description": "Bridge Chrome's debugger API to WebSocket — control your existing browser with Playwright/Puppeteer via CDP",
5
5
  "main": "server/proxy-server.js",
6
- "bin": "./cli/index.js",
6
+ "bin": {
7
+ "cdp-tunnel": "./cli/index.js"
8
+ },
7
9
  "scripts": {
8
10
  "start": "node server/proxy-server.js",
9
- "client": "node client/playwright-client.js",
10
- "client:puppeteer": "node client/puppeteer-client.js",
11
- "test:compat": "node client/test-compat-client.js",
12
- "test:newpage": "node client/test-newpage-sequence.js",
13
- "record": "node cdp-recorder/record.js",
14
- "benchmark": "node benchmark/run-all.js",
15
- "benchmark:native": "node benchmark/run-all.js --native",
16
- "benchmark:proxy": "node benchmark/run-all.js --proxy",
17
- "demo": "node demo/server.js",
18
11
  "test:compare": "node tests/cdp-compare/cdp-compare-runner.js",
19
12
  "test": "node tests/e2e/run-all.js",
20
13
  "test:e2e": "node tests/e2e/run-all.js",
@@ -68,11 +61,6 @@
68
61
  "devDependencies": {
69
62
  "chrome-remote-interface": "^0.34.0",
70
63
  "husky": "^9.1.7",
71
- "lz-string": "^1.5.0",
72
- "pako": "^2.1.0",
73
- "playwright": "^1.58.2",
74
- "playwright-core": "^1.58.2",
75
- "puppeteer-core": "^22.0.0",
76
- "sharp": "^0.34.0"
64
+ "playwright": "^1.58.2"
77
65
  }
78
66
  }
@@ -143,8 +143,6 @@ class PortPoolManager {
143
143
  server.listen(port, '0.0.0.0', () => {
144
144
  console.log(`[CREATE PORT ${portIndex}] Listening on ${port}`);
145
145
  });
146
-
147
- this.createServers[portIndex] = server;
148
146
  }
149
147
 
150
148
  _startTakeoverPort() {
@@ -336,7 +334,15 @@ class PortPoolManager {
336
334
 
337
335
  ws.on('close', () => {
338
336
  session.clients.delete(ws);
339
- console.log(`[PORT ${session.port}] Client disconnected (remaining: ${session.clients.size})`);
337
+ // 内存泄漏修复:清理该 client pendingRequests 和 sessionToClient 引用,
338
+ // 避免 plugin 永不响应时 entry 一直挂着 ws 对象
339
+ if (ws._origIds) ws._origIds.clear();
340
+ for (const [reqId, pending] of session.pendingRequests.entries()) {
341
+ if (pending.clientWs === ws) session.pendingRequests.delete(reqId);
342
+ }
343
+ for (const [sid, cws] of session.sessionToClient.entries()) {
344
+ if (cws === ws) session.sessionToClient.delete(sid);
345
+ }
340
346
  // 对齐原生 Chrome:断开不清理 tab
341
347
  });
342
348
 
@@ -483,6 +489,34 @@ class PortPoolManager {
483
489
  }
484
490
  }
485
491
 
492
+ // 内存泄漏修复:target 销毁/detach 时清理 sessionToPort、targetToPort、sessionToClient
493
+ // 之前只在 closeTarget 响应路径清理,事件路径漏了 → sessionToPort 无界增长
494
+ if (msg.method === 'Target.targetDestroyed' && targetId) {
495
+ const portIdx = this.targetToPort.get(targetId);
496
+ this.targetToPort.delete(targetId);
497
+ if (portIdx !== undefined) {
498
+ const sess = this.portSessions[portIdx];
499
+ if (sess) {
500
+ sess.targetIds.delete(targetId);
501
+ sess.targetUrls.delete(targetId);
502
+ }
503
+ }
504
+ }
505
+ if (msg.method === 'Target.detachedFromTarget') {
506
+ const sid = msg.params.sessionId;
507
+ if (sid) {
508
+ this.sessionToPort.delete(sid);
509
+ const portIdx = this.sessionToPort.get(sid);
510
+ if (portIdx !== undefined && this.portSessions[portIdx]) {
511
+ this.portSessions[portIdx].sessionToClient.delete(sid);
512
+ }
513
+ }
514
+ if (targetId) {
515
+ // detach 也意味着 target 不再活跃,清理 targetToPort(保留 targetIds 历史用于 list)
516
+ this.targetToPort.delete(targetId);
517
+ }
518
+ }
519
+
486
520
  if (targetId) {
487
521
  const portIndex = this.targetToPort.get(targetId);
488
522
  if (portIndex !== undefined) {
@@ -158,7 +158,19 @@ function updateExtensionState(connected) {
158
158
  clearLog();
159
159
 
160
160
  const wss = new WebSocket.Server({ noServer: true });
161
- const server = http.createServer((req, res) => handleHttpRequest(req, res));
161
+ const server = http.createServer(async (req, res) => {
162
+ try {
163
+ await handleHttpRequest(req, res);
164
+ } catch (err) {
165
+ logCDP('error', 'Unhandled error in handleHttpRequest', { url: req.url, message: err.message });
166
+ if (!res.headersSent) {
167
+ try {
168
+ res.writeHead(500, { 'Content-Type': 'application/json' });
169
+ res.end(JSON.stringify({ error: 'Internal proxy error' }));
170
+ } catch (_) {}
171
+ }
172
+ }
173
+ });
162
174
 
163
175
  const pluginConnections = new Set();
164
176
  const clientConnections = new Set();
@@ -166,7 +178,6 @@ const clientConnections = new Set();
166
178
  class PluginNamespace {
167
179
  constructor() {
168
180
  this.sessionToClientId = new Map();
169
- this.pendingAttachRequests = new Map();
170
181
  this.pendingAttachedEvents = new Map();
171
182
  this.pendingTargetCreatedEvents = new Map();
172
183
  this.targetIdToClientId = new Map();
@@ -237,8 +248,6 @@ async function requestVersionFromPlugin(pluginWs) {
237
248
 
238
249
  return new Promise((resolve) => {
239
250
  const requestId = `version_${Date.now()}`;
240
- const timeout = setTimeout(() => resolve(null), 2000);
241
-
242
251
  const handler = (data) => {
243
252
  try {
244
253
  const msg = JSON.parse(data.toString());
@@ -252,9 +261,13 @@ async function requestVersionFromPlugin(pluginWs) {
252
261
  }
253
262
  } catch (e) {}
254
263
  };
264
+ const timeout = setTimeout(() => {
265
+ pluginWs.off('message', handler);
266
+ resolve(null);
267
+ }, 2000);
255
268
 
256
269
  pluginWs.on('message', handler);
257
- pluginWs.send(JSON.stringify({ id: requestId, method: 'Browser.getVersion' }));
270
+ safeSend(pluginWs, JSON.stringify({ id: requestId, method: 'Browser.getVersion' }), 'plugin');
258
271
  });
259
272
  }
260
273
 
@@ -276,10 +289,6 @@ async function requestTargetsFromPlugin(pluginWs) {
276
289
 
277
290
  return new Promise((resolve) => {
278
291
  const requestId = `targets_${Date.now()}`;
279
- const timeout = setTimeout(() => {
280
- resolve(ns.cachedTargets);
281
- }, CONFIG.TARGETS_REQUEST_TIMEOUT);
282
-
283
292
  const handler = (data) => {
284
293
  try {
285
294
  const msg = JSON.parse(data.toString());
@@ -292,9 +301,13 @@ async function requestTargetsFromPlugin(pluginWs) {
292
301
  }
293
302
  } catch (e) {}
294
303
  };
304
+ const timeout = setTimeout(() => {
305
+ pluginWs.off('message', handler);
306
+ resolve(ns.cachedTargets);
307
+ }, CONFIG.TARGETS_REQUEST_TIMEOUT);
295
308
 
296
309
  pluginWs.on('message', handler);
297
- pluginWs.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
310
+ safeSend(pluginWs, JSON.stringify({ id: requestId, method: 'Target.getTargets' }), 'plugin');
298
311
  });
299
312
  }
300
313
 
@@ -440,9 +453,7 @@ async function execCdpViaPlugin(pluginId, params) {
440
453
  }
441
454
  }
442
455
  // 发 client-disconnected 让扩展关分组 + 关 tab
443
- try {
444
- pluginWs.send(JSON.stringify({ type: 'client-disconnected', clientId }));
445
- } catch (e) {}
456
+ safeSend(pluginWs, JSON.stringify({ type: 'client-disconnected', clientId }), 'plugin');
446
457
  // 清理 key session(让下次连接重新分配)
447
458
  if (pluginWs.apiKey && portPool) {
448
459
  portPool.keySessions.delete(pluginWs.apiKey);
@@ -953,12 +964,10 @@ function cleanupPlugin(ws, id, reason) {
953
964
  }
954
965
  clientWs.pairedPlugin = null;
955
966
  affectedClients.push(clientWs.id);
956
- if (clientWs.readyState === WebSocket.OPEN) {
957
- clientWs.send(JSON.stringify({
958
- type: 'plugin-disconnected',
959
- message: 'Plugin connection lost'
960
- }));
961
- }
967
+ safeSend(clientWs, JSON.stringify({
968
+ type: 'plugin-disconnected',
969
+ message: 'Plugin connection lost'
970
+ }), 'client');
962
971
  }
963
972
  });
964
973
 
@@ -968,8 +977,7 @@ function cleanupPlugin(ws, id, reason) {
968
977
  remainingPlugins: pluginConnections.size,
969
978
  affectedClients,
970
979
  uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
971
- activeSessions: ns.sessionToClientId.size,
972
- pendingRequests: ns.pendingAttachRequests.size
980
+ activeSessions: ns.sessionToClientId.size
973
981
  });
974
982
 
975
983
  if (ws.pairedClientId) {
@@ -1198,7 +1206,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1198
1206
  if (eventClientId) {
1199
1207
  const clientWs = clientById.get(eventClientId);
1200
1208
  if (clientWs && clientWs.readyState === WebSocket.OPEN) {
1201
- clientWs.send(cdpData);
1209
+ safeSend(clientWs, cdpData, 'client');
1202
1210
  console.log(`[TARGET EVENT ROUTED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> clientId=${eventClientId}`);
1203
1211
  }
1204
1212
  } else if (targetId && (parsed.method === 'Target.targetCreated' || parsed.method === 'Target.attachedToTarget')) {
@@ -1331,7 +1339,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1331
1339
 
1332
1340
  const cachedCreated = ns.pendingTargetCreatedEvents.get(targetId);
1333
1341
  if (cachedCreated) {
1334
- clientWs.send(cachedCreated.cdpData);
1342
+ safeSend(clientWs, cachedCreated.cdpData, 'client');
1335
1343
  console.log(`[TARGET CREATED EVENT] Sent cached Target.targetCreated to client: ${mapping.clientId}`);
1336
1344
  ns.pendingTargetCreatedEvents.delete(targetId);
1337
1345
  }
@@ -1350,7 +1358,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1350
1358
  };
1351
1359
  const msgStr = JSON.stringify(cdpMsg);
1352
1360
  console.log(`[ATTACHED EVENT] Full message: ${msgStr}`);
1353
- clientWs.send(msgStr);
1361
+ safeSend(clientWs, msgStr, 'client');
1354
1362
  console.log(`[ATTACHED EVENT] Sent cached event to client: ${mapping.clientId}`);
1355
1363
  }
1356
1364
  const newTargetInfo = cachedCreated?.parsed?.params?.targetInfo
@@ -1385,23 +1393,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1385
1393
  invalidateTargetsCache(ws);
1386
1394
  }
1387
1395
 
1388
- if (mapping.isAutoDefaultPage) {
1389
- console.log(`[AUTO DEFAULT PAGE] createTarget response received for client=${mapping.clientId}, targetId=${parsed.result?.targetId?.substring(0,8) || 'none'} — skipping response send to client`);
1390
-
1391
- if (mapping.pendingSetAutoAttach) {
1392
- const pending = mapping.pendingSetAutoAttach;
1393
- const pendingParsed = pending.parsed;
1394
- const pendingClientId = pending.clientId;
1395
-
1396
- console.log(`[AUTO DEFAULT PAGE] Now forwarding pending setAutoAttach for client=${pendingClientId}`);
1397
-
1398
- if (ws.readyState === WebSocket.OPEN) {
1399
- const forwardMsg = { ...pendingParsed, __clientId: pendingClientId };
1400
- ws.send(JSON.stringify(forwardMsg));
1401
- console.log(`[SEND TO PLUGIN] Forwarding setAutoAttach for client=${pendingClientId}`);
1402
- }
1403
- }
1404
- } else {
1396
+ {
1405
1397
  const originalId = mapping.originalId;
1406
1398
  parsed.id = originalId;
1407
1399
  if (mapping.sessionId && !parsed.sessionId) {
@@ -1409,7 +1401,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1409
1401
  }
1410
1402
  const responseStr = JSON.stringify(parsed);
1411
1403
  console.log(`[SEND TO CLIENT] ${responseStr.substring(0, 300)}`);
1412
- clientWs.send(responseStr);
1404
+ safeSend(clientWs, responseStr, 'client');
1413
1405
  console.log(`[ROUTE] Response global=${globalId} -> original=${originalId} -> client=${mapping.clientId} sessionId=${parsed.sessionId?.substring(0,8) || 'none'}`);
1414
1406
  }
1415
1407
  }
@@ -1428,7 +1420,7 @@ function handlePluginConnection(ws, clientInfo, request) {
1428
1420
  if (targetClientId) {
1429
1421
  const clientWs = clientById.get(targetClientId);
1430
1422
  if (clientWs && clientWs.readyState === WebSocket.OPEN) {
1431
- clientWs.send(data);
1423
+ safeSend(clientWs, data, 'client');
1432
1424
  logCDP('DEBUG', `FORWARDED to client: ${targetClientId} (sessionId route)`, parsed?.sessionId);
1433
1425
  }
1434
1426
  } else {
@@ -1507,52 +1499,6 @@ function handlePluginConnection(ws, clientInfo, request) {
1507
1499
  }));
1508
1500
  }
1509
1501
 
1510
- function autoCreateDefaultPageAndForward(clientWs, setAutoAttachParsed, originalData, clientId, originalRequestId) {
1511
- const pluginWs = clientWs.pairedPlugin;
1512
- if (!pluginWs || pluginWs.readyState !== WebSocket.OPEN) {
1513
- forwardToPlugin(clientWs, originalData, clientId);
1514
- return;
1515
- }
1516
-
1517
- globalRequestIdCounter++;
1518
- const createGlobalId = globalRequestIdCounter;
1519
-
1520
- globalRequestIdMap.set(createGlobalId, {
1521
- clientId: clientId,
1522
- originalId: -1,
1523
- sessionId: null,
1524
- method: 'Target.createTarget',
1525
- isCreateTarget: true,
1526
- isAutoDefaultPage: true,
1527
- pendingSetAutoAttach: {
1528
- parsed: setAutoAttachParsed,
1529
- data: originalData,
1530
- clientId: clientId,
1531
- originalRequestId: originalRequestId
1532
- }
1533
- });
1534
-
1535
- const request = {
1536
- id: createGlobalId,
1537
- method: 'Target.createTarget',
1538
- params: { url: 'about:blank' },
1539
- __clientId: clientId
1540
- };
1541
-
1542
- console.log(`[AUTO DEFAULT PAGE] Sending Target.createTarget for client=${clientId} globalId=${createGlobalId}, will forward setAutoAttach after`);
1543
- pluginWs.send(JSON.stringify(request));
1544
- }
1545
-
1546
- function forwardToPlugin(clientWs, data, clientId) {
1547
- const pluginWs = clientWs.pairedPlugin;
1548
- if (pluginWs && pluginWs.readyState === WebSocket.OPEN) {
1549
- console.log(`[SEND TO PLUGIN] method=Target.setAutoAttach clientId=${clientId}`);
1550
- pluginWs.send(data);
1551
- } else {
1552
- broadcastToPlugins(data, clientWs);
1553
- }
1554
- }
1555
-
1556
1502
  /**
1557
1503
  * 处理 CDP 客户端连接 (Playwright/Puppeteer)
1558
1504
  */
@@ -1626,10 +1572,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1626
1572
 
1627
1573
  logConnectionEvent('CLIENT_PAIRED', { clientId: id, pluginId: pluginWs.id });
1628
1574
 
1629
- pluginWs.send(JSON.stringify({
1575
+ safeSend(pluginWs, JSON.stringify({
1630
1576
  type: 'client-connected',
1631
1577
  clientId: id
1632
- }));
1578
+ }), 'plugin');
1633
1579
 
1634
1580
  broadcastClientList();
1635
1581
  }
@@ -1775,13 +1721,9 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
1775
1721
 
1776
1722
  if (parsed && parsed.method === 'Target.setAutoAttach' && parsed.params?.autoAttach && !ws._autoDefaultPageSent) {
1777
1723
  ws._autoDefaultPageSent = true;
1778
- if (ws.mode === 'takeover') {
1779
- const takeoverMsg = { ...parsed, __clientId: id, __mode: 'takeover' };
1780
- if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
1781
- ws.pairedPlugin.send(JSON.stringify(takeoverMsg));
1782
- }
1783
- } else {
1784
- autoCreateDefaultPageAndForward(ws, parsed, modifiedData, id, originalId);
1724
+ const forwardMsg = { ...parsed, __clientId: id, __mode: ws.mode };
1725
+ if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
1726
+ ws.pairedPlugin.send(JSON.stringify(forwardMsg));
1785
1727
  }
1786
1728
  return;
1787
1729
  }
@@ -2154,7 +2096,7 @@ function safeSend(ws, data, label = '') {
2154
2096
  }
2155
2097
  }
2156
2098
 
2157
- setInterval(() => {
2099
+ const flushInterval = setInterval(() => {
2158
2100
  messageQueues.forEach((queue, wsId) => {
2159
2101
  let ws = null;
2160
2102
  for (const conn of pluginConnections) {
@@ -2254,7 +2196,7 @@ const heartbeatInterval = setInterval(() => {
2254
2196
  });
2255
2197
  }, CONFIG.HEARTBEAT_INTERVAL);
2256
2198
 
2257
- setInterval(() => {
2199
+ const zombieCleanupInterval = setInterval(() => {
2258
2200
  const toRemove = [];
2259
2201
  pluginConnections.forEach(ws => {
2260
2202
  if (ws.readyState !== WebSocket.OPEN) {
@@ -2287,10 +2229,10 @@ wss.on('close', () => {
2287
2229
  /**
2288
2230
  * 定期打印状态
2289
2231
  */
2290
- setInterval(() => {
2232
+ const statusPrintInterval = setInterval(() => {
2291
2233
  const now = new Date().toISOString();
2292
2234
  const nowMs = Date.now();
2293
-
2235
+
2294
2236
  const validPlugins = Array.from(pluginConnections).filter(ws => ws.readyState === WebSocket.OPEN);
2295
2237
  const zombiePlugins = Array.from(pluginConnections).filter(ws => ws.readyState !== WebSocket.OPEN);
2296
2238
  const validClients = Array.from(clientConnections).filter(ws => ws.readyState === WebSocket.OPEN);
@@ -2331,10 +2273,8 @@ setInterval(() => {
2331
2273
  });
2332
2274
 
2333
2275
  let totalSessions = 0;
2334
- let totalPendingAttach = 0;
2335
2276
  pluginNamespaces.forEach(ns => {
2336
2277
  totalSessions += ns.sessionToClientId.size;
2337
- totalPendingAttach += ns.pendingAttachRequests.size;
2338
2278
  });
2339
2279
 
2340
2280
  logStatus({
@@ -2346,8 +2286,7 @@ setInterval(() => {
2346
2286
  pairs: connectionPairs.size,
2347
2287
  pluginDetails: pluginList,
2348
2288
  clientDetails: clientList,
2349
- sessions: totalSessions,
2350
- pendingAttach: totalPendingAttach
2289
+ sessions: totalSessions
2351
2290
  });
2352
2291
  }, CONFIG.STATUS_PRINT_INTERVAL);
2353
2292
 
@@ -2355,7 +2294,10 @@ setInterval(() => {
2355
2294
  process.on('SIGINT', () => {
2356
2295
  console.log('\n[SERVER] Shutting down (SIGINT)...');
2357
2296
  logCDP('SERVER', 'Shutting down (SIGINT)');
2297
+ clearInterval(flushInterval);
2358
2298
  clearInterval(heartbeatInterval);
2299
+ clearInterval(zombieCleanupInterval);
2300
+ clearInterval(statusPrintInterval);
2359
2301
 
2360
2302
  pluginConnections.forEach(ws => ws.close(1001, 'Server shutting down'));
2361
2303
  clientConnections.forEach(ws => ws.close(1001, 'Server shutting down'));
@@ -2370,6 +2312,10 @@ process.on('SIGINT', () => {
2370
2312
  process.on('SIGTERM', () => {
2371
2313
  console.log('[SERVER] Shutting down (SIGTERM)...');
2372
2314
  logCDP('SERVER', 'Shutting down (SIGTERM)');
2315
+ clearInterval(flushInterval);
2316
+ clearInterval(heartbeatInterval);
2317
+ clearInterval(zombieCleanupInterval);
2318
+ clearInterval(statusPrintInterval);
2373
2319
  flushAllLogs();
2374
2320
  process.exit(0);
2375
2321
  });
@@ -2424,11 +2370,6 @@ portPool = new PortPoolManager({
2424
2370
  if (!apiKey) return false;
2425
2371
  return !!validateApiKey(apiKey);
2426
2372
  },
2427
- handlePluginUpgrade: (req, socket, head) => {
2428
- wss.handleUpgrade(req, socket, head, (ws) => {
2429
- wss.emit('connection', ws, req);
2430
- });
2431
- },
2432
2373
  handleTakeoverUpgrade: (req, socket, head) => {
2433
2374
  req._takeoverMode = true;
2434
2375
  const url = new URL(req.url, `http://localhost:${CONFIG.TAKEOVER_PORT}`);
@@ -2448,22 +2389,6 @@ portPool = new PortPoolManager({
2448
2389
  wss.handleUpgrade(req, socket, head, (ws) => {
2449
2390
  wss.emit('connection', ws, req);
2450
2391
  });
2451
- },
2452
- getAllTargets: async (portIndex) => {
2453
- const session = portPool.portSessions[portIndex];
2454
- if (!session) return [];
2455
- // 从主 proxy 的 namespace 拿所有 target,过滤出这个端口的
2456
- const targets = [];
2457
- for (const [, ns] of pluginNamespaces) {
2458
- if (ns.cachedTargets) {
2459
- for (const t of ns.cachedTargets) {
2460
- if (session.targetIds.has(t.targetId)) {
2461
- targets.push(t);
2462
- }
2463
- }
2464
- }
2465
- }
2466
- return targets;
2467
2392
  }
2468
2393
  });
2469
2394
  portPool.start();