cdp-tunnel 3.6.0 → 3.6.1
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/utils/config.js +1 -1
- package/package.json +1 -1
- package/server/modules/port-pool.js +0 -2
- package/server/proxy-server.js +6 -98
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
package/server/proxy-server.js
CHANGED
|
@@ -166,7 +166,6 @@ const clientConnections = new Set();
|
|
|
166
166
|
class PluginNamespace {
|
|
167
167
|
constructor() {
|
|
168
168
|
this.sessionToClientId = new Map();
|
|
169
|
-
this.pendingAttachRequests = new Map();
|
|
170
169
|
this.pendingAttachedEvents = new Map();
|
|
171
170
|
this.pendingTargetCreatedEvents = new Map();
|
|
172
171
|
this.targetIdToClientId = new Map();
|
|
@@ -968,8 +967,7 @@ function cleanupPlugin(ws, id, reason) {
|
|
|
968
967
|
remainingPlugins: pluginConnections.size,
|
|
969
968
|
affectedClients,
|
|
970
969
|
uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
|
|
971
|
-
activeSessions: ns.sessionToClientId.size
|
|
972
|
-
pendingRequests: ns.pendingAttachRequests.size
|
|
970
|
+
activeSessions: ns.sessionToClientId.size
|
|
973
971
|
});
|
|
974
972
|
|
|
975
973
|
if (ws.pairedClientId) {
|
|
@@ -1385,23 +1383,7 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
1385
1383
|
invalidateTargetsCache(ws);
|
|
1386
1384
|
}
|
|
1387
1385
|
|
|
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 {
|
|
1386
|
+
{
|
|
1405
1387
|
const originalId = mapping.originalId;
|
|
1406
1388
|
parsed.id = originalId;
|
|
1407
1389
|
if (mapping.sessionId && !parsed.sessionId) {
|
|
@@ -1507,52 +1489,6 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
1507
1489
|
}));
|
|
1508
1490
|
}
|
|
1509
1491
|
|
|
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
1492
|
/**
|
|
1557
1493
|
* 处理 CDP 客户端连接 (Playwright/Puppeteer)
|
|
1558
1494
|
*/
|
|
@@ -1775,13 +1711,9 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
|
|
|
1775
1711
|
|
|
1776
1712
|
if (parsed && parsed.method === 'Target.setAutoAttach' && parsed.params?.autoAttach && !ws._autoDefaultPageSent) {
|
|
1777
1713
|
ws._autoDefaultPageSent = true;
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
ws.pairedPlugin.send(JSON.stringify(takeoverMsg));
|
|
1782
|
-
}
|
|
1783
|
-
} else {
|
|
1784
|
-
autoCreateDefaultPageAndForward(ws, parsed, modifiedData, id, originalId);
|
|
1714
|
+
const forwardMsg = { ...parsed, __clientId: id, __mode: ws.mode };
|
|
1715
|
+
if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
|
|
1716
|
+
ws.pairedPlugin.send(JSON.stringify(forwardMsg));
|
|
1785
1717
|
}
|
|
1786
1718
|
return;
|
|
1787
1719
|
}
|
|
@@ -2331,10 +2263,8 @@ setInterval(() => {
|
|
|
2331
2263
|
});
|
|
2332
2264
|
|
|
2333
2265
|
let totalSessions = 0;
|
|
2334
|
-
let totalPendingAttach = 0;
|
|
2335
2266
|
pluginNamespaces.forEach(ns => {
|
|
2336
2267
|
totalSessions += ns.sessionToClientId.size;
|
|
2337
|
-
totalPendingAttach += ns.pendingAttachRequests.size;
|
|
2338
2268
|
});
|
|
2339
2269
|
|
|
2340
2270
|
logStatus({
|
|
@@ -2346,8 +2276,7 @@ setInterval(() => {
|
|
|
2346
2276
|
pairs: connectionPairs.size,
|
|
2347
2277
|
pluginDetails: pluginList,
|
|
2348
2278
|
clientDetails: clientList,
|
|
2349
|
-
sessions: totalSessions
|
|
2350
|
-
pendingAttach: totalPendingAttach
|
|
2279
|
+
sessions: totalSessions
|
|
2351
2280
|
});
|
|
2352
2281
|
}, CONFIG.STATUS_PRINT_INTERVAL);
|
|
2353
2282
|
|
|
@@ -2424,11 +2353,6 @@ portPool = new PortPoolManager({
|
|
|
2424
2353
|
if (!apiKey) return false;
|
|
2425
2354
|
return !!validateApiKey(apiKey);
|
|
2426
2355
|
},
|
|
2427
|
-
handlePluginUpgrade: (req, socket, head) => {
|
|
2428
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2429
|
-
wss.emit('connection', ws, req);
|
|
2430
|
-
});
|
|
2431
|
-
},
|
|
2432
2356
|
handleTakeoverUpgrade: (req, socket, head) => {
|
|
2433
2357
|
req._takeoverMode = true;
|
|
2434
2358
|
const url = new URL(req.url, `http://localhost:${CONFIG.TAKEOVER_PORT}`);
|
|
@@ -2448,22 +2372,6 @@ portPool = new PortPoolManager({
|
|
|
2448
2372
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2449
2373
|
wss.emit('connection', ws, req);
|
|
2450
2374
|
});
|
|
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
2375
|
}
|
|
2468
2376
|
});
|
|
2469
2377
|
portPool.start();
|