ghost-bridge 0.5.2 โ 0.6.0
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 +121 -153
- package/dist/server.js +21 -18
- package/extension/background.js +306 -104
- package/extension/manifest.json +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,183 +1,151 @@
|
|
|
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.0
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
- `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
|
|
24
|
+
- `capture_screenshot` now defaults to JPEG for better transfer efficiency: visible viewport screenshots default to `quality: 80`, and `fullPage` screenshots default to `quality: 70`
|
|
25
|
+
- Use `format: "png"` when you need high-fidelity text rendering, 1px lines, icon edges, transparency, or pixel-level UI inspection
|
|
26
|
+
- Attachment and request cleanup paths are more robust under concurrent usage and multi-client reconnect scenarios
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### 1. Install
|
|
25
31
|
|
|
26
32
|
```bash
|
|
27
|
-
# Install globally
|
|
28
33
|
npm install -g ghost-bridge
|
|
29
|
-
|
|
30
|
-
# Auto-configure MCP (Claude Code, Codex, Cursor, Antigravity) and prepare the extension directory
|
|
31
34
|
ghost-bridge init
|
|
32
35
|
```
|
|
33
36
|
|
|
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.
|
|
37
|
+
`ghost-bridge init` currently writes config for:
|
|
149
38
|
|
|
150
|
-
|
|
39
|
+
- Claude Code: `~/.claude/settings.json` or `~/.claude.json`
|
|
40
|
+
- Codex: `~/.codex/config.toml`
|
|
41
|
+
- Cursor: `~/.cursor/mcp.json`
|
|
42
|
+
- Antigravity: `~/.gemini/antigravity/mcp.json`
|
|
151
43
|
|
|
152
|
-
|
|
44
|
+
If your MCP client is not auto-detected, add one of these manually.
|
|
153
45
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
46
|
+
JSON config:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"ghost-bridge": {
|
|
52
|
+
"command": "/absolute/path/to/node",
|
|
53
|
+
"args": ["/absolute/path/to/global/node_modules/ghost-bridge/dist/server.js"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Codex TOML:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[mcp_servers.ghost-bridge]
|
|
63
|
+
type = "stdio"
|
|
64
|
+
command = "/absolute/path/to/node"
|
|
65
|
+
args = ["/absolute/path/to/global/node_modules/ghost-bridge/dist/server.js"]
|
|
159
66
|
```
|
|
160
67
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
68
|
+
### 2. Load the Extension
|
|
69
|
+
|
|
70
|
+
1. Open `chrome://extensions`
|
|
71
|
+
2. Enable Developer mode
|
|
72
|
+
3. Click `Load unpacked`
|
|
73
|
+
4. Select `~/.ghost-bridge/extension`
|
|
164
74
|
|
|
165
|
-
|
|
75
|
+
You can also run:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ghost-bridge extension --open
|
|
79
|
+
```
|
|
166
80
|
|
|
167
|
-
|
|
81
|
+
### 3. Connect
|
|
168
82
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
83
|
+
1. Click the Ghost Bridge extension icon
|
|
84
|
+
2. Click `Connect`
|
|
85
|
+
3. Wait until the status becomes `ON`
|
|
86
|
+
4. Open your MCP client and start working on the current page
|
|
173
87
|
|
|
174
|
-
|
|
88
|
+
Typical prompts:
|
|
89
|
+
|
|
90
|
+
- `Analyze the current page`
|
|
91
|
+
- `Check why this layout is broken`
|
|
92
|
+
- `Inspect the DOM structure`
|
|
93
|
+
- `Click the login button and submit the form`
|
|
94
|
+
|
|
95
|
+
## Tools
|
|
96
|
+
|
|
97
|
+
| Tool | Purpose |
|
|
98
|
+
|------|---------|
|
|
99
|
+
| `inspect_page` | Default entry point for page analysis |
|
|
100
|
+
| `capture_screenshot` | Visual inspection and UI debugging |
|
|
101
|
+
| `get_page_content` | Text, HTML, and structured DOM extraction |
|
|
102
|
+
| `get_interactive_snapshot` | Find clickable and editable elements |
|
|
103
|
+
| `dispatch_action` | Click, fill, press, scroll, hover, select |
|
|
104
|
+
| `list_network_requests` | Inspect captured network traffic |
|
|
105
|
+
| `get_network_detail` | Read one request in detail |
|
|
106
|
+
| `get_last_error` | Inspect recent errors and exceptions |
|
|
107
|
+
| `get_script_source` | Extract page scripts |
|
|
108
|
+
| `find_by_string` | Search within bundled script content |
|
|
109
|
+
| `coverage_snapshot` | Identify active scripts quickly |
|
|
110
|
+
| `perf_metrics` | Collect Web Vitals and engine metrics |
|
|
111
|
+
|
|
112
|
+
Recommended flow:
|
|
113
|
+
|
|
114
|
+
1. Start with `inspect_page`
|
|
115
|
+
2. Use `capture_screenshot` for visual issues
|
|
116
|
+
Default is optimized for transfer with JPEG; switch to `png` for pixel-level checks
|
|
117
|
+
3. Use `get_page_content` for DOM or text extraction
|
|
118
|
+
4. Use `get_interactive_snapshot` before `dispatch_action`
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
| Setting | Default | Notes |
|
|
123
|
+
|---------|---------|-------|
|
|
124
|
+
| Port | `33333` | Set `GHOST_BRIDGE_PORT` to override |
|
|
125
|
+
| Token | Monthly UUID | Set `GHOST_BRIDGE_TOKEN` to override |
|
|
126
|
+
| Auto detach | `false` | Keeps debugger attached for ongoing capture |
|
|
127
|
+
|
|
128
|
+
## Architecture
|
|
129
|
+
|
|
130
|
+
```mermaid
|
|
131
|
+
flowchart LR
|
|
132
|
+
A["AI Client<br/>Claude / Codex / Cursor"]
|
|
133
|
+
B["Ghost Bridge MCP Server<br/>server.js"]
|
|
134
|
+
C["Chrome Extension<br/>background.js"]
|
|
135
|
+
D["Browser Tab<br/>Target Context"]
|
|
136
|
+
|
|
137
|
+
A <-->|"stdio"| B
|
|
138
|
+
B <-->|"WebSocket"| C
|
|
139
|
+
C <-->|"CDP"| D
|
|
140
|
+
```
|
|
175
141
|
|
|
176
|
-
##
|
|
142
|
+
## Limitations
|
|
177
143
|
|
|
178
|
-
|
|
179
|
-
|
|
144
|
+
- Chrome DevTools on the target tab can conflict with `chrome.debugger.attach`
|
|
145
|
+
- MV3 background lifecycle can still cause reconnect scenarios after long idle periods
|
|
146
|
+
- Very large minified bundles may be truncated during beautify or extraction
|
|
147
|
+
- Deep cross-origin iframe cases are not fully covered yet
|
|
180
148
|
|
|
181
|
-
##
|
|
149
|
+
## License
|
|
182
150
|
|
|
183
|
-
|
|
151
|
+
[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
|
}
|
|
@@ -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
|
@@ -398,46 +398,57 @@ function compactStack(stackTrace) {
|
|
|
398
398
|
|
|
399
399
|
// ========== Debugger ๆไฝ ==========
|
|
400
400
|
|
|
401
|
+
// attach ไบๆฅ้๏ผ้ฒๆญขๅนถๅ่ฐ็จ ensureAttached ๅฏผ่ด้ๅค attach / ็ถๆ็ซๆ
|
|
402
|
+
let _attachLock = Promise.resolve()
|
|
403
|
+
|
|
401
404
|
async function ensureAttached() {
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
405
|
+
let _release
|
|
406
|
+
const _prev = _attachLock
|
|
407
|
+
_attachLock = new Promise(r => _release = r)
|
|
408
|
+
await _prev
|
|
409
|
+
try {
|
|
410
|
+
if (!state.enabled) throw new Error("ๆฉๅฑๅทฒๆๅ๏ผ็นๅปๅพๆ ๅผๅฏๅๅ่ฏ")
|
|
411
|
+
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
|
|
412
|
+
if (!tab) throw new Error("ๆฒกๆๆฟๆดป็ๆ ็ญพ้กต")
|
|
413
|
+
if (attachedTabId !== tab.id) {
|
|
414
|
+
if (attachedTabId) {
|
|
415
|
+
try { await chrome.debugger.detach({ tabId: attachedTabId }) } catch (e) {}
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
await chrome.debugger.attach({ tabId: tab.id }, "1.3")
|
|
415
419
|
setBadgeState("on")
|
|
416
|
-
}
|
|
417
|
-
|
|
420
|
+
} catch (e) {
|
|
421
|
+
attachedTabId = null
|
|
422
|
+
if (state.connected) {
|
|
423
|
+
setBadgeState("on")
|
|
424
|
+
} else {
|
|
425
|
+
setBadgeState("att")
|
|
426
|
+
}
|
|
427
|
+
throw e
|
|
418
428
|
}
|
|
419
|
-
|
|
429
|
+
attachedTabId = tab.id
|
|
430
|
+
scriptMap = new Map()
|
|
431
|
+
scriptSourceCache = new Map()
|
|
432
|
+
networkRequests = []
|
|
433
|
+
requestMap = new Map()
|
|
434
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Runtime.enable")
|
|
435
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Log.enable")
|
|
436
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Console.enable").catch(() => {})
|
|
437
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Debugger.enable")
|
|
438
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Profiler.enable")
|
|
439
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Network.enable").catch(() => {})
|
|
440
|
+
|
|
441
|
+
// Enable auto-attach to sub-targets (iframes, workers) for comprehensive capture
|
|
442
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Target.setAutoAttach", {
|
|
443
|
+
autoAttach: true,
|
|
444
|
+
waitForDebuggerOnStart: false,
|
|
445
|
+
flatten: true,
|
|
446
|
+
}).catch(() => {})
|
|
420
447
|
}
|
|
421
|
-
attachedTabId
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
networkRequests = []
|
|
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(() => {})
|
|
448
|
+
return { tabId: attachedTabId }
|
|
449
|
+
} finally {
|
|
450
|
+
_release()
|
|
439
451
|
}
|
|
440
|
-
return { tabId: attachedTabId }
|
|
441
452
|
}
|
|
442
453
|
|
|
443
454
|
async function maybeDetach(force = false) {
|
|
@@ -879,13 +890,17 @@ function roundMs(seconds) {
|
|
|
879
890
|
|
|
880
891
|
async function handleCaptureScreenshot(params = {}) {
|
|
881
892
|
const target = await ensureAttached()
|
|
882
|
-
const { format
|
|
893
|
+
const { format: requestedFormat, quality: requestedQuality, fullPage = false, clip } = params
|
|
894
|
+
const format = requestedFormat || 'jpeg'
|
|
895
|
+
const quality = format === 'jpeg'
|
|
896
|
+
? (requestedQuality ?? (fullPage ? 70 : 80))
|
|
897
|
+
: undefined
|
|
883
898
|
|
|
884
899
|
await chrome.debugger.sendCommand(target, 'Page.enable')
|
|
885
900
|
|
|
886
901
|
let captureParams = {
|
|
887
902
|
format,
|
|
888
|
-
...(
|
|
903
|
+
...(format === 'jpeg' ? { quality } : {}),
|
|
889
904
|
}
|
|
890
905
|
|
|
891
906
|
if (clip) {
|
|
@@ -927,7 +942,7 @@ async function handleCaptureScreenshot(params = {}) {
|
|
|
927
942
|
}
|
|
928
943
|
await chrome.debugger.sendCommand(target, 'Emulation.clearDeviceMetricsOverride').catch(() => {})
|
|
929
944
|
return {
|
|
930
|
-
imageData: data, format, fullPage: true, width: maxWidth, height: maxHeight,
|
|
945
|
+
imageData: data, format, quality, fullPage: true, width: maxWidth, height: maxHeight,
|
|
931
946
|
note: pageSize.height > maxHeight ? `้กต้ข้ซๅบฆ ${pageSize.height}px ่ถ
่ฟ้ๅถ๏ผๅทฒๆชๅๅ ${maxHeight}px` : undefined,
|
|
932
947
|
}
|
|
933
948
|
} catch (e) {
|
|
@@ -943,7 +958,207 @@ async function handleCaptureScreenshot(params = {}) {
|
|
|
943
958
|
returnByValue: true,
|
|
944
959
|
})
|
|
945
960
|
|
|
946
|
-
return {
|
|
961
|
+
return {
|
|
962
|
+
imageData: data,
|
|
963
|
+
format,
|
|
964
|
+
quality,
|
|
965
|
+
fullPage: false,
|
|
966
|
+
width: sizeResult?.value?.width,
|
|
967
|
+
height: sizeResult?.value?.height
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function handleInspectPageSnapshot(params = {}) {
|
|
972
|
+
const target = await ensureAttached()
|
|
973
|
+
const { selector, includeInteractive = true, maxElements = 30 } = params
|
|
974
|
+
|
|
975
|
+
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
976
|
+
|
|
977
|
+
const expression = `(function() {
|
|
978
|
+
try {
|
|
979
|
+
if (document.readyState === 'loading') {
|
|
980
|
+
return { error: '้กต้ขๅฐๆชๅ ่ฝฝๅฎๆ๏ผ่ฏท็จๅ้่ฏ', readyState: document.readyState };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const includeInteractive = ${includeInteractive};
|
|
984
|
+
const maxEls = ${maxElements};
|
|
985
|
+
const selector = ${selectorStr};
|
|
986
|
+
const result = {};
|
|
987
|
+
let targetElement = document.body;
|
|
988
|
+
|
|
989
|
+
if (selector) {
|
|
990
|
+
try {
|
|
991
|
+
targetElement = document.querySelector(selector);
|
|
992
|
+
if (!targetElement) {
|
|
993
|
+
return { error: '้ๆฉๅจๆชๅน้
ๅฐไปปไฝๅ
็ด ', selector: selector, suggestion: '่ฏทๆฃๆฅ้ๆฉๅจๆฏๅฆๆญฃ็กฎ' };
|
|
994
|
+
}
|
|
995
|
+
result.selector = selector;
|
|
996
|
+
result.matchedTag = targetElement.tagName.toLowerCase();
|
|
997
|
+
} catch (e) {
|
|
998
|
+
return { error: 'ๆ ๆ็ CSS ้ๆฉๅจ: ' + e.message, selector: selector };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
result.metadata = {
|
|
1003
|
+
title: document.title || '',
|
|
1004
|
+
url: window.location.href,
|
|
1005
|
+
description: document.querySelector('meta[name="description"]')?.content || '',
|
|
1006
|
+
keywords: document.querySelector('meta[name="keywords"]')?.content || '',
|
|
1007
|
+
charset: document.characterSet,
|
|
1008
|
+
language: document.documentElement.lang || '',
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const structured = {};
|
|
1012
|
+
const headings = targetElement.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
1013
|
+
structured.headings = Array.from(headings).slice(0, 50).map(h => ({
|
|
1014
|
+
level: parseInt(h.tagName[1]),
|
|
1015
|
+
text: h.innerText.trim().slice(0, 200)
|
|
1016
|
+
}));
|
|
1017
|
+
const links = targetElement.querySelectorAll('a[href]');
|
|
1018
|
+
structured.links = Array.from(links).slice(0, 100).map(a => ({
|
|
1019
|
+
text: (a.innerText || '').trim().slice(0, 100),
|
|
1020
|
+
href: a.href
|
|
1021
|
+
})).filter(l => l.href && !l.href.startsWith('javascript:'));
|
|
1022
|
+
const buttons = targetElement.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]');
|
|
1023
|
+
structured.buttons = Array.from(buttons).slice(0, 50).map(b => ({
|
|
1024
|
+
text: (b.innerText || b.value || b.getAttribute('aria-label') || '').trim().slice(0, 100),
|
|
1025
|
+
type: b.type || 'button',
|
|
1026
|
+
disabled: b.disabled || false
|
|
1027
|
+
}));
|
|
1028
|
+
const forms = targetElement.querySelectorAll('form');
|
|
1029
|
+
structured.forms = Array.from(forms).slice(0, 20).map(f => {
|
|
1030
|
+
const fields = Array.from(f.querySelectorAll('input, select, textarea')).slice(0, 30);
|
|
1031
|
+
return {
|
|
1032
|
+
action: f.action || '',
|
|
1033
|
+
method: (f.method || 'GET').toUpperCase(),
|
|
1034
|
+
fieldCount: fields.length,
|
|
1035
|
+
fields: fields.map(field => ({
|
|
1036
|
+
tag: field.tagName.toLowerCase(),
|
|
1037
|
+
type: field.type || '',
|
|
1038
|
+
name: field.name || '',
|
|
1039
|
+
placeholder: field.placeholder || '',
|
|
1040
|
+
required: field.required || false
|
|
1041
|
+
}))
|
|
1042
|
+
};
|
|
1043
|
+
});
|
|
1044
|
+
const images = targetElement.querySelectorAll('img');
|
|
1045
|
+
structured.images = Array.from(images).slice(0, 50).map(img => ({
|
|
1046
|
+
alt: img.alt || '',
|
|
1047
|
+
src: img.src ? img.src.slice(0, 200) : ''
|
|
1048
|
+
})).filter(img => img.src);
|
|
1049
|
+
const tables = targetElement.querySelectorAll('table');
|
|
1050
|
+
structured.tables = Array.from(tables).slice(0, 10).map(table => {
|
|
1051
|
+
const headers = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim().slice(0, 50));
|
|
1052
|
+
const rows = table.querySelectorAll('tr');
|
|
1053
|
+
return { headers: headers.slice(0, 20), rowCount: rows.length };
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
result.page = {
|
|
1057
|
+
metadata: result.metadata,
|
|
1058
|
+
...(result.selector ? { selector: result.selector, matchedTag: result.matchedTag } : {}),
|
|
1059
|
+
structured,
|
|
1060
|
+
counts: {
|
|
1061
|
+
headings: structured.headings.length,
|
|
1062
|
+
links: structured.links.length,
|
|
1063
|
+
buttons: structured.buttons.length,
|
|
1064
|
+
forms: structured.forms.length,
|
|
1065
|
+
images: structured.images.length,
|
|
1066
|
+
tables: structured.tables.length
|
|
1067
|
+
},
|
|
1068
|
+
mode: 'structured'
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
if (!includeInteractive) {
|
|
1072
|
+
result.interactive = null;
|
|
1073
|
+
return result;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
let refCounter = 0;
|
|
1077
|
+
const elements = [];
|
|
1078
|
+
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]';
|
|
1079
|
+
|
|
1080
|
+
function isVisible(el) {
|
|
1081
|
+
const style = window.getComputedStyle(el);
|
|
1082
|
+
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return null;
|
|
1083
|
+
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
1084
|
+
style.position !== 'fixed' && style.position !== 'sticky') return null;
|
|
1085
|
+
const rect = el.getBoundingClientRect();
|
|
1086
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
1087
|
+
return rect;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function buildEntry(el, rect) {
|
|
1091
|
+
refCounter++;
|
|
1092
|
+
const ref = 'e' + refCounter;
|
|
1093
|
+
el.setAttribute('data-ghost-ref', ref);
|
|
1094
|
+
const tag = el.tagName.toLowerCase();
|
|
1095
|
+
const entry = { ref, tag, cx: Math.round(rect.left + rect.width / 2), cy: Math.round(rect.top + rect.height / 2) };
|
|
1096
|
+
if (el.type) entry.type = el.type;
|
|
1097
|
+
if (el.name) entry.name = el.name;
|
|
1098
|
+
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
1099
|
+
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
1100
|
+
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
1101
|
+
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
1102
|
+
if (tag === 'select') {
|
|
1103
|
+
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
1104
|
+
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
1105
|
+
}));
|
|
1106
|
+
}
|
|
1107
|
+
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
1108
|
+
if (text && text.length <= 100) entry.text = text;
|
|
1109
|
+
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
1110
|
+
if (el.disabled) entry.disabled = true;
|
|
1111
|
+
return entry;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function scanRoot(root) {
|
|
1115
|
+
const candidates = root.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
1116
|
+
for (let i = 0; i < candidates.length && elements.length < maxEls; i++) {
|
|
1117
|
+
const rect = isVisible(candidates[i]);
|
|
1118
|
+
if (rect) elements.push(buildEntry(candidates[i], rect));
|
|
1119
|
+
}
|
|
1120
|
+
if (elements.length < maxEls) {
|
|
1121
|
+
const all = root.querySelectorAll('*');
|
|
1122
|
+
for (let i = 0; i < all.length && elements.length < maxEls; i++) {
|
|
1123
|
+
const el = all[i];
|
|
1124
|
+
if (el.shadowRoot) scanRoot(el.shadowRoot);
|
|
1125
|
+
if (el.onclick && !el.hasAttribute('data-ghost-ref')) {
|
|
1126
|
+
const rect = isVisible(el);
|
|
1127
|
+
if (rect) elements.push(buildEntry(el, rect));
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
document.querySelectorAll('[data-ghost-ref]').forEach(el => el.removeAttribute('data-ghost-ref'));
|
|
1134
|
+
scanRoot(targetElement);
|
|
1135
|
+
|
|
1136
|
+
result.interactive = {
|
|
1137
|
+
url: window.location.href,
|
|
1138
|
+
title: document.title,
|
|
1139
|
+
elementCount: elements.length,
|
|
1140
|
+
viewport: {
|
|
1141
|
+
width: window.innerWidth,
|
|
1142
|
+
height: window.innerHeight,
|
|
1143
|
+
scrollX: Math.round(window.scrollX),
|
|
1144
|
+
scrollY: Math.round(window.scrollY),
|
|
1145
|
+
},
|
|
1146
|
+
elements
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
return result;
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
return { error: e.message };
|
|
1152
|
+
}
|
|
1153
|
+
})()`
|
|
1154
|
+
|
|
1155
|
+
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
1156
|
+
expression,
|
|
1157
|
+
returnByValue: true,
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
if (result?.value?.error) throw new Error(result.value.error)
|
|
1161
|
+
return result?.value
|
|
947
1162
|
}
|
|
948
1163
|
|
|
949
1164
|
async function handleGetPageContent(params = {}) {
|
|
@@ -1061,78 +1276,65 @@ async function handleGetInteractiveSnapshot(params = {}) {
|
|
|
1061
1276
|
let refCounter = 0;
|
|
1062
1277
|
const elements = [];
|
|
1063
1278
|
|
|
1064
|
-
|
|
1279
|
+
const maxEls = ${maxElements};
|
|
1280
|
+
// ๅ้้้ๆฉๅจโโ็จๆต่งๅจๅ็้ๆฉๅจๅผๆไปฃๆฟๅ
จๆ JS ้ๅฝ
|
|
1281
|
+
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]';
|
|
1282
|
+
|
|
1283
|
+
// ๅฏ่งๆงๆฃๆต๏ผๅๆฌก getComputedStyle๏ผ่ฟๅ rect ๅค็จ๏ผ
|
|
1065
1284
|
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
1285
|
const style = window.getComputedStyle(el);
|
|
1070
|
-
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return
|
|
1286
|
+
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return null;
|
|
1287
|
+
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
1288
|
+
style.position !== 'fixed' && style.position !== 'sticky') return null;
|
|
1071
1289
|
const rect = el.getBoundingClientRect();
|
|
1072
|
-
if (rect.width === 0 && rect.height === 0) return
|
|
1073
|
-
return
|
|
1290
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
1291
|
+
return rect;
|
|
1074
1292
|
}
|
|
1075
1293
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1294
|
+
function buildEntry(el, rect) {
|
|
1295
|
+
refCounter++;
|
|
1296
|
+
const ref = 'e' + refCounter;
|
|
1297
|
+
el.setAttribute('data-ghost-ref', ref);
|
|
1078
1298
|
const tag = el.tagName.toLowerCase();
|
|
1079
|
-
|
|
1080
|
-
if (el.
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1299
|
+
const entry = { ref, tag, cx: Math.round(rect.left + rect.width / 2), cy: Math.round(rect.top + rect.height / 2) };
|
|
1300
|
+
if (el.type) entry.type = el.type;
|
|
1301
|
+
if (el.name) entry.name = el.name;
|
|
1302
|
+
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
1303
|
+
if (${includeText}) {
|
|
1304
|
+
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
1305
|
+
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
1306
|
+
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
1307
|
+
if (tag === 'select') {
|
|
1308
|
+
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
1309
|
+
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
1310
|
+
}));
|
|
1311
|
+
}
|
|
1312
|
+
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
1313
|
+
if (text && text.length <= 100) entry.text = text;
|
|
1314
|
+
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
1315
|
+
}
|
|
1316
|
+
if (el.disabled) entry.disabled = true;
|
|
1317
|
+
return entry;
|
|
1088
1318
|
}
|
|
1089
1319
|
|
|
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) + '...';
|
|
1320
|
+
// ๅ้้ๆซๆ๏ผๅซ Shadow DOM ็ฉฟ้๏ผ
|
|
1321
|
+
function scanRoot(root) {
|
|
1322
|
+
const candidates = root.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
1323
|
+
for (let i = 0; i < candidates.length && elements.length < maxEls; i++) {
|
|
1324
|
+
const rect = isVisible(candidates[i]);
|
|
1325
|
+
if (rect) elements.push(buildEntry(candidates[i], rect));
|
|
1326
|
+
}
|
|
1327
|
+
// ็ฉฟ้ Shadow DOM + ๅ
ๅบๆฃๆต el.onclick = fn ๅฝขๅผ็ JS ๅฑๆง็ปๅฎ
|
|
1328
|
+
if (elements.length < maxEls) {
|
|
1329
|
+
const all = root.querySelectorAll('*');
|
|
1330
|
+
for (let i = 0; i < all.length && elements.length < maxEls; i++) {
|
|
1331
|
+
const el = all[i];
|
|
1332
|
+
if (el.shadowRoot) scanRoot(el.shadowRoot);
|
|
1333
|
+
// CSS ้ๆฉๅจๅช่ฝๅน้
[onclick] ๅฑๆง๏ผ่ฟ้ๅ
ไฝ el.onclick = fn ็ๆ
ๅต
|
|
1334
|
+
if (el.onclick && !el.hasAttribute('data-ghost-ref')) {
|
|
1335
|
+
const rect = isVisible(el);
|
|
1336
|
+
if (rect) elements.push(buildEntry(el, rect));
|
|
1126
1337
|
}
|
|
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
1338
|
}
|
|
1137
1339
|
}
|
|
1138
1340
|
}
|
|
@@ -1140,7 +1342,6 @@ async function handleGetInteractiveSnapshot(params = {}) {
|
|
|
1140
1342
|
// ๆธ
็ๆง็ ref ๆ ่ฎฐ
|
|
1141
1343
|
document.querySelectorAll('[data-ghost-ref]').forEach(el => el.removeAttribute('data-ghost-ref'));
|
|
1142
1344
|
|
|
1143
|
-
// ็กฎๅฎๆซๆๆ น่็น
|
|
1144
1345
|
let rootEl = document.body;
|
|
1145
1346
|
const sel = ${selectorStr};
|
|
1146
1347
|
if (sel) {
|
|
@@ -1148,7 +1349,7 @@ async function handleGetInteractiveSnapshot(params = {}) {
|
|
|
1148
1349
|
if (!rootEl) return { error: '้ๆฉๅจๆชๅน้
ๅฐไปปไฝๅ
็ด ', selector: sel };
|
|
1149
1350
|
}
|
|
1150
1351
|
|
|
1151
|
-
|
|
1352
|
+
scanRoot(rootEl);
|
|
1152
1353
|
|
|
1153
1354
|
return {
|
|
1154
1355
|
url: window.location.href,
|
|
@@ -1368,6 +1569,7 @@ async function handleCommand(message) {
|
|
|
1368
1569
|
else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests()
|
|
1369
1570
|
else if (command === "perfMetrics") result = await handlePerfMetrics(params)
|
|
1370
1571
|
else if (command === "captureScreenshot") result = await handleCaptureScreenshot(params)
|
|
1572
|
+
else if (command === "inspectPageSnapshot") result = await handleInspectPageSnapshot(params)
|
|
1371
1573
|
else if (command === "getPageContent") result = await handleGetPageContent(params)
|
|
1372
1574
|
else if (command === "getInteractiveSnapshot") result = await handleGetInteractiveSnapshot(params)
|
|
1373
1575
|
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.0",
|
|
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
|
}
|