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 CHANGED
@@ -1,183 +1,151 @@
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.0
23
22
 
24
- ### 1. Install & Initialize
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
- > `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.
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
- ## ๐Ÿ—๏ธ Architecture
44
+ If your MCP client is not auto-detected, add one of these manually.
153
45
 
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]
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
- - **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.
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
- ## โš ๏ธ Known Limitations
81
+ ### 3. Connect
168
82
 
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.
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
- ## ๐Ÿค Contributing
142
+ ## Limitations
177
143
 
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.
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
- ## ๐Ÿ“„ License
149
+ ## License
182
150
 
183
- This project is [MIT](LICENSE) licensed.
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(({ 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
  }
@@ -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,
@@ -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
- 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) {
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
- } else {
417
- setBadgeState("att")
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
- throw e
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 = 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(() => {})
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 = 'png', quality, fullPage = false, clip } = params
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
- ...(quality !== undefined && format === 'jpeg' ? { quality } : {}),
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 { imageData: data, format, fullPage: false, width: sizeResult?.value?.width, height: sizeResult?.value?.height }
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 false;
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 false;
1073
- return true;
1290
+ if (rect.width === 0 && rect.height === 0) return null;
1291
+ return rect;
1074
1292
  }
1075
1293
 
1076
- // ๅˆคๆ–ญๅ…ƒ็ด ๆ˜ฏๅฆๅฏไบคไบ’
1077
- function isInteractive(el) {
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
- 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;
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
- // ้€’ๅฝ’้ๅކ 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) + '...';
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
- walkDOM(rootEl, rootEl);
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)
@@ -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.0",
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.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
  }