ghost-bridge 0.5.2 โ 0.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 +129 -153
- package/dist/server.js +23 -20
- package/extension/background.js +397 -120
- package/extension/manifest.json +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,183 +1,159 @@
|
|
|
1
1
|
# ๐ป Ghost Bridge
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/ghost-bridge)
|
|
4
|
+
[](https://www.npmjs.com/package/ghost-bridge)
|
|
4
5
|
[](https://opensource.org/licenses/MIT)
|
|
5
6
|
|
|
6
|
-
>
|
|
7
|
+
> Zero-restart Chrome bridge for MCP clients. Let AI inspect, debug, and operate the browser session you are already using.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
## Why
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Most browser-capable AI tools start a separate browser. Ghost Bridge connects AI to your existing Chrome session instead, so it can work with the page state you already have: logged-in accounts, reproduced bugs, in-progress flows, network failures, and real UI state.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- ๐ **No-Sourcemap Debugging** โ Slice code fragments, perform string searches, and analyze coverage to pinpoint bugs straight in minified production code.
|
|
14
|
-
- ๐ **Deep Network Analysis** โ Comprehensive capture of requests/responses with multi-dimensional filtering and response body inspection.
|
|
15
|
-
- ๐ธ **Visual & Structural Perception** โ Full-page or clipped high-fidelity screenshots paired with structural data extraction (titles, links, forms, buttons).
|
|
16
|
-
- ๐ฏ **DOM Physical Manipulation** โ Empowers AI to click, type, and form-submit with CDP-level physical simulation. Fully compatible with complex SPAs (React/Vue/Angular/Svelte).
|
|
17
|
-
- ๐ **Performance Diagnostics** โ Get granular engine metrics: JS Heap, Layout recalculations, Web Vitals (TTFB/FCP/LCP), and resource loading speeds.
|
|
18
|
-
- ๐ **Multi-Client Mastery** โ Built-in singleton manager automatically coordinates multiple MCP clients sharing a single Chrome transport.
|
|
13
|
+
## What It Does
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
- Attach to Chrome without `--remote-debugging-port`
|
|
16
|
+
- Inspect page structure, text, screenshots, errors, and network traffic
|
|
17
|
+
- Search and extract script sources, even in production bundles
|
|
18
|
+
- Click, type, scroll, and submit forms on the current page
|
|
19
|
+
- Share one Chrome transport across multiple MCP clients
|
|
21
20
|
|
|
22
|
-
##
|
|
21
|
+
## What's New in 0.6.1
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
- `list_network_requests` and `get_network_detail` now summarize `data:` URLs and oversized URLs so inline images and long query strings do not overwhelm model context
|
|
24
|
+
|
|
25
|
+
## What's New in 0.6.0
|
|
26
|
+
|
|
27
|
+
- `inspect_page` now collects structured page data and interactive elements in one browser-side snapshot, reducing duplicate DOM scans and cutting one round-trip from the hot path
|
|
28
|
+
- `capture_screenshot` now defaults to JPEG for better transfer efficiency: visible viewport screenshots default to `quality: 80`, and `fullPage` screenshots default to `quality: 70`
|
|
29
|
+
- Use `format: "png"` when you need high-fidelity text rendering, 1px lines, icon edges, transparency, or pixel-level UI inspection
|
|
30
|
+
- Attachment and request cleanup paths are more robust under concurrent usage and multi-client reconnect scenarios
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### 1. Install
|
|
25
35
|
|
|
26
36
|
```bash
|
|
27
|
-
# Install globally
|
|
28
37
|
npm install -g ghost-bridge
|
|
29
|
-
|
|
30
|
-
# Auto-configure MCP (Claude Code, Codex, Cursor, Antigravity) and prepare the extension directory
|
|
31
38
|
ghost-bridge init
|
|
32
39
|
```
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
> - Claude Code: `~/.claude/settings.json` or legacy `~/.claude.json`
|
|
36
|
-
> - Codex: `~/.codex/config.toml`
|
|
37
|
-
> - Cursor: `~/.cursor/mcp.json`
|
|
38
|
-
> - Antigravity: `~/.gemini/antigravity/mcp.json`
|
|
39
|
-
>
|
|
40
|
-
> **Note on other MCP clients (Windsurf, Roo, others):**
|
|
41
|
-
> `ghost-bridge init` attempts to auto-configure supported clients. If your client isn't auto-detected, add one of the following snippets to the appropriate MCP configuration file:
|
|
42
|
-
>
|
|
43
|
-
> JSON-style MCP configs (`mcp.json`, Claude JSON config, Cursor):
|
|
44
|
-
> ```json
|
|
45
|
-
> {
|
|
46
|
-
> "mcpServers": {
|
|
47
|
-
> "ghost-bridge": {
|
|
48
|
-
> "command": "/absolute/path/to/node",
|
|
49
|
-
> "args": ["/absolute/path/to/global/node_modules/ghost-bridge/dist/server.js"]
|
|
50
|
-
> }
|
|
51
|
-
> }
|
|
52
|
-
> }
|
|
53
|
-
> ```
|
|
54
|
-
>
|
|
55
|
-
> Codex TOML config (`~/.codex/config.toml`):
|
|
56
|
-
> ```toml
|
|
57
|
-
> [mcp_servers.ghost-bridge]
|
|
58
|
-
> type = "stdio"
|
|
59
|
-
> command = "/absolute/path/to/node"
|
|
60
|
-
> args = ["/absolute/path/to/global/node_modules/ghost-bridge/dist/server.js"]
|
|
61
|
-
> ```
|
|
62
|
-
|
|
63
|
-
### 2. Load the Chrome Extension
|
|
64
|
-
|
|
65
|
-
1. Open Chrome and navigate to `chrome://extensions`
|
|
66
|
-
2. Toggle **Developer mode** in the top right corner.
|
|
67
|
-
3. Click **Load unpacked**
|
|
68
|
-
4. Select the directory: `~/.ghost-bridge/extension`
|
|
69
|
-
|
|
70
|
-
> ๐ก *Tip: Run `ghost-bridge extension --open` to reveal the directory directly.*
|
|
71
|
-
|
|
72
|
-
### 3. Connect & Command
|
|
73
|
-
|
|
74
|
-
1. Click the **Ghost Bridge** ghost icon in your browser toolbar.
|
|
75
|
-
2. Click **Connect** and wait for the status to turn to โ
**ON**.
|
|
76
|
-
3. Open Claude Desktop or your Claude CLI. All tools are now primed and ready!
|
|
77
|
-
|
|
78
|
-
**Prompting tips:**
|
|
79
|
-
- You usually do not need to say "please use ghost-bridge". Direct requests like `ๅๆๅฝๅ้กต้ข`ใ`็็่ฟไธช็ฝ็ซ็ DOM ็ปๆ`ใ`ๅธฎๆๆฃๆฅ่ฟไธช้กต้ขไธบไปไนๅธๅฑ้ไบ`ใ`ๅธฎๆ็นๅป็ปๅฝๆ้ฎๅนถๆไบค่กจๅ` should be enough.
|
|
80
|
-
- `inspect_page` is the default entry tool for generic page analysis.
|
|
81
|
-
- For visual/UI issues, the model should prefer `capture_screenshot`.
|
|
82
|
-
- For text/DOM extraction, the model should prefer `get_page_content`.
|
|
83
|
-
- For page actions, the model should start with `get_interactive_snapshot` and then call `dispatch_action`.
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## ๐ ๏ธ Tool Arsenal
|
|
88
|
-
|
|
89
|
-
### ๐ Core Debugging
|
|
90
|
-
|
|
91
|
-
| Tool | Capability |
|
|
92
|
-
|------|------------|
|
|
93
|
-
| `inspect_page` | Best default entry for page analysis. Returns metadata, structured content, and an interactive-elements overview. |
|
|
94
|
-
| `get_server_info` | Retrieves server instance status, WebSocket ports, and client roles. |
|
|
95
|
-
| `get_last_error` | Aggregates recent exceptions, console errors, and failed network requests with mapped locators. |
|
|
96
|
-
| `get_script_source` | Pulls raw script definitions. Supports URL-fragment filtering, specific line targeting, and beautification. |
|
|
97
|
-
| `coverage_snapshot` | Triggers a quick coverage trace (1.5s default) to identify the most active scripts on the page. |
|
|
98
|
-
| `find_by_string` | Scans page script sources for keywords, returning a 200-character context window. |
|
|
99
|
-
| `symbolic_hints` | Gathers context clues: Resource lists, Global Variable keys, LocalStorage schema, and UA strings. |
|
|
100
|
-
| `eval_script` | Executes raw JavaScript expressions in the page context. *(Use with caution)* |
|
|
101
|
-
|
|
102
|
-
### ๐ Network Intelligence
|
|
103
|
-
|
|
104
|
-
| Tool | Capability |
|
|
105
|
-
|------|------------|
|
|
106
|
-
| `list_network_requests` | Lists captured network traffic. Supports filtering by URL, Method, Status Code, or Resource Type. |
|
|
107
|
-
| `get_network_detail` | Dives deep into a specific request's Headers, Timing, and optional Response Body extraction. |
|
|
108
|
-
| `clear_network_requests` | Wipes the current network capture buffer. |
|
|
109
|
-
|
|
110
|
-
### ๐ธ Page Perception
|
|
111
|
-
|
|
112
|
-
| Tool | Capability |
|
|
113
|
-
|------|------------|
|
|
114
|
-
| `capture_screenshot` | Captures the viewport or emulates full-page scrolling screenshots. |
|
|
115
|
-
| `get_page_content` | Extracts raw text, sanitized HTML, or structured actionable data representations. |
|
|
116
|
-
|
|
117
|
-
### ๐ฏ Interactive Automation (DOM)
|
|
118
|
-
|
|
119
|
-
| Tool | Capability |
|
|
120
|
-
|------|------------|
|
|
121
|
-
| `get_interactive_snapshot` | Scans for visible interactive elements, returning a concise map `[e1, e2...]`. Pierces open Shadow DOMs. |
|
|
122
|
-
| `dispatch_action` | Dispatches physical UI actions (click, fill, press, hover) against targeted element references (e.g., `e1`). |
|
|
123
|
-
|
|
124
|
-
**Example Agent Workflow:**
|
|
125
|
-
1. AI: `get_interactive_snapshot` โ `[{ref:"e1", tag:"input", placeholder:"Search..."}, {ref:"e2", tag:"button", text:"Login"}]`
|
|
126
|
-
2. AI: `dispatch_action({ref: "e1", action: "fill", value: "hello"})`
|
|
127
|
-
3. AI: `dispatch_action({ref: "e2", action: "click"})`
|
|
128
|
-
4. AI: `capture_screenshot` to verify state changes.
|
|
129
|
-
|
|
130
|
-
### ๐ Performance Profiling
|
|
131
|
-
|
|
132
|
-
| Tool | Capability |
|
|
133
|
-
|------|------------|
|
|
134
|
-
| `perf_metrics` | Collects layered performance data (Engine Metrics, Web Vitals, and Resource Load Summaries). |
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## โ๏ธ Configuration
|
|
139
|
-
|
|
140
|
-
| Setting | Default | Description |
|
|
141
|
-
|---------|---------|-------------|
|
|
142
|
-
| **Base Port** | `33333` | Fixed WS port. If occupied, Ghost Bridge fails fast and asks you to free it or set `GHOST_BRIDGE_PORT`. |
|
|
143
|
-
| **Token** | *Monthly UUID* | Local WS auth token, auto-rotates on the 1st of every month. |
|
|
144
|
-
| **Auto Detach** | `false` | Keeps debugger attached to actively buffer invisible exceptions and network calls. |
|
|
145
|
-
|
|
146
|
-
**Environment Variables:**
|
|
147
|
-
- `GHOST_BRIDGE_PORT` โ Override base port.
|
|
148
|
-
- `GHOST_BRIDGE_TOKEN` โ Override connection token.
|
|
41
|
+
`ghost-bridge init` currently writes config for:
|
|
149
42
|
|
|
150
|
-
|
|
43
|
+
- Claude Code: `~/.claude/settings.json` or `~/.claude.json`
|
|
44
|
+
- Codex: `~/.codex/config.toml`
|
|
45
|
+
- Cursor: `~/.cursor/mcp.json`
|
|
46
|
+
- Antigravity: `~/.gemini/antigravity/mcp.json`
|
|
151
47
|
|
|
152
|
-
|
|
48
|
+
If your MCP client is not auto-detected, add one of these manually.
|
|
153
49
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
50
|
+
JSON config:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"ghost-bridge": {
|
|
56
|
+
"command": "/absolute/path/to/node",
|
|
57
|
+
"args": ["/absolute/path/to/global/node_modules/ghost-bridge/dist/server.js"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
159
61
|
```
|
|
160
62
|
|
|
161
|
-
|
|
162
|
-
- **Chrome Extension (MV3)**: Taps into `chrome.debugger` API. Utilizes an Offscreen Document to prevent WS hibernation.
|
|
163
|
-
- **Singleton Design**: If multiple agents spawn servers, the first becomes the master bridge while subsequent instances chain transparently as clients.
|
|
63
|
+
Codex TOML:
|
|
164
64
|
|
|
165
|
-
|
|
65
|
+
```toml
|
|
66
|
+
[mcp_servers.ghost-bridge]
|
|
67
|
+
type = "stdio"
|
|
68
|
+
command = "/absolute/path/to/node"
|
|
69
|
+
args = ["/absolute/path/to/global/node_modules/ghost-bridge/dist/server.js"]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Load the Extension
|
|
73
|
+
|
|
74
|
+
1. Open `chrome://extensions`
|
|
75
|
+
2. Enable Developer mode
|
|
76
|
+
3. Click `Load unpacked`
|
|
77
|
+
4. Select `~/.ghost-bridge/extension`
|
|
78
|
+
|
|
79
|
+
You can also run:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
ghost-bridge extension --open
|
|
83
|
+
```
|
|
166
84
|
|
|
167
|
-
|
|
85
|
+
### 3. Connect
|
|
168
86
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
87
|
+
1. Click the Ghost Bridge extension icon
|
|
88
|
+
2. Click `Connect`
|
|
89
|
+
3. Wait until the status becomes `ON`
|
|
90
|
+
4. Open your MCP client and start working on the current page
|
|
173
91
|
|
|
174
|
-
|
|
92
|
+
Typical prompts:
|
|
93
|
+
|
|
94
|
+
- `Analyze the current page`
|
|
95
|
+
- `Check why this layout is broken`
|
|
96
|
+
- `Inspect the DOM structure`
|
|
97
|
+
- `Click the login button and submit the form`
|
|
98
|
+
|
|
99
|
+
## Tools
|
|
100
|
+
|
|
101
|
+
| Tool | Purpose |
|
|
102
|
+
|------|---------|
|
|
103
|
+
| `inspect_page` | Default entry point for page analysis |
|
|
104
|
+
| `capture_screenshot` | Visual inspection and UI debugging |
|
|
105
|
+
| `get_page_content` | Text, HTML, and structured DOM extraction |
|
|
106
|
+
| `get_interactive_snapshot` | Find clickable and editable elements |
|
|
107
|
+
| `dispatch_action` | Click, fill, press, scroll, hover, select |
|
|
108
|
+
| `list_network_requests` | Inspect captured network traffic |
|
|
109
|
+
| `get_network_detail` | Read one request in detail |
|
|
110
|
+
| `get_last_error` | Inspect recent errors and exceptions |
|
|
111
|
+
| `get_script_source` | Extract page scripts |
|
|
112
|
+
| `find_by_string` | Search within bundled script content |
|
|
113
|
+
| `coverage_snapshot` | Identify active scripts quickly |
|
|
114
|
+
| `perf_metrics` | Collect Web Vitals and engine metrics |
|
|
115
|
+
|
|
116
|
+
Recommended flow:
|
|
117
|
+
|
|
118
|
+
1. Start with `inspect_page`
|
|
119
|
+
2. Use `capture_screenshot` for visual issues
|
|
120
|
+
Default is optimized for transfer with JPEG; switch to `png` for pixel-level checks
|
|
121
|
+
3. Use `get_page_content` for DOM or text extraction
|
|
122
|
+
4. Use `get_interactive_snapshot` before `dispatch_action`
|
|
123
|
+
|
|
124
|
+
Notes:
|
|
125
|
+
|
|
126
|
+
- `list_network_requests` and `get_network_detail` automatically summarize `data:` URLs and very long URLs so inline images or oversized query strings do not overwhelm model context
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
| Setting | Default | Notes |
|
|
131
|
+
|---------|---------|-------|
|
|
132
|
+
| Port | `33333` | Set `GHOST_BRIDGE_PORT` to override |
|
|
133
|
+
| Token | Monthly UUID | Set `GHOST_BRIDGE_TOKEN` to override |
|
|
134
|
+
| Auto detach | `false` | Keeps debugger attached for ongoing capture |
|
|
135
|
+
|
|
136
|
+
## Architecture
|
|
137
|
+
|
|
138
|
+
```mermaid
|
|
139
|
+
flowchart LR
|
|
140
|
+
A["AI Client<br/>Claude / Codex / Cursor"]
|
|
141
|
+
B["Ghost Bridge MCP Server<br/>server.js"]
|
|
142
|
+
C["Chrome Extension<br/>background.js"]
|
|
143
|
+
D["Browser Tab<br/>Target Context"]
|
|
144
|
+
|
|
145
|
+
A <-->|"stdio"| B
|
|
146
|
+
B <-->|"WebSocket"| C
|
|
147
|
+
C <-->|"CDP"| D
|
|
148
|
+
```
|
|
175
149
|
|
|
176
|
-
##
|
|
150
|
+
## Limitations
|
|
177
151
|
|
|
178
|
-
|
|
179
|
-
|
|
152
|
+
- Chrome DevTools on the target tab can conflict with `chrome.debugger.attach`
|
|
153
|
+
- MV3 background lifecycle can still cause reconnect scenarios after long idle periods
|
|
154
|
+
- Very large minified bundles may be truncated during beautify or extraction
|
|
155
|
+
- Deep cross-origin iframe cases are not fully covered yet
|
|
180
156
|
|
|
181
|
-
##
|
|
157
|
+
## License
|
|
182
158
|
|
|
183
|
-
|
|
159
|
+
[MIT](LICENSE)
|
package/dist/server.js
CHANGED
|
@@ -28813,9 +28813,18 @@ function connectToMainInstance() {
|
|
|
28813
28813
|
});
|
|
28814
28814
|
}
|
|
28815
28815
|
function failAllPending(message) {
|
|
28816
|
-
pendingRequests.forEach((
|
|
28817
|
-
|
|
28818
|
-
|
|
28816
|
+
pendingRequests.forEach((pending, id) => {
|
|
28817
|
+
if (pending.reject) {
|
|
28818
|
+
clearTimeout(pending.timer);
|
|
28819
|
+
pending.reject(new Error(message));
|
|
28820
|
+
} else if (pending.source) {
|
|
28821
|
+
try {
|
|
28822
|
+
if (pending.source.readyState === import_websocket.default.OPEN) {
|
|
28823
|
+
pending.source.send(JSON.stringify({ id, error: message }));
|
|
28824
|
+
}
|
|
28825
|
+
} catch {
|
|
28826
|
+
}
|
|
28827
|
+
}
|
|
28819
28828
|
});
|
|
28820
28829
|
pendingRequests.clear();
|
|
28821
28830
|
}
|
|
@@ -29015,7 +29024,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
29015
29024
|
},
|
|
29016
29025
|
{
|
|
29017
29026
|
name: "list_network_requests",
|
|
29018
|
-
description: "\u5217\u51FA\u6355\u83B7\u7684\u7F51\u7EDC\u8BF7\u6C42\uFF0C\u652F\u6301\u6309 URL\u3001\u65B9\u6CD5\u3001\u72B6\u6001\u3001\u7C7B\u578B\u8FC7\u6EE4\u3002\u9ED8\u8BA4\u6309\u6392\u969C\u4F18\u5148\u7EA7\u6392\u5E8F\uFF1A\u5931\u8D25\u8BF7\u6C42\u3001\u8FDB\u884C\u4E2D\u8BF7\u6C42\u3001XHR/Fetch \u4F1A\u4F18\u5148\u5C55\u793A\u3002",
|
|
29027
|
+
description: "\u5217\u51FA\u6355\u83B7\u7684\u7F51\u7EDC\u8BF7\u6C42\uFF0C\u652F\u6301\u6309 URL\u3001\u65B9\u6CD5\u3001\u72B6\u6001\u3001\u7C7B\u578B\u8FC7\u6EE4\u3002\u9ED8\u8BA4\u6309\u6392\u969C\u4F18\u5148\u7EA7\u6392\u5E8F\uFF1A\u5931\u8D25\u8BF7\u6C42\u3001\u8FDB\u884C\u4E2D\u8BF7\u6C42\u3001XHR/Fetch \u4F1A\u4F18\u5148\u5C55\u793A\u3002\u4E3A\u907F\u514D\u4E0A\u4E0B\u6587\u81A8\u80C0\uFF0Cdata URL \u548C\u8D85\u957F URL \u4F1A\u81EA\u52A8\u6458\u8981\u5316\u3002",
|
|
29019
29028
|
inputSchema: {
|
|
29020
29029
|
type: "object",
|
|
29021
29030
|
properties: {
|
|
@@ -29034,7 +29043,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
29034
29043
|
},
|
|
29035
29044
|
{
|
|
29036
29045
|
name: "get_network_detail",
|
|
29037
|
-
description: "\u83B7\u53D6\u5355\u4E2A\u7F51\u7EDC\u8BF7\u6C42\u7684\u8BE6\u7EC6\u4FE1\u606F\uFF0C\u5305\u62EC\u8BF7\u6C42\u5934\u3001\u54CD\u5E94\u5934\uFF0C\u53EF\u9009\u83B7\u53D6\u54CD\u5E94\u4F53",
|
|
29046
|
+
description: "\u83B7\u53D6\u5355\u4E2A\u7F51\u7EDC\u8BF7\u6C42\u7684\u8BE6\u7EC6\u4FE1\u606F\uFF0C\u5305\u62EC\u8BF7\u6C42\u5934\u3001\u54CD\u5E94\u5934\uFF0C\u53EF\u9009\u83B7\u53D6\u54CD\u5E94\u4F53\u3002\u4E3A\u907F\u514D\u4E0A\u4E0B\u6587\u81A8\u80C0\uFF0Cdata URL \u548C\u8D85\u957F URL \u4F1A\u81EA\u52A8\u6458\u8981\u5316\u3002",
|
|
29038
29047
|
inputSchema: {
|
|
29039
29048
|
type: "object",
|
|
29040
29049
|
properties: {
|
|
@@ -29068,18 +29077,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
29068
29077
|
},
|
|
29069
29078
|
{
|
|
29070
29079
|
name: "capture_screenshot",
|
|
29071
|
-
description: "\u3010\u63A8\u8350\u7528\u4E8E\u89C6\u89C9\u5206\u6790\u3011\u622A\u53D6\u5F53\u524D\u9875\u9762\u7684\u622A\u56FE\uFF0C\u8FD4\u56DE base64 \u56FE\u7247\u3002\u9002\u7528\u4E8E\uFF1A1) \u67E5\u770B\u9875\u9762\u5B9E\u9645\u89C6\u89C9\u6548\u679C 2) \u6392\u67E5 UI/\u6837\u5F0F/\u5E03\u5C40/\u989C\u8272\u95EE\u9898 3) \u9A8C\u8BC1\u9875\u9762\u6E32\u67D3 4) \u5206\u6790\u5143\u7D20\u4F4D\u7F6E\u548C\u95F4\u8DDD 5) \u67E5\u770B\u56FE\u7247/\u56FE\u6807\u7B49\u89C6\u89C9\u5185\u5BB9\u3002\u5F53\u7528\u6237\u8BF4\u201C\u770B\u770B\u8FD9\u4E2A\u9875\u9762\u957F\u4EC0\u4E48\u6837\u201D\u201C\u5E2E\u6211\u5206\u6790\u754C\u9762/\u5E03\u5C40/\u6837\u5F0F\u201D\u65F6\uFF0C\u5E94\u4F18\u5148\u4F7F\u7528\u6B64\u5DE5\u5177\uFF0C\u65E0\u9700\u7528\u6237\u663E\u5F0F\u63D0\u5230 ghost-bridge\u3002\u5F53\u9700\u8981\u770B\u5230\u9875\u9762\u300C\u957F\u4EC0\u4E48\u6837\u300D\u65F6\u4F7F\u7528\u6B64\u5DE5\u5177\u3002\u5982\u4EC5\u9700\u6587\u672C/\u94FE\u63A5\u7B49\u4FE1\u606F\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u66F4\u5FEB\u7684 get_page_content\u3002",
|
|
29080
|
+
description: "\u3010\u63A8\u8350\u7528\u4E8E\u89C6\u89C9\u5206\u6790\u3011\u622A\u53D6\u5F53\u524D\u9875\u9762\u7684\u622A\u56FE\uFF0C\u8FD4\u56DE base64 \u56FE\u7247\u3002\u9002\u7528\u4E8E\uFF1A1) \u67E5\u770B\u9875\u9762\u5B9E\u9645\u89C6\u89C9\u6548\u679C 2) \u6392\u67E5 UI/\u6837\u5F0F/\u5E03\u5C40/\u989C\u8272\u95EE\u9898 3) \u9A8C\u8BC1\u9875\u9762\u6E32\u67D3 4) \u5206\u6790\u5143\u7D20\u4F4D\u7F6E\u548C\u95F4\u8DDD 5) \u67E5\u770B\u56FE\u7247/\u56FE\u6807\u7B49\u89C6\u89C9\u5185\u5BB9\u3002\u5F53\u7528\u6237\u8BF4\u201C\u770B\u770B\u8FD9\u4E2A\u9875\u9762\u957F\u4EC0\u4E48\u6837\u201D\u201C\u5E2E\u6211\u5206\u6790\u754C\u9762/\u5E03\u5C40/\u6837\u5F0F\u201D\u65F6\uFF0C\u5E94\u4F18\u5148\u4F7F\u7528\u6B64\u5DE5\u5177\uFF0C\u65E0\u9700\u7528\u6237\u663E\u5F0F\u63D0\u5230 ghost-bridge\u3002\u5F53\u9700\u8981\u770B\u5230\u9875\u9762\u300C\u957F\u4EC0\u4E48\u6837\u300D\u65F6\u4F7F\u7528\u6B64\u5DE5\u5177\u3002\u9ED8\u8BA4\u4F18\u5148\u4F7F\u7528\u66F4\u7701\u4F20\u8F93\u7684 JPEG\uFF1A\u666E\u901A\u622A\u56FE\u9ED8\u8BA4 quality 80\uFF0C\u5B8C\u6574\u957F\u622A\u56FE\u9ED8\u8BA4 quality 70\u3002\u5F53\u9700\u8981\u68C0\u67E5\u6587\u5B57\u6E05\u6670\u5EA6\u30011px \u7EC6\u7EBF\u3001\u56FE\u6807\u8FB9\u7F18\u3001\u900F\u660E\u80CC\u666F\u6216\u50CF\u7D20\u7EA7\u7EC6\u8282\u65F6\uFF0C\u5E94\u4F18\u5148\u4F7F\u7528 PNG\u3002\u5982\u4EC5\u9700\u6587\u672C/\u94FE\u63A5\u7B49\u4FE1\u606F\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u66F4\u5FEB\u7684 get_page_content\u3002",
|
|
29072
29081
|
inputSchema: {
|
|
29073
29082
|
type: "object",
|
|
29074
29083
|
properties: {
|
|
29075
29084
|
format: {
|
|
29076
29085
|
type: "string",
|
|
29077
29086
|
enum: ["png", "jpeg"],
|
|
29078
|
-
description: "\u56FE\u7247\u683C\u5F0F\
|
|
29087
|
+
description: "\u56FE\u7247\u683C\u5F0F\u3002\u9ED8\u8BA4\u4F7F\u7528 jpeg\uFF1B\u9700\u8981\u9AD8\u4FDD\u771F\u6587\u5B57\u3001\u7EC6\u7EBF\u3001\u900F\u660E\u80CC\u666F\u65F6\u7528 png"
|
|
29079
29088
|
},
|
|
29080
29089
|
quality: {
|
|
29081
29090
|
type: "number",
|
|
29082
|
-
description: "JPEG \u8D28\u91CF (0-100)\uFF0C\u4EC5\u5F53 format \u4E3A jpeg \u65F6\u6709\u6548\uFF0C\
|
|
29091
|
+
description: "JPEG \u8D28\u91CF (0-100)\uFF0C\u4EC5\u5F53 format \u4E3A jpeg \u65F6\u6709\u6548\u3002\u9ED8\u8BA4\u666E\u901A\u622A\u56FE 80\uFF0C\u5B8C\u6574\u957F\u622A\u56FE 70"
|
|
29083
29092
|
},
|
|
29084
29093
|
fullPage: {
|
|
29085
29094
|
type: "boolean",
|
|
@@ -29192,20 +29201,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
29192
29201
|
try {
|
|
29193
29202
|
if (name === "inspect_page") {
|
|
29194
29203
|
const { selector, includeInteractive = true, maxElements = 30 } = args;
|
|
29195
|
-
const
|
|
29196
|
-
mode: "structured",
|
|
29204
|
+
const snapshot = await askChrome("inspectPageSnapshot", {
|
|
29197
29205
|
selector,
|
|
29198
|
-
|
|
29199
|
-
|
|
29206
|
+
includeInteractive,
|
|
29207
|
+
maxElements
|
|
29200
29208
|
});
|
|
29201
|
-
|
|
29202
|
-
|
|
29203
|
-
interactive = await askChrome("getInteractiveSnapshot", {
|
|
29204
|
-
selector,
|
|
29205
|
-
includeText: true,
|
|
29206
|
-
maxElements
|
|
29207
|
-
});
|
|
29208
|
-
}
|
|
29209
|
+
const page = snapshot?.page;
|
|
29210
|
+
const interactive = snapshot?.interactive ?? null;
|
|
29209
29211
|
const links = page?.counts?.links;
|
|
29210
29212
|
const buttons = page?.counts?.buttons;
|
|
29211
29213
|
const forms = page?.counts?.forms;
|
|
@@ -29357,6 +29359,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
29357
29359
|
}
|
|
29358
29360
|
const metadata = {
|
|
29359
29361
|
format: res.format,
|
|
29362
|
+
...res.quality !== void 0 ? { quality: res.quality } : {},
|
|
29360
29363
|
fullPage: res.fullPage,
|
|
29361
29364
|
width: res.width,
|
|
29362
29365
|
height: res.height,
|
package/extension/background.js
CHANGED
|
@@ -315,6 +315,81 @@ function compareNetworkEntries(a, b, mode = 'debug') {
|
|
|
315
315
|
return (b.timestamp || 0) - (a.timestamp || 0)
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
const MAX_NETWORK_URL_OUTPUT_LENGTH = 240
|
|
319
|
+
const NETWORK_URL_HEAD_LENGTH = 180
|
|
320
|
+
const NETWORK_URL_TAIL_LENGTH = 40
|
|
321
|
+
const MAX_DATA_URL_OUTPUT_LENGTH = 256
|
|
322
|
+
|
|
323
|
+
function summarizeNetworkUrl(url) {
|
|
324
|
+
if (!url) return { displayUrl: url }
|
|
325
|
+
|
|
326
|
+
const urlOriginalLength = url.length
|
|
327
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(url)
|
|
328
|
+
const urlScheme = schemeMatch?.[1]?.toLowerCase()
|
|
329
|
+
|
|
330
|
+
if (urlScheme === 'data') {
|
|
331
|
+
const commaIndex = url.indexOf(',')
|
|
332
|
+
const meta = commaIndex >= 0 ? url.slice(5, commaIndex) : url.slice(5)
|
|
333
|
+
const dataUrlMimeType = (meta.split(';')[0] || 'text/plain').toLowerCase()
|
|
334
|
+
const isBase64 = meta.includes(';base64')
|
|
335
|
+
|
|
336
|
+
if (!isBase64 && urlOriginalLength <= MAX_DATA_URL_OUTPUT_LENGTH) {
|
|
337
|
+
return {
|
|
338
|
+
displayUrl: url,
|
|
339
|
+
urlOriginalLength,
|
|
340
|
+
urlScheme,
|
|
341
|
+
urlTruncated: false,
|
|
342
|
+
dataUrlMimeType,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
displayUrl: `data:${dataUrlMimeType}${isBase64 ? ';base64' : ''},<omitted ${urlOriginalLength} chars>`,
|
|
348
|
+
urlOriginalLength,
|
|
349
|
+
urlScheme,
|
|
350
|
+
urlTruncated: true,
|
|
351
|
+
dataUrlMimeType,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (urlOriginalLength > MAX_NETWORK_URL_OUTPUT_LENGTH) {
|
|
356
|
+
return {
|
|
357
|
+
displayUrl: `${url.slice(0, NETWORK_URL_HEAD_LENGTH)}...${url.slice(-NETWORK_URL_TAIL_LENGTH)}`,
|
|
358
|
+
urlOriginalLength,
|
|
359
|
+
urlScheme,
|
|
360
|
+
urlTruncated: true,
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
displayUrl: url,
|
|
366
|
+
urlOriginalLength,
|
|
367
|
+
urlScheme,
|
|
368
|
+
urlTruncated: false,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function buildNetworkRequestSummary(entry) {
|
|
373
|
+
const urlMeta = summarizeNetworkUrl(entry.url)
|
|
374
|
+
return {
|
|
375
|
+
requestId: entry.requestId,
|
|
376
|
+
url: urlMeta.displayUrl,
|
|
377
|
+
...(urlMeta.urlTruncated ? { urlTruncated: true, urlOriginalLength: urlMeta.urlOriginalLength } : {}),
|
|
378
|
+
...(urlMeta.urlScheme ? { urlScheme: urlMeta.urlScheme } : {}),
|
|
379
|
+
...(urlMeta.dataUrlMimeType ? { dataUrlMimeType: urlMeta.dataUrlMimeType } : {}),
|
|
380
|
+
method: entry.method,
|
|
381
|
+
status: entry.status,
|
|
382
|
+
statusCode: entry.statusCode,
|
|
383
|
+
resourceType: entry.resourceType,
|
|
384
|
+
mimeType: entry.mimeType,
|
|
385
|
+
duration: entry.duration,
|
|
386
|
+
encodedDataLength: entry.encodedDataLength,
|
|
387
|
+
fromCache: entry.fromCache,
|
|
388
|
+
timestamp: entry.timestamp,
|
|
389
|
+
errorText: entry.errorText,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
318
393
|
function trimNetworkRequests() {
|
|
319
394
|
while (networkRequests.length > CONFIG.maxRequestsTracked) {
|
|
320
395
|
let worstIndex = 0
|
|
@@ -398,46 +473,57 @@ function compactStack(stackTrace) {
|
|
|
398
473
|
|
|
399
474
|
// ========== Debugger ๆไฝ ==========
|
|
400
475
|
|
|
476
|
+
// attach ไบๆฅ้๏ผ้ฒๆญขๅนถๅ่ฐ็จ ensureAttached ๅฏผ่ด้ๅค attach / ็ถๆ็ซๆ
|
|
477
|
+
let _attachLock = Promise.resolve()
|
|
478
|
+
|
|
401
479
|
async function ensureAttached() {
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
480
|
+
let _release
|
|
481
|
+
const _prev = _attachLock
|
|
482
|
+
_attachLock = new Promise(r => _release = r)
|
|
483
|
+
await _prev
|
|
484
|
+
try {
|
|
485
|
+
if (!state.enabled) throw new Error("ๆฉๅฑๅทฒๆๅ๏ผ็นๅปๅพๆ ๅผๅฏๅๅ่ฏ")
|
|
486
|
+
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
|
|
487
|
+
if (!tab) throw new Error("ๆฒกๆๆฟๆดป็ๆ ็ญพ้กต")
|
|
488
|
+
if (attachedTabId !== tab.id) {
|
|
489
|
+
if (attachedTabId) {
|
|
490
|
+
try { await chrome.debugger.detach({ tabId: attachedTabId }) } catch (e) {}
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
await chrome.debugger.attach({ tabId: tab.id }, "1.3")
|
|
415
494
|
setBadgeState("on")
|
|
416
|
-
}
|
|
417
|
-
|
|
495
|
+
} catch (e) {
|
|
496
|
+
attachedTabId = null
|
|
497
|
+
if (state.connected) {
|
|
498
|
+
setBadgeState("on")
|
|
499
|
+
} else {
|
|
500
|
+
setBadgeState("att")
|
|
501
|
+
}
|
|
502
|
+
throw e
|
|
418
503
|
}
|
|
419
|
-
|
|
504
|
+
attachedTabId = tab.id
|
|
505
|
+
scriptMap = new Map()
|
|
506
|
+
scriptSourceCache = new Map()
|
|
507
|
+
networkRequests = []
|
|
508
|
+
requestMap = new Map()
|
|
509
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Runtime.enable")
|
|
510
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Log.enable")
|
|
511
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Console.enable").catch(() => {})
|
|
512
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Debugger.enable")
|
|
513
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Profiler.enable")
|
|
514
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Network.enable").catch(() => {})
|
|
515
|
+
|
|
516
|
+
// Enable auto-attach to sub-targets (iframes, workers) for comprehensive capture
|
|
517
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Target.setAutoAttach", {
|
|
518
|
+
autoAttach: true,
|
|
519
|
+
waitForDebuggerOnStart: false,
|
|
520
|
+
flatten: true,
|
|
521
|
+
}).catch(() => {})
|
|
420
522
|
}
|
|
421
|
-
attachedTabId
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
requestMap = new Map()
|
|
426
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Runtime.enable")
|
|
427
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Log.enable")
|
|
428
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Console.enable").catch(() => {})
|
|
429
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Debugger.enable")
|
|
430
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Profiler.enable")
|
|
431
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Network.enable").catch(() => {})
|
|
432
|
-
|
|
433
|
-
// Enable auto-attach to sub-targets (iframes, workers) for comprehensive capture
|
|
434
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Target.setAutoAttach", {
|
|
435
|
-
autoAttach: true,
|
|
436
|
-
waitForDebuggerOnStart: false,
|
|
437
|
-
flatten: true,
|
|
438
|
-
}).catch(() => {})
|
|
439
|
-
}
|
|
440
|
-
return { tabId: attachedTabId }
|
|
523
|
+
return { tabId: attachedTabId }
|
|
524
|
+
} finally {
|
|
525
|
+
_release()
|
|
526
|
+
}
|
|
441
527
|
}
|
|
442
528
|
|
|
443
529
|
async function maybeDetach(force = false) {
|
|
@@ -666,20 +752,7 @@ async function handleListNetworkRequests(params = {}) {
|
|
|
666
752
|
total: networkRequests.length + requestMap.size,
|
|
667
753
|
filtered: results.length,
|
|
668
754
|
priorityMode,
|
|
669
|
-
requests: results.map(
|
|
670
|
-
requestId: r.requestId,
|
|
671
|
-
url: r.url,
|
|
672
|
-
method: r.method,
|
|
673
|
-
status: r.status,
|
|
674
|
-
statusCode: r.statusCode,
|
|
675
|
-
resourceType: r.resourceType,
|
|
676
|
-
mimeType: r.mimeType,
|
|
677
|
-
duration: r.duration,
|
|
678
|
-
encodedDataLength: r.encodedDataLength,
|
|
679
|
-
fromCache: r.fromCache,
|
|
680
|
-
timestamp: r.timestamp,
|
|
681
|
-
errorText: r.errorText,
|
|
682
|
-
})),
|
|
755
|
+
requests: results.map(buildNetworkRequestSummary),
|
|
683
756
|
}
|
|
684
757
|
}
|
|
685
758
|
|
|
@@ -692,7 +765,20 @@ async function handleGetNetworkDetail(params = {}) {
|
|
|
692
765
|
if (!entry) entry = networkRequests.find(r => r.requestId === requestId)
|
|
693
766
|
if (!entry) throw new Error(`ๆชๆพๅฐ่ฏทๆฑ: ${requestId}`)
|
|
694
767
|
|
|
695
|
-
const
|
|
768
|
+
const urlMeta = summarizeNetworkUrl(entry.url)
|
|
769
|
+
const result = {
|
|
770
|
+
...entry,
|
|
771
|
+
url: urlMeta.displayUrl,
|
|
772
|
+
...(urlMeta.urlTruncated ? { urlTruncated: true, urlOriginalLength: urlMeta.urlOriginalLength } : {}),
|
|
773
|
+
...(urlMeta.urlScheme ? { urlScheme: urlMeta.urlScheme } : {}),
|
|
774
|
+
...(urlMeta.dataUrlMimeType ? { dataUrlMimeType: urlMeta.dataUrlMimeType } : {}),
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (urlMeta.urlTruncated) {
|
|
778
|
+
result.urlNote = urlMeta.urlScheme === 'data'
|
|
779
|
+
? 'ไธบ้ฟๅ
ไธไธๆ่จ่๏ผdata URL ๅทฒๆ่ฆๅๅฑ็คบใ'
|
|
780
|
+
: 'ไธบ้ฟๅ
ไธไธๆ่จ่๏ผ่ถ
้ฟ URL ๅทฒๆ่ฆๅๅฑ็คบใ'
|
|
781
|
+
}
|
|
696
782
|
|
|
697
783
|
if (includeBody && entry.status !== "pending" && entry.status !== "failed") {
|
|
698
784
|
try {
|
|
@@ -879,13 +965,17 @@ function roundMs(seconds) {
|
|
|
879
965
|
|
|
880
966
|
async function handleCaptureScreenshot(params = {}) {
|
|
881
967
|
const target = await ensureAttached()
|
|
882
|
-
const { format
|
|
968
|
+
const { format: requestedFormat, quality: requestedQuality, fullPage = false, clip } = params
|
|
969
|
+
const format = requestedFormat || 'jpeg'
|
|
970
|
+
const quality = format === 'jpeg'
|
|
971
|
+
? (requestedQuality ?? (fullPage ? 70 : 80))
|
|
972
|
+
: undefined
|
|
883
973
|
|
|
884
974
|
await chrome.debugger.sendCommand(target, 'Page.enable')
|
|
885
975
|
|
|
886
976
|
let captureParams = {
|
|
887
977
|
format,
|
|
888
|
-
...(
|
|
978
|
+
...(format === 'jpeg' ? { quality } : {}),
|
|
889
979
|
}
|
|
890
980
|
|
|
891
981
|
if (clip) {
|
|
@@ -927,7 +1017,7 @@ async function handleCaptureScreenshot(params = {}) {
|
|
|
927
1017
|
}
|
|
928
1018
|
await chrome.debugger.sendCommand(target, 'Emulation.clearDeviceMetricsOverride').catch(() => {})
|
|
929
1019
|
return {
|
|
930
|
-
imageData: data, format, fullPage: true, width: maxWidth, height: maxHeight,
|
|
1020
|
+
imageData: data, format, quality, fullPage: true, width: maxWidth, height: maxHeight,
|
|
931
1021
|
note: pageSize.height > maxHeight ? `้กต้ข้ซๅบฆ ${pageSize.height}px ่ถ
่ฟ้ๅถ๏ผๅทฒๆชๅๅ ${maxHeight}px` : undefined,
|
|
932
1022
|
}
|
|
933
1023
|
} catch (e) {
|
|
@@ -943,7 +1033,207 @@ async function handleCaptureScreenshot(params = {}) {
|
|
|
943
1033
|
returnByValue: true,
|
|
944
1034
|
})
|
|
945
1035
|
|
|
946
|
-
return {
|
|
1036
|
+
return {
|
|
1037
|
+
imageData: data,
|
|
1038
|
+
format,
|
|
1039
|
+
quality,
|
|
1040
|
+
fullPage: false,
|
|
1041
|
+
width: sizeResult?.value?.width,
|
|
1042
|
+
height: sizeResult?.value?.height
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function handleInspectPageSnapshot(params = {}) {
|
|
1047
|
+
const target = await ensureAttached()
|
|
1048
|
+
const { selector, includeInteractive = true, maxElements = 30 } = params
|
|
1049
|
+
|
|
1050
|
+
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
1051
|
+
|
|
1052
|
+
const expression = `(function() {
|
|
1053
|
+
try {
|
|
1054
|
+
if (document.readyState === 'loading') {
|
|
1055
|
+
return { error: '้กต้ขๅฐๆชๅ ่ฝฝๅฎๆ๏ผ่ฏท็จๅ้่ฏ', readyState: document.readyState };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const includeInteractive = ${includeInteractive};
|
|
1059
|
+
const maxEls = ${maxElements};
|
|
1060
|
+
const selector = ${selectorStr};
|
|
1061
|
+
const result = {};
|
|
1062
|
+
let targetElement = document.body;
|
|
1063
|
+
|
|
1064
|
+
if (selector) {
|
|
1065
|
+
try {
|
|
1066
|
+
targetElement = document.querySelector(selector);
|
|
1067
|
+
if (!targetElement) {
|
|
1068
|
+
return { error: '้ๆฉๅจๆชๅน้
ๅฐไปปไฝๅ
็ด ', selector: selector, suggestion: '่ฏทๆฃๆฅ้ๆฉๅจๆฏๅฆๆญฃ็กฎ' };
|
|
1069
|
+
}
|
|
1070
|
+
result.selector = selector;
|
|
1071
|
+
result.matchedTag = targetElement.tagName.toLowerCase();
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
return { error: 'ๆ ๆ็ CSS ้ๆฉๅจ: ' + e.message, selector: selector };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
result.metadata = {
|
|
1078
|
+
title: document.title || '',
|
|
1079
|
+
url: window.location.href,
|
|
1080
|
+
description: document.querySelector('meta[name="description"]')?.content || '',
|
|
1081
|
+
keywords: document.querySelector('meta[name="keywords"]')?.content || '',
|
|
1082
|
+
charset: document.characterSet,
|
|
1083
|
+
language: document.documentElement.lang || '',
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
const structured = {};
|
|
1087
|
+
const headings = targetElement.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
1088
|
+
structured.headings = Array.from(headings).slice(0, 50).map(h => ({
|
|
1089
|
+
level: parseInt(h.tagName[1]),
|
|
1090
|
+
text: h.innerText.trim().slice(0, 200)
|
|
1091
|
+
}));
|
|
1092
|
+
const links = targetElement.querySelectorAll('a[href]');
|
|
1093
|
+
structured.links = Array.from(links).slice(0, 100).map(a => ({
|
|
1094
|
+
text: (a.innerText || '').trim().slice(0, 100),
|
|
1095
|
+
href: a.href
|
|
1096
|
+
})).filter(l => l.href && !l.href.startsWith('javascript:'));
|
|
1097
|
+
const buttons = targetElement.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]');
|
|
1098
|
+
structured.buttons = Array.from(buttons).slice(0, 50).map(b => ({
|
|
1099
|
+
text: (b.innerText || b.value || b.getAttribute('aria-label') || '').trim().slice(0, 100),
|
|
1100
|
+
type: b.type || 'button',
|
|
1101
|
+
disabled: b.disabled || false
|
|
1102
|
+
}));
|
|
1103
|
+
const forms = targetElement.querySelectorAll('form');
|
|
1104
|
+
structured.forms = Array.from(forms).slice(0, 20).map(f => {
|
|
1105
|
+
const fields = Array.from(f.querySelectorAll('input, select, textarea')).slice(0, 30);
|
|
1106
|
+
return {
|
|
1107
|
+
action: f.action || '',
|
|
1108
|
+
method: (f.method || 'GET').toUpperCase(),
|
|
1109
|
+
fieldCount: fields.length,
|
|
1110
|
+
fields: fields.map(field => ({
|
|
1111
|
+
tag: field.tagName.toLowerCase(),
|
|
1112
|
+
type: field.type || '',
|
|
1113
|
+
name: field.name || '',
|
|
1114
|
+
placeholder: field.placeholder || '',
|
|
1115
|
+
required: field.required || false
|
|
1116
|
+
}))
|
|
1117
|
+
};
|
|
1118
|
+
});
|
|
1119
|
+
const images = targetElement.querySelectorAll('img');
|
|
1120
|
+
structured.images = Array.from(images).slice(0, 50).map(img => ({
|
|
1121
|
+
alt: img.alt || '',
|
|
1122
|
+
src: img.src ? img.src.slice(0, 200) : ''
|
|
1123
|
+
})).filter(img => img.src);
|
|
1124
|
+
const tables = targetElement.querySelectorAll('table');
|
|
1125
|
+
structured.tables = Array.from(tables).slice(0, 10).map(table => {
|
|
1126
|
+
const headers = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim().slice(0, 50));
|
|
1127
|
+
const rows = table.querySelectorAll('tr');
|
|
1128
|
+
return { headers: headers.slice(0, 20), rowCount: rows.length };
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
result.page = {
|
|
1132
|
+
metadata: result.metadata,
|
|
1133
|
+
...(result.selector ? { selector: result.selector, matchedTag: result.matchedTag } : {}),
|
|
1134
|
+
structured,
|
|
1135
|
+
counts: {
|
|
1136
|
+
headings: structured.headings.length,
|
|
1137
|
+
links: structured.links.length,
|
|
1138
|
+
buttons: structured.buttons.length,
|
|
1139
|
+
forms: structured.forms.length,
|
|
1140
|
+
images: structured.images.length,
|
|
1141
|
+
tables: structured.tables.length
|
|
1142
|
+
},
|
|
1143
|
+
mode: 'structured'
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
if (!includeInteractive) {
|
|
1147
|
+
result.interactive = null;
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
let refCounter = 0;
|
|
1152
|
+
const elements = [];
|
|
1153
|
+
const INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="radio"],[role="switch"],[role="combobox"],[tabindex]:not([tabindex="-1"]),[contenteditable="true"],[onclick]';
|
|
1154
|
+
|
|
1155
|
+
function isVisible(el) {
|
|
1156
|
+
const style = window.getComputedStyle(el);
|
|
1157
|
+
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return null;
|
|
1158
|
+
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
1159
|
+
style.position !== 'fixed' && style.position !== 'sticky') return null;
|
|
1160
|
+
const rect = el.getBoundingClientRect();
|
|
1161
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
1162
|
+
return rect;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function buildEntry(el, rect) {
|
|
1166
|
+
refCounter++;
|
|
1167
|
+
const ref = 'e' + refCounter;
|
|
1168
|
+
el.setAttribute('data-ghost-ref', ref);
|
|
1169
|
+
const tag = el.tagName.toLowerCase();
|
|
1170
|
+
const entry = { ref, tag, cx: Math.round(rect.left + rect.width / 2), cy: Math.round(rect.top + rect.height / 2) };
|
|
1171
|
+
if (el.type) entry.type = el.type;
|
|
1172
|
+
if (el.name) entry.name = el.name;
|
|
1173
|
+
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
1174
|
+
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
1175
|
+
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
1176
|
+
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
1177
|
+
if (tag === 'select') {
|
|
1178
|
+
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
1179
|
+
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
1180
|
+
}));
|
|
1181
|
+
}
|
|
1182
|
+
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
1183
|
+
if (text && text.length <= 100) entry.text = text;
|
|
1184
|
+
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
1185
|
+
if (el.disabled) entry.disabled = true;
|
|
1186
|
+
return entry;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function scanRoot(root) {
|
|
1190
|
+
const candidates = root.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
1191
|
+
for (let i = 0; i < candidates.length && elements.length < maxEls; i++) {
|
|
1192
|
+
const rect = isVisible(candidates[i]);
|
|
1193
|
+
if (rect) elements.push(buildEntry(candidates[i], rect));
|
|
1194
|
+
}
|
|
1195
|
+
if (elements.length < maxEls) {
|
|
1196
|
+
const all = root.querySelectorAll('*');
|
|
1197
|
+
for (let i = 0; i < all.length && elements.length < maxEls; i++) {
|
|
1198
|
+
const el = all[i];
|
|
1199
|
+
if (el.shadowRoot) scanRoot(el.shadowRoot);
|
|
1200
|
+
if (el.onclick && !el.hasAttribute('data-ghost-ref')) {
|
|
1201
|
+
const rect = isVisible(el);
|
|
1202
|
+
if (rect) elements.push(buildEntry(el, rect));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
document.querySelectorAll('[data-ghost-ref]').forEach(el => el.removeAttribute('data-ghost-ref'));
|
|
1209
|
+
scanRoot(targetElement);
|
|
1210
|
+
|
|
1211
|
+
result.interactive = {
|
|
1212
|
+
url: window.location.href,
|
|
1213
|
+
title: document.title,
|
|
1214
|
+
elementCount: elements.length,
|
|
1215
|
+
viewport: {
|
|
1216
|
+
width: window.innerWidth,
|
|
1217
|
+
height: window.innerHeight,
|
|
1218
|
+
scrollX: Math.round(window.scrollX),
|
|
1219
|
+
scrollY: Math.round(window.scrollY),
|
|
1220
|
+
},
|
|
1221
|
+
elements
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
return result;
|
|
1225
|
+
} catch (e) {
|
|
1226
|
+
return { error: e.message };
|
|
1227
|
+
}
|
|
1228
|
+
})()`
|
|
1229
|
+
|
|
1230
|
+
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
1231
|
+
expression,
|
|
1232
|
+
returnByValue: true,
|
|
1233
|
+
})
|
|
1234
|
+
|
|
1235
|
+
if (result?.value?.error) throw new Error(result.value.error)
|
|
1236
|
+
return result?.value
|
|
947
1237
|
}
|
|
948
1238
|
|
|
949
1239
|
async function handleGetPageContent(params = {}) {
|
|
@@ -1061,78 +1351,65 @@ async function handleGetInteractiveSnapshot(params = {}) {
|
|
|
1061
1351
|
let refCounter = 0;
|
|
1062
1352
|
const elements = [];
|
|
1063
1353
|
|
|
1064
|
-
|
|
1354
|
+
const maxEls = ${maxElements};
|
|
1355
|
+
// ๅ้้้ๆฉๅจโโ็จๆต่งๅจๅ็้ๆฉๅจๅผๆไปฃๆฟๅ
จๆ JS ้ๅฝ
|
|
1356
|
+
const INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="radio"],[role="switch"],[role="combobox"],[tabindex]:not([tabindex="-1"]),[contenteditable="true"],[onclick]';
|
|
1357
|
+
|
|
1358
|
+
// ๅฏ่งๆงๆฃๆต๏ผๅๆฌก getComputedStyle๏ผ่ฟๅ rect ๅค็จ๏ผ
|
|
1065
1359
|
function isVisible(el) {
|
|
1066
|
-
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
1067
|
-
window.getComputedStyle(el).position !== 'fixed' &&
|
|
1068
|
-
window.getComputedStyle(el).position !== 'sticky') return false;
|
|
1069
1360
|
const style = window.getComputedStyle(el);
|
|
1070
|
-
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return
|
|
1361
|
+
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return null;
|
|
1362
|
+
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
1363
|
+
style.position !== 'fixed' && style.position !== 'sticky') return null;
|
|
1071
1364
|
const rect = el.getBoundingClientRect();
|
|
1072
|
-
if (rect.width === 0 && rect.height === 0) return
|
|
1073
|
-
return
|
|
1365
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
1366
|
+
return rect;
|
|
1074
1367
|
}
|
|
1075
1368
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1369
|
+
function buildEntry(el, rect) {
|
|
1370
|
+
refCounter++;
|
|
1371
|
+
const ref = 'e' + refCounter;
|
|
1372
|
+
el.setAttribute('data-ghost-ref', ref);
|
|
1078
1373
|
const tag = el.tagName.toLowerCase();
|
|
1079
|
-
|
|
1080
|
-
if (el.
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1374
|
+
const entry = { ref, tag, cx: Math.round(rect.left + rect.width / 2), cy: Math.round(rect.top + rect.height / 2) };
|
|
1375
|
+
if (el.type) entry.type = el.type;
|
|
1376
|
+
if (el.name) entry.name = el.name;
|
|
1377
|
+
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
1378
|
+
if (${includeText}) {
|
|
1379
|
+
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
1380
|
+
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
1381
|
+
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
1382
|
+
if (tag === 'select') {
|
|
1383
|
+
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
1384
|
+
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
1385
|
+
}));
|
|
1386
|
+
}
|
|
1387
|
+
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
1388
|
+
if (text && text.length <= 100) entry.text = text;
|
|
1389
|
+
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
1390
|
+
}
|
|
1391
|
+
if (el.disabled) entry.disabled = true;
|
|
1392
|
+
return entry;
|
|
1088
1393
|
}
|
|
1089
1394
|
|
|
1090
|
-
//
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (elements.
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
cy: Math.round(rect.top + rect.height / 2),
|
|
1108
|
-
};
|
|
1109
|
-
// ็ฑปๅไฟกๆฏ
|
|
1110
|
-
if (el.type) entry.type = el.type;
|
|
1111
|
-
if (el.name) entry.name = el.name;
|
|
1112
|
-
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
1113
|
-
// ๆๆฌไฟกๆฏ
|
|
1114
|
-
if (${includeText}) {
|
|
1115
|
-
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
1116
|
-
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
1117
|
-
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
1118
|
-
if (tag === 'select') {
|
|
1119
|
-
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
1120
|
-
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
1121
|
-
}));
|
|
1122
|
-
}
|
|
1123
|
-
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
1124
|
-
if (text && text.length <= 100) entry.text = text;
|
|
1125
|
-
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
1395
|
+
// ๅ้้ๆซๆ๏ผๅซ Shadow DOM ็ฉฟ้๏ผ
|
|
1396
|
+
function scanRoot(root) {
|
|
1397
|
+
const candidates = root.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
1398
|
+
for (let i = 0; i < candidates.length && elements.length < maxEls; i++) {
|
|
1399
|
+
const rect = isVisible(candidates[i]);
|
|
1400
|
+
if (rect) elements.push(buildEntry(candidates[i], rect));
|
|
1401
|
+
}
|
|
1402
|
+
// ็ฉฟ้ Shadow DOM + ๅ
ๅบๆฃๆต el.onclick = fn ๅฝขๅผ็ JS ๅฑๆง็ปๅฎ
|
|
1403
|
+
if (elements.length < maxEls) {
|
|
1404
|
+
const all = root.querySelectorAll('*');
|
|
1405
|
+
for (let i = 0; i < all.length && elements.length < maxEls; i++) {
|
|
1406
|
+
const el = all[i];
|
|
1407
|
+
if (el.shadowRoot) scanRoot(el.shadowRoot);
|
|
1408
|
+
// CSS ้ๆฉๅจๅช่ฝๅน้
[onclick] ๅฑๆง๏ผ่ฟ้ๅ
ไฝ el.onclick = fn ็ๆ
ๅต
|
|
1409
|
+
if (el.onclick && !el.hasAttribute('data-ghost-ref')) {
|
|
1410
|
+
const rect = isVisible(el);
|
|
1411
|
+
if (rect) elements.push(buildEntry(el, rect));
|
|
1126
1412
|
}
|
|
1127
|
-
// disabled ็ถๆ
|
|
1128
|
-
if (el.disabled) entry.disabled = true;
|
|
1129
|
-
elements.push(entry);
|
|
1130
|
-
}
|
|
1131
|
-
// ้ๅฝๅญ่็น
|
|
1132
|
-
walkDOM(el, root);
|
|
1133
|
-
// ็ฉฟ้ Shadow DOM
|
|
1134
|
-
if (el.shadowRoot) {
|
|
1135
|
-
walkDOM(el.shadowRoot, root);
|
|
1136
1413
|
}
|
|
1137
1414
|
}
|
|
1138
1415
|
}
|
|
@@ -1140,7 +1417,6 @@ async function handleGetInteractiveSnapshot(params = {}) {
|
|
|
1140
1417
|
// ๆธ
็ๆง็ ref ๆ ่ฎฐ
|
|
1141
1418
|
document.querySelectorAll('[data-ghost-ref]').forEach(el => el.removeAttribute('data-ghost-ref'));
|
|
1142
1419
|
|
|
1143
|
-
// ็กฎๅฎๆซๆๆ น่็น
|
|
1144
1420
|
let rootEl = document.body;
|
|
1145
1421
|
const sel = ${selectorStr};
|
|
1146
1422
|
if (sel) {
|
|
@@ -1148,7 +1424,7 @@ async function handleGetInteractiveSnapshot(params = {}) {
|
|
|
1148
1424
|
if (!rootEl) return { error: '้ๆฉๅจๆชๅน้
ๅฐไปปไฝๅ
็ด ', selector: sel };
|
|
1149
1425
|
}
|
|
1150
1426
|
|
|
1151
|
-
|
|
1427
|
+
scanRoot(rootEl);
|
|
1152
1428
|
|
|
1153
1429
|
return {
|
|
1154
1430
|
url: window.location.href,
|
|
@@ -1368,6 +1644,7 @@ async function handleCommand(message) {
|
|
|
1368
1644
|
else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests()
|
|
1369
1645
|
else if (command === "perfMetrics") result = await handlePerfMetrics(params)
|
|
1370
1646
|
else if (command === "captureScreenshot") result = await handleCaptureScreenshot(params)
|
|
1647
|
+
else if (command === "inspectPageSnapshot") result = await handleInspectPageSnapshot(params)
|
|
1371
1648
|
else if (command === "getPageContent") result = await handleGetPageContent(params)
|
|
1372
1649
|
else if (command === "getInteractiveSnapshot") result = await handleGetInteractiveSnapshot(params)
|
|
1373
1650
|
else if (command === "dispatchAction") result = await handleDispatchAction(params)
|
package/extension/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghost-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Ghost Bridge: Zero-restart Chrome debugger bridge for Claude MCP. Includes CLI for easy setup.",
|
|
@@ -48,5 +48,8 @@
|
|
|
48
48
|
"fs-extra": "^11.1.1",
|
|
49
49
|
"js-beautify": "^1.14.11",
|
|
50
50
|
"ws": "^8.14.2"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"ghost-bridge": "^0.5.2"
|
|
51
54
|
}
|
|
52
55
|
}
|