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 +222 -133
- package/extension-new/background.js +6 -2
- package/extension-new/cdp/handler/forward.js +10 -1
- package/extension-new/cdp/handler/local.js +31 -6
- package/extension-new/cdp/handler/special.js +0 -10
- package/extension-new/cdp/index.js +1 -3
- package/extension-new/core/connection-state.js +0 -13
- package/extension-new/core/debugger.js +0 -5
- package/extension-new/core/state.js +0 -8
- package/extension-new/core/websocket.js +0 -15
- package/extension-new/manifest.json +1 -1
- package/extension-new/utils/config.js +1 -1
- package/package.json +5 -17
- package/server/modules/port-pool.js +37 -3
- package/server/proxy-server.js +52 -127
package/README.md
CHANGED
|
@@ -5,220 +5,309 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>Chrome
|
|
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
|
-
|
|
13
|
-
|
|
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://
|
|
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
|
-
│
|
|
34
|
-
│
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
│
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
##
|
|
64
|
+
## Quick Start
|
|
46
65
|
|
|
47
|
-
|
|
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
|
-
|
|
68
|
+
```bash
|
|
69
|
+
npm install -g cdp-tunnel
|
|
70
|
+
cdp-tunnel setup # Start server + auto-load extension
|
|
71
|
+
```
|
|
53
72
|
|
|
54
|
-
|
|
73
|
+
Or run from source:
|
|
55
74
|
|
|
56
|
-
|
|
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
|
-
###
|
|
82
|
+
### 2. Load Extension
|
|
59
83
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
72
|
-
#
|
|
73
|
-
|
|
100
|
+
```python
|
|
101
|
+
# Python Playwright
|
|
102
|
+
from playwright.sync_api import sync_playwright
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
111
|
+
## Key Features
|
|
112
|
+
|
|
113
|
+
### Port Pool — Multiple Isolated Environments
|
|
83
114
|
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
131
|
+
### Takeover Mode — Control Existing Tabs
|
|
93
132
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
142
|
+
Enable authentication for cloud/remote deployment:
|
|
100
143
|
|
|
101
|
-
|
|
144
|
+
```bash
|
|
145
|
+
REQUIRE_AUTH=true node server/proxy-server.js
|
|
102
146
|
|
|
103
|
-
|
|
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
|
-
//
|
|
107
|
-
const
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
const puppeteer = require('puppeteer');
|
|
163
|
+
Built-in web UI at `/admin`:
|
|
116
164
|
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
### Version Check
|
|
127
177
|
|
|
128
178
|
```bash
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
136
|
-
agent-browser --cdp http://localhost:9221 snapshot -i
|
|
183
|
+
## Remote / Cloud Deployment
|
|
137
184
|
|
|
138
|
-
|
|
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
|
-
|
|
143
|
-
|
|
187
|
+
```
|
|
188
|
+
Remote Client ──wss──▶ Cloud Proxy ──wss──▶ User's Chrome + Extension
|
|
144
189
|
```
|
|
145
190
|
|
|
146
|
-
|
|
191
|
+
See [`docs/DEPLOY-CLOUDFLARE.md`](docs/DEPLOY-CLOUDFLARE.md) for Cloudflare Tunnel setup.
|
|
147
192
|
|
|
148
|
-
|
|
193
|
+
Key environment variables:
|
|
149
194
|
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
##
|
|
216
|
+
## API Key Management
|
|
165
217
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
222
|
+
# List all keys
|
|
223
|
+
node server/saas/key-manager.js list
|
|
173
224
|
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
#
|
|
198
|
-
node tests/
|
|
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
|
-
#
|
|
201
|
-
node tests/
|
|
238
|
+
# Key isolation test
|
|
239
|
+
node tests/e2e/test-key-isolation.js
|
|
202
240
|
|
|
203
|
-
#
|
|
204
|
-
node tests/
|
|
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
|
-
##
|
|
248
|
+
## How It Works
|
|
208
249
|
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
306
|
+
Apache License 2.0 with Attribution Requirement. See [LICENSE](LICENSE).
|
|
218
307
|
|
|
219
308
|
---
|
|
220
309
|
|
|
221
|
-
If you use this project
|
|
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
|
-
//
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
package/package.json
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-tunnel",
|
|
3
|
-
"version": "3.6.
|
|
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":
|
|
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
|
-
"
|
|
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
|
-
|
|
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) {
|
package/server/proxy-server.js
CHANGED
|
@@ -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) =>
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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();
|