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 CHANGED
@@ -1,183 +1,159 @@
1
1
  # ๐Ÿ‘ป Ghost Bridge
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/ghost-bridge.svg?style=flat-square)](https://www.npmjs.com/package/ghost-bridge)
4
+ [![npm total downloads](https://img.shields.io/npm/dt/ghost-bridge.svg?style=flat-square&label=downloads)](https://www.npmjs.com/package/ghost-bridge)
4
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
6
 
6
- > **Zero-restart Chrome AI Copilot** โ€” Subvert your workflow. Allow AI to seamlessly take over the browser you're already using, enabling real-time debugging, visual observation, and interactive manipulation without launching a new Chrome instance.
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
- ## โœจ Why Ghost Bridge?
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
- - ๐Ÿ”Œ **Zero-Config Attach** โ€” Bypasses the need for `--remote-debugging-port`. Captures Chrome's native DevTools Protocol directly via an extension.
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
- ## ๐Ÿš€ Quick Start
21
+ ## What's New in 0.6.1
23
22
 
24
- ### 1. Install & Initialize
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
- > `ghost-bridge init` currently writes:
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
- ## ๐Ÿ—๏ธ Architecture
48
+ If your MCP client is not auto-detected, add one of these manually.
153
49
 
154
- ```mermaid
155
- graph LR
156
- A[Claude CLI/Desktop] <-->|stdio| B(MCP Server\nserver.js)
157
- B <-->|WebSocket| C(Chrome Extension\nbackground.js)
158
- C <-->|CDP| D[Browser Tab\nTarget Context]
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
- - **MCP Server**: Spawned by Claude via standard I/O streams. Orchestrates WS connections.
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
- ## โš ๏ธ Known Limitations
85
+ ### 3. Connect
168
86
 
169
- - **Service Workers Suspending**: MV3 background workers may suspend. We've built robust auto-reconnection logic, but prolonged inactivity might require re-toggling.
170
- - **DevTools Conflict**: If you manually open Chrome DevTools (F12) on the target tab, `chrome.debugger.attach` may be rejected.
171
- - **Beautify Overhead**: Beautifying massive single-line bundles is expensive; the server will auto-truncate overly large scripts.
172
- - **Cross-Origin OOPIF**: Elements and errors deeply embedded in strict Cross-Origin Iframes might evade the primary debugger hook without further multi-target attach logic.
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
- ## ๐Ÿค Contributing
150
+ ## Limitations
177
151
 
178
- Contributions, issues, and feature requests are welcome!
179
- Check out our [Contributing Guide](CONTRIBUTING.md) to get started building tools or improving the bridge.
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
- ## ๐Ÿ“„ License
157
+ ## License
182
158
 
183
- This project is [MIT](LICENSE) licensed.
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(({ reject, timer }) => {
28817
- clearTimeout(timer);
28818
- reject(new Error(message));
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\uFF0C\u9ED8\u8BA4 png\uFF08\u65E0\u635F\uFF09\uFF0Cjpeg \u66F4\u5C0F"
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\u5EFA\u8BAE 80"
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 page = await askChrome("getPageContent", {
29196
- mode: "structured",
29204
+ const snapshot = await askChrome("inspectPageSnapshot", {
29197
29205
  selector,
29198
- maxLength: 2e4,
29199
- includeMetadata: true
29206
+ includeInteractive,
29207
+ maxElements
29200
29208
  });
29201
- let interactive = null;
29202
- if (includeInteractive) {
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,
@@ -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
- if (!state.enabled) throw new Error("ๆ‰ฉๅฑ•ๅทฒๆš‚ๅœ๏ผŒ็‚นๅ‡ปๅ›พๆ ‡ๅผ€ๅฏๅŽๅ†่ฏ•")
403
- const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
404
- if (!tab) throw new Error("ๆฒกๆœ‰ๆฟ€ๆดป็š„ๆ ‡็ญพ้กต")
405
- if (attachedTabId !== tab.id) {
406
- if (attachedTabId) {
407
- try { await chrome.debugger.detach({ tabId: attachedTabId }) } catch (e) {}
408
- }
409
- try {
410
- await chrome.debugger.attach({ tabId: tab.id }, "1.3")
411
- setBadgeState("on")
412
- } catch (e) {
413
- attachedTabId = null
414
- if (state.connected) {
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
- } else {
417
- setBadgeState("att")
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
- throw e
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 = tab.id
422
- scriptMap = new Map()
423
- scriptSourceCache = new Map()
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(() => {})
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(r => ({
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 result = { ...entry }
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 = 'png', quality, fullPage = false, clip } = params
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
- ...(quality !== undefined && format === 'jpeg' ? { quality } : {}),
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 { imageData: data, format, fullPage: false, width: sizeResult?.value?.width, height: sizeResult?.value?.height }
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 false;
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 false;
1073
- return true;
1365
+ if (rect.width === 0 && rect.height === 0) return null;
1366
+ return rect;
1074
1367
  }
1075
1368
 
1076
- // ๅˆคๆ–ญๅ…ƒ็ด ๆ˜ฏๅฆๅฏไบคไบ’
1077
- function isInteractive(el) {
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
- if (['a', 'button', 'input', 'select', 'textarea'].includes(tag)) return true;
1080
- if (el.getAttribute('role') === 'button' || el.getAttribute('role') === 'link' ||
1081
- el.getAttribute('role') === 'tab' || el.getAttribute('role') === 'menuitem' ||
1082
- el.getAttribute('role') === 'checkbox' || el.getAttribute('role') === 'radio' ||
1083
- el.getAttribute('role') === 'switch' || el.getAttribute('role') === 'combobox') return true;
1084
- if (el.getAttribute('tabindex') && parseInt(el.getAttribute('tabindex')) >= 0) return true;
1085
- if (el.getAttribute('contenteditable') === 'true') return true;
1086
- if (el.onclick || el.getAttribute('onclick')) return true;
1087
- return false;
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
- // ้€’ๅฝ’้ๅކ DOM ๆ ‘๏ผˆๅซ Shadow DOM๏ผ‰
1091
- function walkDOM(node, root) {
1092
- if (elements.length >= ${maxElements}) return;
1093
- const children = node.children || [];
1094
- for (let i = 0; i < children.length; i++) {
1095
- if (elements.length >= ${maxElements}) return;
1096
- const el = children[i];
1097
- if (isInteractive(el) && isVisible(el)) {
1098
- refCounter++;
1099
- const ref = 'e' + refCounter;
1100
- el.setAttribute('data-ghost-ref', ref);
1101
- const tag = el.tagName.toLowerCase();
1102
- const rect = el.getBoundingClientRect();
1103
- const entry = {
1104
- ref: ref,
1105
- tag: tag,
1106
- cx: Math.round(rect.left + rect.width / 2),
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
- walkDOM(rootEl, rootEl);
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)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Ghost Bridge",
4
- "version": "0.5.2",
4
+ "version": "0.6.1",
5
5
  "description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
6
6
  "permissions": [
7
7
  "debugger",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost-bridge",
3
- "version": "0.5.2",
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
  }